Repository navigation
Description
Several strings in the playground are hard-coded in English, so the French UI shows English in the instrument selector, on the loading screen and in a few labels. The playground offers English and French through its LanguageToggle and translates most text with t({ en, fr }), but these strings skip it. The instrument selector, the control used most, is the most visible case: in French, its search placeholder, its empty state and all three group headings (Examples, Saved, Templates, which are the raw category values) stay in English. react/jsx-no-literals, which catches this in apps/web and packages/react-core, is not enabled for apps/playground in eslint.config.js.
Where
apps/playground/src/components/Header/InstrumentSelector/InstrumentSelector.tsx:33, :44, :46, :48:
aria-label="Load an instrument..."
// ...
<Command.Input placeholder="Search..." />
<Command.List>
<Command.Empty>No Instruments Found</Command.Empty>
{Object.entries(categorizedInstruments).map(([heading, instruments]) => (
<Command.Group heading={heading} key={heading}>Also:
apps/playground/src/App.tsx:13: the loading screen'stitle="Loading Editor and Toolchain"andsubtitle="Please Be Patient, This May Take a While".apps/playground/src/components/Viewer/PreviewFrame.tsx:69: the preview iframe'stitle="Open Data Capture - Instrument Preview", which screen readers announce.apps/playground/src/components/Editor/EditorPane.tsx:118: the image preview'salt={`Rendering of ${defaultFile.name}`}.apps/playground/src/components/Header/UploadButton/UploadButton.tsx:33,36: the fallback label'Unlabeled', which becomes the uploaded instrument's visible name.
Reproduce
- Open the playground and switch the language toggle to Français.
- Open the instrument selector, then type
zzzinto its search box. - Reload the page.
Actual: the placeholder reads Search..., the headings read Examples, Saved and Templates, the empty state reads No Instruments Found, and the loading screen is in English.
Expected: all of these appear in French.
Tests
Add apps/playground/src/**/*.tsx (excluding stories) to the react/jsx-no-literals block in eslint.config.js, so pnpm lint catches any regression. That lint rule is the guard; the strings themselves have no pure logic to unit-test.
E2E in testing/src/specs/playground.spec.ts: switch to French, open the selector, and assert that the search placeholder and a group heading are the French strings.
Suggested fix
Wrap each string in t({ en, fr }), map the category values to translated headings, and enable react/jsx-no-literals for the playground. The loading screen renders outside CoreProvider, so it needs i18n.t or a small wrapper rather than the hook.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e), and no open PR changes these strings. The lint guard, the loading-screen fix and the test plan need adjusting, and a few strings are missing from the list.The lint rule catches one of these strings
The
react/jsx-no-literalsblock setsignoreProps: true(eslint.config.js:237), so it reports JSX text only. Of the strings listed here, that is justNo Instruments Found.aria-label,placeholder,title,subtitleandaltare props,heading={heading}is a variable, and'Unlabeled'is not JSX. Enabling the rule is still worth it, with three changes:apps/playground/src/instruments/**as well as stories. The proposed glob matches the seven example-instrument.tsxfiles, such asexamples/interactive/Interactive-With-React/App.tsx, which is full of English JSX text. Those files are instrument source, not playground UI.<span>GitHub</span>atActionsDropdown.tsx:43fails. AddGitHubtoallowedStrings..after{formatByteSize(shareURL.size)}atShareButton.tsx:61fails too. Add.toallowedStringsnext to:and-, or move the period into thet()call.apps/webandpackages/react-corehave the same gap for props. A guard on literalalt,aria-label,placeholderandtitlevalues, for example ano-restricted-syntaxselector onJSXAttribute, should cover all three, so it belongs in its own change.The loading screen stays in English anyway
useTranslationdoes not needCoreProvider. libui's hook reads the module-leveli18nsingleton, andCoreProvideronly renders the notification hub and the destructive-action dialog, soAppcan call the hook directly.The language is not kept across loads, though.
main.tsx:12-14callsi18n.initwith nodefaultLanguage, and libui 6.16.1 falls back to'en'(src/i18n/translator.ts:102). Nothing stores the toggle's choice: libui keeps the theme inlocalStorage, but not the language. The Suspense fallback only shows during the initial load, so after step 3 the whole UI is in English, not just the loading screen. Wrap the two strings anyway, but take the loading screen out of Expected. Remembering the language is a separate decision. The language has to be known whenmain.tsxcallsi18n.init, so it cannot come from the store, which hydrates from IndexedDB asynchronously. AlocalStoragekey would work.Missed strings
None of these is JSX text, so the rule will not flag them either.
LoginDialog.tsx:159andUserSettingsDialog.tsx:38:placeholder: 'e.g., https://demo.opendatacapture.org/api'.LoginDialog.tsx:62,:83andUploadBundleDialog.tsx:113:'Unknown Error', which becomes the message of the error notification.StorageUsageDialog.tsx:53and:57: the'N/A'fallback.Plan
InstrumentSelector/group-instruments.tsthat walks$InstrumentCategory.optionsand skips empty categories. In the component, look each category up in a{ [K in InstrumentCategory]: string }oft()values. This replaces the cast atInstrumentSelector.tsx:23-25, which claims every category is present. A fresh profile has noSavedgroup, so code that walks the categories under that cast getsundefined. The group order also becomes fixed. Today it depends on which category holds the alphabetically first label.Load an instrument...andSearch...as theenvalues.index.page.ts:31and fix(playground): search instruments by label #1817 both locate elements by them. fix(playground): search instruments by label #1817 also editsInstrumentSelector.tsxandplayground.spec.ts, so rebase onto it.LanguageToggledirectly (Header.tsx:54,IndexPage.tsx:90), and that toggle has no name and no test id. Switch both to react-core'sLanguageTogglewithactiveLanguages={['en', 'fr']}. It offers the sameEnglishandFrançaisitems and wraps the trigger indata-testid="language-toggle", so the spec can switch language the waygateway-landing.spec.ts:27-35does. Any trigger name Gateway header's theme and language buttons have no accessible name, so screen readers announce both as just "button" #1722 adds to that wrapper then reaches the playground too. After the switch, the combobox's accessible name is French, so the spec has to open the selector by the French label.InstrumentSelector/__tests__/group-instruments.test.ts, in node, following fix: resolve 26 low-difficulty issues, one commit each #1825'sSaveButton/is-save-disabled.ts. Assert the fixed category order, and that there is noSavedentry when nothing is saved. A test that renders the selector and checks the French strings needshappy-domand@testing-library/react, which build: upgrade dependencies held back by breaking changes #1660 adds toapps/playground. Adding them in this change instead needs sign-off.Triage note written with Claude Code. Code references point at
main@d86740e.