Add in-app language selector with live (no-reload) switching#927
Merged
Conversation
SuperSplat previously chose its UI language solely from the browser locale, with the undiscoverable ?lng= query param as the only override. Users whose locale doesn't match their preferred language had no obvious way to change it. This adds a Language selector to the View Options panel and reworks localization so switching applies live, with no page reload — the loaded scene, engine state, and unsaved work are all preserved. Localization is refactored from a flat set of free functions into a cohesive `Localization` class exposed as a single `i18n` singleton (src/ui/localization.ts), encapsulating the i18next instance and the language-change subscriber set: i18n.t(key, opts?) translate i18n.bindText(el, key|builder) reactive .text (Label/Button) i18n.bindOptions(select, builder) reactive SelectInput options i18n.onChange(fn, owner?) run now + on every language change i18n.setLanguage(code|null) switch live; null = Automatic i18n.locale / i18n.languages active code / supported list i18n.init() startup Reactivity: bound UI re-translates on i18next's `languageChanged` event; binders auto-unsubscribe via the element `destroy` event. Tooltips and menu items take a resolver (string | () => string) so they reflect the current language without a listener. All ~220 call sites were migrated across labels, buttons, transformed text, SelectInput option lists, menu items, tooltips, and the data panel's interpolated/raw-DOM strings. Persistence: enables i18next's localStorage cache. Detection order is querystring -> localStorage -> navigator -> htmlTag, so ?lng= still wins for shareable links, an explicit choice beats the browser locale, and behaviour is unchanged when nothing is stored. "Automatic" clears the stored choice and reverts to the detected locale. Adds panel.view-options.language / .language.auto strings to all nine locale files (native language names live in code, not the locale files). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The localization migration left `new Label({})` as an empty multi-line
object after its only `text` property was removed; collapse to `new Label()`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
slimbuck
approved these changes
Jun 29, 2026
Previously the dropdown defaulted to the resolved active language (e.g. "English"), so an untouched setting never read "Automatic" and the option only appeared once selected — surprising for users who hadn't chosen. Now the selector shows "Automatic" unless the user has explicitly pinned a language. This required distinguishing an explicit choice from auto-detection: i18next's localStorage cache wrote the detected language on every load, so its presence couldn't signal intent. Disable that auto-cache (caches: []) and have setLanguage manage the key itself — write it on an explicit pick, remove it on "Automatic". A new i18n.storedLanguage getter exposes the pinned code (or null) for the selector's default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With a Language preference now living in the panel, "View Options" no longer describes its contents — Language is an app-level setting, not a view option. "Settings" is an honest umbrella for both the view/render controls and the language preference, and is where users instinctively look for things like language. Updates only the two user-facing strings (panel header and toolbar tooltip) across all nine locales; the panel.view-options.* key names are left as-is (namespacing) and the toolbar already uses a gear-style icon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dimitribarbot
added a commit
to dimitribarbot/supersplat
that referenced
this pull request
Jul 2, 2026
Resolve conflicts by adopting upstream's reactive i18n localization (PR playcanvas#927, which removed the free localize()/formatTooltipWithShortcut() helpers) across all fork subsystems: converted ~80 localize() call sites to i18n.t(), using thunk form () => i18n.t() at reactive menu/tooltip sites. Locale files merged (all fork keys kept; upstream's renamed 'panel.view-options' -> Settings + language-selector keys adopted). Kept fork's richer LCC import guard alongside upstream's i18n change. Requires @playcanvas/splat-transform 2.7.1 (upstream dep bump, PR playcanvas#929) which now exports WorkerQueue, used by upstream's inline worker-pool fix (PR playcanvas#930). Verified: npm run lint (clean), npm run build (ok), npm run test (189/189 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #923
What
Adds a Language selector to the editor's Settings panel (renamed from "View Options") and reworks localization so changing language applies live, with no page reload — the loaded scene, engine state, and unsaved work are all preserved.
Why
SuperSplat chose its UI language solely from the browser locale, with the undiscoverable
?lng=query parameter as the only override. Users whose locale doesn't match their preferred language (e.g. an English speaker on a Japanese-locale machine) had no obvious way to change it. This makes the choice explicit, discoverable, and persistent.How
Cohesive interface.
src/ui/localization.tsis refactored from a flat set of free functions into aLocalizationclass exposed as a singlei18nsingleton, encapsulating the i18next instance and the language-change subscriber set:i18n.t(key, opts?)i18n.bindText(el, key | builder).text(Label/Button)i18n.bindOptions(select, builder)SelectInputoptionsi18n.onChange(fn, owner?)i18n.setLanguage(code | null)null= Automatici18n.storedLanguagenullon Automatici18n.locale/i18n.languagesi18n.init()Reactivity. Bound UI re-translates on i18next's
languageChangedevent; binders auto-unsubscribe via the elementdestroyevent (no leaks for transient dialogs). Tooltips and menu items take a resolver (string | () => string) so they reflect the current language with no listener. All ~220 call sites were migrated across labels, buttons, transformed text (.toUpperCase()/ellipsis),SelectInputoption lists, menu items, tooltips, and the data panel's interpolated + raw-DOM strings. Runtime popup/progress messages are left as-is since they're rebuilt at call time.Persistence. Detection order is
querystring → localStorage → navigator → htmlTag, so?lng=still wins for shareable links, an explicit choice beats the browser locale, and an untouched setting follows the browser. i18next's automaticlocalStoragecaching is disabled (caches: []) so a stored value reliably means an explicit pick rather than "detected this time";setLanguagemanages the key itself — writing the chosen code on a pick, removing it on Automatic (which then reverts to browser detection).i18n.storedLanguageexposes the pinned code (ornull) so the selector can default to Automatic until the user chooses.Strings. Adds
panel.view-options.language/.language.autoto all nine locale files, and updates the panel header + toolbar tooltip strings to "Settings". Translation key names (panel.view-options.*) are left unchanged as namespacing. Language names are shown in their native form and live in code (not the locale files), so a user who can't read the current UI language can still recognise their own.Notes for reviewers
i18n.bindText/bindOptions/onChange, or they won't update on switch — documented in theLocalizationclass JSDoc. Plaini18n.t()is only correct for call-time strings.Verification
npm run lint,npx tsc --noEmit, andnpm run buildall pass clean. Suggested manual check: a fresh load shows Automatic; open Settings (right toolbar), switch language → entire UI updates instantly with the scene intact; reload → a pinned choice persists; selecting Automatic reverts to the browser locale;?lng=frstill overrides.🤖 Generated with Claude Code