Localization
:::info Status Outline — expand with a walkthrough of adding a new language and a new localized string to a configuration. :::
The stacked-module chain
LocalizationChain lets multiple localization modules stack, with the most-recently
registered module winning per key and falling through to earlier modules for anything it
doesn't define. Register your configuration's module with
services.AddLocalizationModule<T>().
Ordering rule: services.AddLocalization() must be called before
services.AddLocalizationModule(). The composite localizer registration must use Replace,
not TryAdd, or it silently loses to the framework default. Both of these bit the platform
once historically — there's a regression test (LocalizationChainTests) covering it now.
This site vs. application localization
This documentation site's own multilingual setup (Docusaurus i18n, EN/UK) is a separate
concern from the application's runtime localization described above — see the repo root
README.md's "Maintaining translations" section for how this site's translation tracking
works.
Time zones (adjacent, often confused with localization)
The server stores/emits DateTime in UTC everywhere. Conversion to local time happens on
both sides: client-side for display (a registered JsonConverter<DateTime>), server-side for
PDF/CSV export (Kandra.Printing's PrintValueFormatter). The client detects its IANA time
zone natively (TimeZoneInfo.Local + TryConvertWindowsIdToIanaId, no JS interop needed on
.NET 6+ WASM) and sends it as an X-Time-Zone header on every request.
Two gotchas worth knowing:
- A
[Format]-lessDateTimecolumn can silently skip conversion — this shipped as a real bug once, worth checking for on any new print-form date column. - For a calendar-day identity field (not an instant), use
DateOnly, notDateTime— a DTO field that's really "just a date" will otherwise get silently shifted a day by the client'sUtcToLocalDateTimeJsonConverterwhen the local-midnight pick crosses a UTC day boundary. For an effective-instant field where every reader's local "today" genuinely can't agree (e.g. "constant becomes effective at..."), be precise about the editing user's own offset and show both UTC and local in the preview — never add a per-reader timezone selector to try to make everyone see the same instant as "their" midnight, because no instant is everyone's midnight at once.
See also
- Platform doc:
src/KandraCore/CLAUDE.md's "Time zone handling" section (full design + the historical bug).