Skip to content

Commit 3c97890

Browse files
committed
docs/localization: audit and update localization-guide.md
1 parent f05fd4d commit 3c97890

1 file changed

Lines changed: 89 additions & 16 deletions

File tree

docs/guides/localization-guide.md

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,46 +23,51 @@ This approach ensures:
2323

2424
### Supported Languages
2525

26-
The API is configured using **ISO 639-1 language codes**. You can configure either generic codes (e.g., `en`, `pt`) or specific regional cultures (e.g., `en-US`, `pt-PT`).
26+
The API is configured using **ISO 639-1 language codes**. You can configure either generic codes (e.g., `en`, `pt`) or specific regional cultures (e.g., `pt-PT`).
2727

28-
**Standard Configuration (Generic)**:
28+
**Standard Configuration** (`appsettings.json`, section `"Localization"`):
2929
```json
3030
{
3131
"Localization": {
3232
"DefaultCulture": "en",
33-
"SupportedCultures": ["en", "pt", "fr", "de", "es"]
33+
"SupportedCultures": ["en", "pt", "pt-PT", "es", "fr", "de"]
3434
}
3535
}
3636
```
3737

38-
**Regional Configuration (Specific)**:
39-
```json
40-
{
41-
"Localization": {
42-
"DefaultCulture": "en-US",
43-
"SupportedCultures": ["en-US", "en-GB", "pt-PT", "pt-BR"]
44-
}
45-
}
38+
The `LocalizationOptions` class (`src/BookStore.ApiService/Infrastructure/LocalizationOptions.cs`) binds this section. The default/fallback list used when no configuration is present is `["en", "pt", "pt-PT", "es", "fr", "de"]` with `"en"` as the default culture.
39+
40+
The supported cultures are exposed at runtime via:
41+
42+
```
43+
GET /api/config/localization
4644
```
4745

46+
This returns a `LocalizationConfigDto` containing `DefaultCulture` and `SupportedCultures[]`. The Web frontend calls this endpoint at startup to configure its own `RequestLocalizationOptions` (with `"en"` as the fallback if the backend is unavailable).
47+
4848
### Cache Configuration
4949

50-
**Critical**: Localized content is cached using `HybridCache` with the `GetOrCreateLocalizedAsync` extension method, which automatically varies the cache by culture.
50+
**Critical**: Localized content is cached using `HybridCache` with the `GetOrCreateLocalizedAsync` extension method (`src/BookStore.ApiService/Infrastructure/Extensions/HybridCacheExtensions.cs`), which automatically appends `|{CultureInfo.CurrentUICulture.Name}` to the cache key.
51+
52+
In practice, endpoints also include the culture explicitly in the base key alongside tenant and query parameters:
5153

5254
```csharp
55+
var culture = CultureInfo.CurrentUICulture.Name;
56+
var cacheKey = $"categories:tenant={tenantContext.TenantId}:culture={culture}:search={request.Search}:page={paging.Page}:size={paging.PageSize}:sort={normalizedSortBy}:{normalizedSortOrder}";
57+
5358
var response = await cache.GetOrCreateLocalizedAsync(
54-
$"category:{id}",
59+
cacheKey, // GetOrCreateLocalizedAsync appends "|{culture}" to this
5560
async cancel => { /* load from database */ },
5661
options: new HybridCacheEntryOptions
5762
{
5863
Expiration = TimeSpan.FromMinutes(5),
5964
LocalCacheExpiration = TimeSpan.FromMinutes(2)
6065
},
61-
tags: [$"category:{id}"],
66+
tags: [CacheTags.CategoryList],
6267
token: cancellationToken);
6368
```
6469

65-
The cache key automatically becomes `category:{id}|{culture}` (e.g., `category:123|en-US`, `category:123|pt-PT`).
70+
The final stored key is `categories:tenant=...:culture=en:...|en`. The culture appears twice (once in the explicit key for transparency, once appended by `GetOrCreateLocalizedAsync`), ensuring correct variation by tenant, culture, and query parameters.
6671

6772
## Translation Storage
6873

@@ -173,7 +178,7 @@ static async Task<Ok<PagedListDto<CategoryDto>>> GetCategories(
173178
[FromServices] IOptions<LocalizationOptions> localizationOptions,
174179
HttpContext context)
175180
{
176-
var culture = CultureInfo.CurrentCulture.Name;
181+
var culture = CultureInfo.CurrentUICulture.Name;
177182
var defaultCulture = localizationOptions.Value.DefaultCulture;
178183
await using var session = store.QuerySession();
179184

@@ -218,6 +223,74 @@ public class CategoryProjection
218223
}
219224
```
220225

