Repository navigation
Conversation
The `Translator` interface already shipped as a public export from `@astryxdesign/core/i18n` with zero runtime behind it, and the CLI docs advertised it as unbuilt. Thread it through: `InternationalizationProvider` gains an optional `translator` prop, carried on the context value and consumed at the final format step in `resolve()`. Astryx keeps its own lookup chain — overrides, then `messages`, then the parent locale, then the shipped `en` catalog — and hands the translator the already-resolved ICU message rather than an `@astryx.*` key. That split is forced by the shipped signature `format(message, values?, locale?)`, so no API shape is invented here. Omitting the prop is a no-op: the bundled `intl-messageformat` path and its formatter cache are unchanged, and value-less messages still short-circuit before the translator. Refs meta#4029
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNo new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Visual Regression1 of 420 shot(s) changed. View the report A change here is a question, not a failure: check whether the after is the
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
The translator sat behind the `values === undefined` short-circuit, so 171 of astryx's 228 `t()` callsites never reached it — while the docs promised an app catalog could override any astryx string by keying on its text. Only the interpolating quarter could, and the miss was silent. Move the translator ahead of the short-circuit. A single consumer catalog now owns all of astryx's strings, and issue meta#4029's headline example — `translator` with no `messages` — does what it claims. The bundled path keeps its fast return; omitting the prop costs one extra `!= null` compare. Also: - Guard what `format` returns. A translator is consumer code and its string return type is not enforced at runtime — react-intl hands back a ReactNode[] for rich text, i18next can return null for a miss. Astryx's output feeds aria-label and title, so a non-string warns once in dev and falls back to the resolved message instead of leaking "[object Object]" into the accessibility tree. Exceptions stay uncaught, deliberately: a broken adapter should be loud. - Tolerate `translator={null}`. The prop is typed optional so TS callers cannot pass null, but a JS consumer writing `translator={on ? t : null}` can, and `.format` on null took down every string in the tree. - Document the shapes that bite: a translator holds a function so it only crosses from a client component, a nested provider replaces it rather than inheriting it, and `locale` is the locale asked for — not necessarily the language `message` is written in. - 14 edge tests: odd locales, empty-string catalog entries, parent-locale overrides, placeholder-introducing overrides, live translator swap and removal, nested translators. Mutation testing caught one of the new dep-array tests being fake — the provider's `messages ?? {}` allocates a fresh object per memo recompute, so omitting `messages` invalidated useCallback by itself and `ctx.translator` was never exercised. Pinned to a stable reference; the mutant dies now.
Upstream meta#4269 (RTL direction API) added `direction` to the i18n context, a `dir` prop to the provider, and useDirection / getLocaleDirection exports — the same three spots this branch touches for `translator`. Every conflict was add-add, so both features are kept side by side. - provider memo now carries {locale, direction, messages, overrides, translator} with all five deps - SYNC list keeps this branch's t.client.ts -> translator.ts fix (t.client.ts no longer exists in the tree); upstream's useDirection and getLocaleDirection entries added alongside it Verified: 83 i18n tests and 59 useTableSortable tests pass, typecheck and lint exit 0, and the net diff against upstream/main is unchanged at 1123+/15- across the same 10 files.
There was a problem hiding this comment.
Thanks for wiring the Translator adapter through the provider — the lookup staying in Astryx, the resolved-message (never key) handoff, and the test coverage are clear.
Approval is waiting on a maintainer decision: this adds a new public translator prop (and context field), and a maintainer needs to provide or identify the governing spec for that addition, settling (1) that Astryx keeps lookup/locale fallback and the adapter only formats the resolved ICU message, and (2) that every resolved string, including value-less ones, is delegated. Please do not treat this as a request to remove or split the change for that reason alone.
One independent fix is also needed before approval: when translator.format returns a non-string, the fallback in resolve returns the raw ICU template (for example Go to page {page, number}), which reaches Pagination's visible label and aria-label. Please format the resolved message with its values through the bundled formatter on that path (raw message only when there are no values) and update the test/docs that currently expect the template.
[Automated review]
Resolved conflicts (upstream meta#4696 replaced resolve() with a getResolve(locale, messages, overrides) closure stored on the context as `translate`): - packages/core/src/i18n/resolve.ts: took upstream's getResolve/getLookup shape and moved the translator delegation into the returned closure; getResolve gains an optional fourth `translator` argument. - packages/core/src/i18n/InternationalizationProvider.tsx: build `translate: getResolve(locale, messages ?? {}, overrides, translator)` and keep `translator` in the context value and the useMemo deps. - packages/core/src/i18n/InternationalizationContext.ts: keep both the PR's `translator?` field and upstream's `translate` field. - packages/core/src/i18n/useTranslator.ts: took upstream's `return ctx.translate` (the PR's useCallback change no longer applies). Merge fallout: translator.test.tsx called the removed `resolve` export, so it now goes through a local getResolve shim like resolve.test.ts's, and the dep-array comments point at the provider memo instead of useCallback.
F2 (astracat): when `translator.format` returned a non-string, resolve
returned the raw ICU template, so with an i18next-style translator that
returns null every Pagination page button got the accessible name
"Go to page {page, number}". The non-string path now warns once and falls
through to the bundled formatter, so the output matches the no-translator
path (raw message only when there are no values) and reuses the formatter
cache.
- resolve.ts: `if (typeof output === 'string') return output;` then
warnOnce and fall through instead of `return result`.
- translator.test.tsx: the four bad-return cases (undefined, null,
ReactNode[], number) now expect "Go to page 5"; new Pagination test
with a null-returning translator asserts a "Go to page 1" button and no
aria-label containing "{". Red before the fix: expected
'Go to page {page, number}' to be 'Go to page 5', and no button named
"Go to page 1". Green after: 44/44.
- translator.ts, InternationalizationProvider.tsx,
InternationalizationProvider.doc.mjs, the CLI internationalization doc
and the changeset now say the fallback is the bundled formatter.
|
Thanks for the review. I've pushed an update at 5f2d7b6.
Still open, for a maintainer: this adds a public On my side, the PR description still needs updating to match. |
There was a problem hiding this comment.
Thanks for wiring the Translator adapter through the provider, and for the follow-up push — I verified the non-string fallback fix at this revision: a bad adapter return now falls through to the bundled formatter, and the tests expect formatted labels (for example "Go to page 5") with no raw placeholders in Pagination's accessible names. That blocker from the previous review is resolved.
Approval is still waiting on a maintainer decision. This adds a new public translator prop on InternationalizationProvider, plus a translator field on the exported context value, and a maintainer needs to provide or identify the governing spec for that addition, settling:
- that Astryx keeps lookup and locale fallback, and the adapter only formats the resolved ICU message;
- that every resolved string, including value-less ones, is delegated to the adapter; and
- whether the context
translatorfield is kept or dropped, since nothing internal reads it now and you noted you are happy to drop it for a smaller public surface.
Please do not treat this as a request to remove or split the change for that reason alone. Once a maintainer settles that spec question, no other fix from this review is outstanding — though the PR description update you already flagged would help the next reader.
[Automated review]



What this does
Teams that already run an i18n library (react-intl, i18next, LinguiJS) can now have Astryx's own built-in strings go through their translation setup instead of Astryx's bundled formatter.
Why
The
Translatorinterface was already exported publicly with nothing behind it — the type shipped,InternationalizationProviderhad no way to accept one, andresolve()hard-coded the runtime. The CLI docs even advertised it as unbuilt, pointing readers at this issue.What changed
InternationalizationProvidertakes an optionaltranslator. Astryx still does its own lookup — your overrides, then yourmessages, then the parent language (pt-BR→pt), then the shipped English catalogue — and hands your translator the finished ICU message, never a raw@astryx.*key. That means you don't have to load Astryx's catalogue into your own store.Every Astryx string goes through your translator, including the ones with nothing to interpolate — which is most of them (162 of 197 shipped keys; 171 of 228 callsites).
valuesisundefinedfor those calls. This is deliberate: if only the interpolating quarter reached your runtime, a catalogue keyed on the English text would translateGo to page {page}and silently missClose, with no signal that half your UI never arrived. Two things follow — your runtime's missing-translation path runs on every string, so keep it cheap; and this is what makes the issue's one-provider example (translator, nomessages) actually do what it claims.formatmust return a string. Anything else — react-intl'sReactNode[]for rich text, i18next'snullfor a miss — warns once in development and falls back to Astryx's resolved message, because that output also feedsaria-labelandtitle, where a non-string becomes[object Object]or drops the attribute. Exceptions are not caught: a broken adapter should be loud, not quietly papered over.Leaving the prop off is a no-op: the bundled path and its formatter cache behave exactly as before, at the cost of one
!= nullcomparison.Needs a maintainer call
This adds a new public prop on a core provider, plus a new optional field on the publicly-exported context value. The shape isn't invented — the shipped
Translatorsignatureformat(message, values?, locale?)forces the lookup/format split, and the issue names the prop verbatim — but whether the API should exist is a maintainer's decision, not a contributor's.Two specifics worth confirming:
formattakes a message, not a key — and the code follows that answer: Astryx owns the lookup chain, the adapter owns formatting.What this does not do
No catalogue-merge helper (the issue lists it as an optional nice-to-have).
No server/RSC path.
translatorholds a function, so it can only be passed from a client component — a server-rendered subtree stays on the bundled runtime, and a future server resolver will need to threadtranslatortoo. Relatedly, a nestedInternationalizationProviderreplaces the translator rather than inheriting it, matching howmessagesandoverridesalready behave. Both are documented and pinned by tests.The react-intl example added to the docs is written from its public API but not executed, since react-intl isn't a dependency of this repo. Anyone who uses it should sanity-check that snippet — particularly the
valuescast, which exists because react-intl types values more narrowly than Astryx'sRecord<string, unknown>and its rich-text overload returnsReactNode[].How to check it
pnpm vitest run packages/core/src/i18n/— 73 tests (30 in this directory before the PR, 47 at the first revision). They assert that the translator receives the resolved message rather than the key (proving the fallback chain stayed inside Astryx), that value-less strings reach it, that a non-string return degrades safely instead of reaching anaria-label, and that a livetranslatororlocaleswap propagates. Edge coverage includes odd locale tags, empty-string catalogue entries, parent-locale overrides, and nested providers.Four assertions were mutation-tested. One turned out to be fake: the dep-array test passed even with
ctx.translatordeleted fromuseTranslator'suseCallback, because the provider'smessages ?? {}allocates a fresh object on every memo recompute, soctx.messagesinvalidated the callback on its own. Pinningmessagesto a stable reference kills the mutant.Full
packages/coresuite: 225 files / 5254 tests, exit 0.Refs #4029