You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
react-i18next's not-ready t returns defaultValue VERBATIM — 65 inline defaults at bare useObjectTranslation() sites carry {{holes}} that reach the user as literal braces #6219
Measured while implementing objectui#4905 (PR #6218) — not in that PR's scope, and it is the reason that card's route C reached only 5 of its 66 sites instead of all of them.
The mechanism
react-i18next/dist/es/useTranslation.js ships a fallback t used whenever no i18next instance is initialised:
It returns options.defaultValueverbatim. It does not interpolate. So an inline default written as 'Deleted {{count}} rows' renders those braces to the user, character for character.
This is a different code path from objectui#3865 / objectui#3512, which are both about createSafeTranslation's fallbackT — that one does interpolate (value.split(\{{${k}}}`).join(String(v))), which is exactly why those cards are about the *spelling* of the hole rather than whether it is filled at all. A call site bound to a **bare useObjectTranslation()** never reaches fallbackT; useObjectTranslationreturns react-i18next'st directly (packages/i18n/src/provider.tsx), so with no instance it gets notReadyT`.
The census on main
A sweep of defaultValue: '…{{…}}…' string literals under packages/ and apps/, classified by the translator the file binds:
translator
files
inline defaults with a {{hole}}
bare useObjectTranslation() — notReadyT, does not interpolate
24
65
a createSafeTranslation hook — fallbackT, interpolates
3
4
t received as a parameter/prop — provenance depends on the caller
4
6
The 24 affected files, worst first: AppManagementPage.tsx (23), InboxPopover.tsx (6), BulkActionBar.tsx (5), ImpersonationBanner.tsx (4), then AiChatPage.tsx, LiveCanvas.tsx, PendingDraftsBar.tsx, MarketplacePackagePage.tsx, AppSidebar.tsx, UnifiedSidebar.tsx, FlowRunner.tsx (2 each), and 13 files with one each.
Note these are only the defaults written as string literals. The far more common shape at these sites is a pre-interpolated template literal (`Deleted ${n} rows`), which is correct precisely because notReadyT will not interpolate — that is what PR #6218 measured when three objectBulkActionDispatch.test.tsx cases went red on converting ten of them to {{hole}} form.
Reachability, stated honestly
With I18nProvider mounted — the console's own path — i18next is ready, the pack value wins, and the inline default never renders at all. The not-ready path is reachable in:
tests that render a component with no provider (this is how it surfaced),
any host embedding an ObjectUI component without I18nProvider — which is the entire scenario createSafeTranslation was built for (objectui#3865), so it is a supported configuration rather than a hypothetical one.
Severity is a triage call, not this finding's to make: 0 users are known to have hit it, and the same sweep is what makes it 0-by-measurement rather than 0-by-guarantee.
What a fix would have to decide
Not attached as a recommendation — the interesting question is which of these the project wants, and that is a partition decision:
Bind these call sites to a createSafeTranslation hook so fallbackT interpolates. Consistent with objectui#3865's direction, but it is 24 files and each needs a defaults map.
Pre-interpolate at these call sites (the template-literal form the repo already uses at the majority of them), and gate the {{hole}} form out at bare-useObjectTranslation sites. Cheap, and mechanically checkable — PR Hold every inline defaultValue to the placeholder spelling the provider-less fallback resolves #6218's new unresolvable-default-spelling class in scripts/check-i18n-call-site-keys.mjs already walks exactly these call sites and reads exactly this text; it would need the binding's factory as one more input.
Teach useObjectTranslation to interpolate its own not-ready result, i.e. stop handing back react-i18next's notReadyT bare. One place, but it forks from react-i18next's behaviour, which objectui#3512 explicitly declined to do in the other direction.
Related: objectui#4905 (where this was measured, PR #6218), objectui#3865 (the fallbackT chain step this is the react-i18next half of), objectui#3512 (the spelling rule that assumes an interpolating fallback).
Measured while implementing objectui#4905 (PR #6218) — not in that PR's scope, and it is the reason that card's route C reached only 5 of its 66 sites instead of all of them.
The mechanism
react-i18next/dist/es/useTranslation.jsships a fallbacktused whenever no i18next instance is initialised:It returns
options.defaultValueverbatim. It does not interpolate. So an inline default written as'Deleted {{count}} rows'renders those braces to the user, character for character.This is a different code path from objectui#3865 / objectui#3512, which are both about
createSafeTranslation'sfallbackT— that one does interpolate (value.split(\{{${k}}}`).join(String(v))), which is exactly why those cards are about the *spelling* of the hole rather than whether it is filled at all. A call site bound to a **bareuseObjectTranslation()** never reachesfallbackT;useObjectTranslationreturns react-i18next'stdirectly (packages/i18n/src/provider.tsx), so with no instance it getsnotReadyT`.The census on
mainA sweep of
defaultValue: '…{{…}}…'string literals underpackages/andapps/, classified by the translator the file binds:{{hole}}useObjectTranslation()—notReadyT, does not interpolatecreateSafeTranslationhook —fallbackT, interpolatestreceived as a parameter/prop — provenance depends on the callerThe 24 affected files, worst first:
AppManagementPage.tsx(23),InboxPopover.tsx(6),BulkActionBar.tsx(5),ImpersonationBanner.tsx(4), thenAiChatPage.tsx,LiveCanvas.tsx,PendingDraftsBar.tsx,MarketplacePackagePage.tsx,AppSidebar.tsx,UnifiedSidebar.tsx,FlowRunner.tsx(2 each), and 13 files with one each.Note these are only the defaults written as string literals. The far more common shape at these sites is a pre-interpolated template literal (
`Deleted ${n} rows`), which is correct precisely becausenotReadyTwill not interpolate — that is what PR #6218 measured when threeobjectBulkActionDispatch.test.tsxcases went red on converting ten of them to{{hole}}form.Reachability, stated honestly
With
I18nProvidermounted — the console's own path — i18next is ready, the pack value wins, and the inline default never renders at all. The not-ready path is reachable in:I18nProvider— which is the entire scenariocreateSafeTranslationwas built for (objectui#3865), so it is a supported configuration rather than a hypothetical one.Severity is a triage call, not this finding's to make: 0 users are known to have hit it, and the same sweep is what makes it 0-by-measurement rather than 0-by-guarantee.
What a fix would have to decide
Not attached as a recommendation — the interesting question is which of these the project wants, and that is a partition decision:
createSafeTranslationhook sofallbackTinterpolates. Consistent with objectui#3865's direction, but it is 24 files and each needs a defaults map.{{hole}}form out at bare-useObjectTranslationsites. Cheap, and mechanically checkable — PR Hold every inline defaultValue to the placeholder spelling the provider-less fallback resolves #6218's newunresolvable-default-spellingclass inscripts/check-i18n-call-site-keys.mjsalready walks exactly these call sites and reads exactly this text; it would need the binding's factory as one more input.useObjectTranslationto interpolate its own not-ready result, i.e. stop handing back react-i18next'snotReadyTbare. One place, but it forks from react-i18next's behaviour, which objectui#3512 explicitly declined to do in the other direction.Related: objectui#4905 (where this was measured, PR #6218), objectui#3865 (the
fallbackTchain step this is the react-i18next half of), objectui#3512 (the spelling rule that assumes an interpolating fallback).