226+
## Web Frontend Localization
227+
228+
### How Culture Flows from Web to API
229+
230+
The `BookStoreHeaderHandler` (`src/BookStore.Client/Infrastructure/BookStoreHeaderHandler.cs`) is a `DelegatingHandler` applied to all Refit clients. It automatically adds the `Accept-Language` header when absent:
231+
232+
```csharp
233+
if (!request.Headers.Contains("Accept-Language"))
234+
{
235+
var culture = CultureInfo.CurrentUICulture.Name;
236+
if (!string.IsNullOrEmpty(culture))
237+
{
238+
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(culture));
239+
}
240+
}
241+
```
242+
243+
This means the Blazor circuit's current UI culture is propagated transparently to every API call.
244+
245+
### `RequestLocalizationOptions` in the Web
246+
247+
At startup, `BookStore.Web/Program.cs` calls `GET /api/config/localization` to fetch the supported cultures from the API and configures its own `RequestLocalizationOptions` accordingly:
248+
249+
```csharp
250+
var localizationConfig = await configClient.GetLocalizationConfigAsync();
251+
supportedCultures = [.. localizationConfig.SupportedCultures];
252+
defaultCulture = localizationConfig.DefaultCulture;
253+
254+
var localizationOptions = new RequestLocalizationOptions()
255+
.SetDefaultCulture(defaultCulture)
256+
.AddSupportedCultures(supportedCultures)
257+
.AddSupportedUICultures(supportedCultures);
258+
259+
app.UseRequestLocalization(localizationOptions);
260+
```
261+
262+
If the backend is unreachable at startup, the Web falls back to `["en"]` with default culture `"en"`.
263+
264+
### `LanguageService`
265+
266+
`src/BookStore.Web/Services/LanguageService.cs` wraps the configuration endpoint with an in-memory cache:
267+
268+
- `GetSupportedLanguagesAsync()` — returns the supported culture codes (cached after first call, with fallback to `["en", "pt", "pt-PT", "es", "fr", "de"]` on error).
269+
- `GetLanguagesWithDisplayNamesAsync()` — returns a `Dictionary<string, string>` mapping code → display name in the current UI language.
270+
- `GetDefaultCultureAsync()` — returns the configured default culture (fallback: `"en"`).
271+
- `GetDisplayName(string cultureCode)` — static helper converting a culture code to its .NET display name.
272+
- `GetAllLanguages()` — enumerates all .NET cultures (used for selecting a book's primary written language, not the UI language).
273+
274+
### Language Selector Components
275+
276+
Two Blazor components provide language selection in the UI:
277+
278+
| Component | File | Purpose |
279+
|---|---|---|
280+
| `<LanguageSelector>` | `Components/Shared/LanguageSelector.razor` | Selects a UI/content language from the **supported** cultures returned by the API. Shows "(Default)" next to the configured default. |
281+
| `<AllLanguageSelector>` | `Components/Shared/AllLanguageSelector.razor` | Selects from **all** .NET cultures. Used in admin dialogs to set a book's primary written language. |
282+
283+
### Error Message Localization (`IStringLocalizer`)
284+
285+
The Web project uses standard ASP.NET Core resource-based localization for UI error messages:
286+
287+
- `AddLocalization(options => options.ResourcesPath = "Resources")` is registered in `Program.cs`.
288+
- `src/BookStore.Web/Services/ErrorLocalizationService.cs` injects `IStringLocalizer<ErrorLocalizationService>` and looks up error codes from resource files.
289+
- Default (English) strings live in `src/BookStore.Web/Resources/Services/ErrorLocalizationService.resx`.
290+
- To add a translated version, add `ErrorLocalizationService.{culture}.resx` (e.g., `ErrorLocalizationService.pt.resx`) in the same folder.
291+
292+
This is distinct from the dictionary-based content localization used by the API — `.resx` files only cover UI error messages in the Web project.
293+
221294
## Testing
222295

223296
### Test Different Languages

0 commit comments

Comments
 (0)