Localization
SimpleModule includes a built-in localization system that provides multi-language support across both the .NET backend and the React frontend. Translations are stored as embedded JSON resources in each module and automatically discovered at startup.
How It Works
- Each module embeds locale JSON files (e.g.,
en.json,es.json) as assembly resources - At startup, the framework scans all module assemblies and loads translations into frozen (immutable) dictionaries
- On each request, middleware resolves the user's preferred locale
- Translations are injected into Inertia shared props, making them available to all React components
Adding Translations to a Module
1. Create Locale Files
Add a Locales/ directory to your module with JSON files named by locale code:
modules/Products/src/Products/
├── Locales/
│ ├── en.json
│ └── es.jsonTranslation keys use dot notation:
// Locales/en.json
{
"Browse.Title": "Products",
"Browse.Description": "Browse the product catalog.",
"Manage.DeleteConfirm": "Are you sure you want to delete \"{name}\"?"
}// Locales/es.json
{
"Browse.Title": "Productos",
"Browse.Description": "Navegar el catálogo de productos.",
"Manage.DeleteConfirm": "¿Estás seguro de que deseas eliminar \"{name}\"?"
}2. Embed as Resources
In your module's .csproj, mark the locale files as embedded resources:
<ItemGroup>
<EmbeddedResource Include="Locales/*.json" />
</ItemGroup>3. Use Translations in React
Import the useTranslation hook from @simplemodule/client:
import { useTranslation } from '@simplemodule/client';
export default function Browse({ products }) {
const { t, locale } = useTranslation('Products');
return (
<div>
<h1>{t('Browse.Title')}</h1>
<p>{t('Browse.Description')}</p>
</div>
);
}The hook accepts a namespace (your module name) and returns:
t(key, params?)-- translates a key, with optional parameter interpolationlocale-- the current locale string (e.g.,"en","es")
Parameter Interpolation
Use {paramName} placeholders in translation values:
// Translation: "Are you sure you want to delete \"{name}\"?"
t('Manage.DeleteConfirm', { name: product.name })4. Type-Safe Keys (Optional)
Create a keys.ts file for compile-time key safety:
// Locales/keys.ts
export const ProductsKeys = {
Browse: {
Title: 'Browse.Title',
Description: 'Browse.Description',
},
Manage: {
DeleteConfirm: 'Manage.DeleteConfirm',
},
} as const;Then use it in components:
import { ProductsKeys } from '../Locales/keys';
const { t } = useTranslation('Products');
t(ProductsKeys.Browse.Title);Locale Resolution
The LocaleResolutionMiddleware determines the user's locale using this priority order:
- User setting -- the
app.languagesetting stored in the database (cached for 5 minutes) - Accept-Language header -- parsed with quality values (cached for 30 minutes)
- Configuration default --
Localization:DefaultLocalefromappsettings.json - Hardcoded fallback --
"en"
Backend Usage
The localization system integrates with .NET's IStringLocalizer:
public class MyService(IStringLocalizer<MyService> localizer)
{
public string GetWelcome(string name) =>
localizer["welcome", name];
}Fallback Behavior
- If a key is missing for the requested locale, the system falls back to English (
"en") - If the key is missing in English too, the raw key string is returned
Configuration
{
"Localization": {
"DefaultLocale": "en"
}
}Contract Interface
Other modules can access translations programmatically through ILocalizationContracts:
public interface ILocalizationContracts
{
string? GetTranslation(string key, string locale);
IReadOnlyDictionary<string, string> GetAllTranslations(string locale);
IReadOnlyList<string> GetSupportedLocales();
}Next Steps
- Settings -- user-scoped settings that store language preferences
- Inertia.js Integration -- how shared props deliver translations to React
- Modules -- module structure and embedded resources