RFC: Introduce i18n to the renderer (chrome strings + locale-aware dates)#1995
RFC: Introduce i18n to the renderer (chrome strings + locale-aware dates)#1995oliveregger wants to merge 12 commits into
Conversation
|
Tracking issue: #1996 — background/motivation for this RFC. |
Add a locale-driven catalog for renderer-owned UI text ("chrome") plus
locale-aware date formatting/validation, configured via the existing
rendererConfigStore (new `locale` + optional `rendererStrings` override).
- RendererStrings catalog with English defaults; bundled de-CH/fr-CH/it-CH
catalogs authored as JSON translation files (translator-tool friendly)
- Boolean Yes/No labels sourced from the catalog
- Date input/display format derived from the active locale via Intl (all
locales, no bundled locale data), with an optional `dateFormat` override
- Order-aware date validation (DD/MM, MM/DD US, YYYY/MM/DD)
- Localized, interpolated date validation error messages
- Swiss dayjs locale data for date-picker calendar localization
- Storybook: de-CH boolean/date stories; wrapper forwards rendererConfigOptions
and resets config per story for isolation
- Tests: catalog resolution, typed key + placeholder guards, format
derivation, US ordering, interpolation
Backward compatible: defaults are unchanged (English, DD/MM/YYYY); all
localization is opt-in via config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Describes the two-layer i18n model, the chrome string catalog, locale-aware dates, key design decisions, and next steps. Companion to aehrc#1995 / aehrc#1996. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the hardcoded validation feedback strings into the RendererStrings catalog so they resolve to the active locale. - useValidationFeedback: regex/min-max length/decimal/value/quantity messages (interpolated templates for the limit values) + the required and unknown-issue fallbacks - useDateTimeNonEmpty: "Date is required" - English defaults unchanged; de/fr/it-CH translations added - Keys guard + placeholder-consistency guard extended to cover the new keys Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the terminology autocomplete feedback messages into the RendererStrings catalog (min-characters hint, query error, no-results). - English defaults unchanged; de/fr/it-CH translations added - Keys guard extended Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the choice/open-choice value-set state messages and the form-body
fallbacks into the RendererStrings catalog.
- "No options available.", "Unable to fetch options...", the terminology-server
fetch error (interpolated {valueSet}), autocomplete "Fetching results...",
"Unable to load form", and "Something went wrong here"
- Wired across 8 choice/open-choice field components + the FormBody* containers
and GroupItemSwitcher
- English defaults unchanged; de/fr/it-CH translations added; guards extended
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the interactive-chrome strings (buttons, tooltips, aria-labels) into the
RendererStrings catalog.
- Clear, Remove item, Next/Previous page, Attach/Remove file, Pick a date,
Sync successful/with server, the sync-failed tooltip (interpolated {item}),
Drag row, and the "Form sections" landmark label
- Wired across the clear/remove/page/attachment/date-picker/repopulate/table
components and the tab-list wrapper
- English defaults unchanged; de/fr/it-CH translations added; guards extended
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the accessibility-label chrome into the RendererStrings catalog.
- "Mandatory field" (required-field screen-reader label)
- "Select row {label}" and "Select all rows in {label}" (table selection),
with an interpolated {label}
- "Unnamed {type} item" fallback template used by the table selection labels
- English defaults unchanged; de/fr/it-CH translations added; guards extended
Note: the assorted per-component "Unnamed <x>" a11y fallbacks (e.g. group,
slider, checkbox) are only shown when an item has no text and remain a small
optional follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… batch 6)
Move the per-component fallback accessible labels (shown when an item has no
text) into the RendererStrings catalog.
- Generic "Unnamed {type} item" template applied to the type-based fallbacks
- Specific fallbacks: attachment, nested item, open label, radio group,
checkbox, checkbox list, choice dropdown, group, time field, slider
- Page region labels: "{label} page" / "Unnamed page"
- English defaults unchanged; de/fr/it-CH translations added; guards extended
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ensures the batched i18n changes pass the prettier/prettier lint rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndererStrings Adapt the i18n RFC to the maintainer review in aehrc#1996: - remove the de-CH/fr-CH/it-CH JSON catalogs and the bundledRendererStrings registry; consuming apps own their translation files and inject them via rendererConfigOptions.rendererStrings - resolveRendererStrings(overrides) now merges English defaults <- overrides only; locale no longer selects strings and drives date formatting and calendar localisation exclusively - remove the dayjs/locale/* imports from BaseRenderer; consumers import dayjs locale data themselves for a localised calendar popup - update the Swiss Storybook stories to demonstrate the injection path with an inline catalog (the documented consumer pattern) - adapt tests and i18n.md accordingly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0bc08ea to
dff2afc
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
39e1632 to
9e896e5
Compare
|
Hi @clinnygee, I've updated PR #1995 to address your recommendations:
As proof of the consumer side, our deployment branch hosts the Swiss catalogs in the app and injects them via a locale picker + You can check here that the actions have successfully run: https://github.com/ahdis/smart-forms/actions?query=branch%3Afeat%2Fi18n-renderer-strings++ Happy to adjust anything else — thanks again for the constructive review! From our view the next step would be to create a new PR afterwards to support rendering of mutli-language questionaires, mentioned in `i18n.md' as Layer 2. |
|
Nice work on this — the string-catalog extraction is clean and the English default path stays behavior-preserving. A few things in the locale-aware date logic I'd want to sort out before merge, mostly around the year-first / non-Western locales this RFC is meant to demonstrate. 1. Month-year handling ignores the locale's token orderFull-date validation is nicely order-aware via export function getMonthYearFormat(dateFormat: string): string {
return `MM${getDateSeparator(dateFormat)}YYYY`;
}and const matches = input.split(separator);
if (!validateTwoMatches(matches[0], matches[1])) { ... } // matches[0] assumed to be monthSo for the year-first locales the PR advertises (
Deriving the month-year format and its part order from 3.
|
|
This is what claude has said - she said some other things as well, but those were for wrong. If you could check these out that would be great. After that I am happy to merge it. Also interested to see what you come with for the rendering of mutli-language questionaires, as the .md file did not go into much detail on it. Thanks Oliver! |
Summary — proposal / RFC seeking direction
We'd like to introduce internationalization (i18n) to
@aehrc/smart-forms-rendererand get the core team's feedback on whether this is a direction you'd welcome before we invest further and expand coverage.Today the renderer hardcodes English UI text and an Australian date format, so there's no way to localize it — e.g. we need Ja/Nein and DD.MM.YYYY for Switzerland (German/French/Italian). This PR is an opt-in, backward-compatible foundation that demonstrates the approach on a first slice (boolean labels + the date subsystem), deliberately kept small so the design can be discussed before scaling it out.
This is intentionally opened as a draft — we're looking for agreement on the architecture and naming, not a merge as-is. Translations are also first-pass and would need native-speaker review.
What's included
RendererStrings) with English defaults, configured through the existingrendererConfigStorevia a newlocaleand an optionalrendererStringsoverride — no new config surface or provider.de-CH/fr-CH/it-CHcatalogs authored as JSON translation files.Yes/Nolabels sourced from the catalog.Intl, with an optional explicitdateFormatoverride.DD/MM/YYYY,MM/DD/YYYYUS,YYYY/MM/DD).dayjslocale data so the date-picker calendar popup is localized too.BooleanLocaleDeCH,DateLocaleDeCH) demonstrating the result.Usage
Two layers of i18n (scope of this PR)
We see two distinct concerns and this PR addresses only the first:
item.text,answerOption.display, titles. This is data, and FHIR already standardizes it viaQuestionnaire.language+ thetranslationextension on_text. A separate, larger piece we'd tackle next if you're on board with the overall direction.Key design decisions & reasoning
1. Config-injected catalog, not an i18n framework (i18next/FormatJS/Lingui).
The renderer is a published library. Pulling in a framework would force peer deps, a provider, and a workflow on every consumer. Instead the library ships English defaults + optional bundled locales, and consumers either pass
localeor inject a plainPartial<RendererStrings>object — so an app already using i18next/etc. can feed its own translations in without adopting ours. Zero setup for the common case.2. Bundled catalogs as JSON, not
.tsor.po.JSON is translation-tool friendly (Crowdin/Weblate/Locize/Poedit), needs no runtime parser, and
tscalready emits it tolib/. We keep type safety without a framework: values are typed via theRecord<string, Partial<RendererStrings>>registry, a unit test guards against unknown keys (typos), and another guards placeholder consistency across locales..po(via Lingui) would give the best translator tooling but needs a compile step — worth revisiting only if a translation-management workflow is desired.3. Date format derived from the locale via
Intl, not hardcoded per locale.Intl.DateTimeFormat().formatToParts()yields the correct pattern for every locale (de-CH→DD.MM.YYYY,en-US→MM/DD/YYYY,ja-JP→YYYY/MM/DD) with no bundled locale data and no per-locale import list. An explicitdateFormatoverride remains as an escape hatch. We deliberately kept the renderer's existing custom parser (rather than switching to MUI X's native field or pureIntl) because it supports FHIR partial dates (YYYY,YYYY-MM) that those alternatives don't handle cleanly.4. Order-aware validation.
validateThreeMatchespreviously assumed day-first. It now maps positional parts to day/month/year using the active format, so month-first (US) and other orderings validate correctly — a prerequisite for locale-driven formats.5. Localized messages via
{placeholder}interpolation.Validation messages became catalog templates (e.g.
Input does not match the format {format}.) filled at runtime, so translations stay natural while dynamic values (format, separator) are injected. A test asserts every translation preserves the original placeholders.6.
dayjsSwiss locale imports kept only for the calendar popup.The field format needs no locale import (Intl). The three
dayjs/locale/*imports remain solely to localize the date-picker calendar's month/weekday names for the Swiss locales; consumers wanting this for other locales can import the matching dayjs locale themselves.Backward compatibility
Fully opt-in. With no
locale/rendererStrings, behavior is unchanged: English labels,DD/MM/YYYY, English validation messages. The full renderer test suite passes (2182 tests), including the pre-existing date/boolean tests unchanged.Open questions we'd love feedback on
RendererStrings,locale/rendererStringsonRendererConfig, thei18n/folder — happy to align with your conventions.buildFormcalls unless re-passed; we worked around story isolation in the Storybook wrapper only. Would you wantbuildFormto reset config to defaults per build? (behavioral change, kept out of scope here)translation-extension layer (item.text/answerOption) as a follow-up?.po/Lingui with a compile step for a real translation workflow?Happy to iterate, split this up, or realign to fit your roadmap.
🤖 Generated with Claude Code