Skip to content

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

Description

@yinlianghui-tw

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:

constnotReadyT=(k,optsOrDefaultValue)=>{if(isString(optsOrDefaultValue))returnoptsOrDefaultValue;if(isObject(optsOrDefaultValue)&&isString(optsOrDefaultValue.defaultValue))returnoptsOrDefaultValue.defaultValue;returnArray.isArray(k) ? k[k.length-1] : k;};

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:

translatorfilesinline defaults with a {{hole}}
bare useObjectTranslation()notReadyT, does not interpolate2465
a createSafeTranslation hook — fallbackT, interpolates34
t received as a parameter/prop — provenance depends on the caller46

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).

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions