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
Thanks — this is a well-scoped request and the plugin/downstream registration angle is the right frame; any i18n design that requires forking core strings is dead on arrival for us.
I've just closed discussion #272 pointing here, so this issue is now the canonical i18n thread. What would move it forward is exactly what you offered: a short proposal covering (1) an inventory of user-facing string surfaces (editor UI, inspector labels, node/tool labels, plugin-provided strings — they have different owners and lifecycles), (2) how locale packs load and fall back, and (3) how a plugin registers translations without touching core packages. I won't commit to a framework before that contract exists, but I'll engage seriously with the proposal — start it as a comment here or a discussion, whichever you prefer.
Triage update: I closed #740 because it attempted the full UI migration before this issue’s extensibility contract was settled and mixed that work with unrelated fork repairs and repository artifacts. This remains the canonical open request. The next useful contribution is the short design proposed above: enumerate string owners/surfaces, define locale loading and fallback, and define how plugins register translations. Once that is agreed, implementation should land one package or surface at a time from current main.
Following up on the request above for (1) a string inventory, (2) locale loading/fallback and (3) plugin registration — here is a proposal backed by a scan of current main and a small working prototype.
1. Inventory: who owns which strings
Method: TypeScript AST scan (not regex) of packages/{editor,nodes,viewer,core,ui}/src and apps/editor, tests/fixtures/dist excluded; cli, mcp, ifc-converter out of scope (terminal/agent text). Every hit is classified by owner (package), audience, and form (JSX text, JSX prop, config object, node definition, label map, default name, constant/generated, toast). Spot checks: ~95% of sampled hits are real UI text; the scan catches ~82% of capitalised multi-word literals, so treat totals as a floor.
User-interface strings: ~4.3k (2.7k unique) in 463 files
Owner
Strings
Unique
Files
Lifecycle / who changes it
nodes — one folder per node kind (definition.ts labels/descriptions, parametrics.ts field labels, panel.tsx inspectors, tool hints)
2,243
1,411
230
Changes with each node kind. Plugins ship node kinds of their own, so this is also the shape plugin strings have.
Small, but some of it ends up inside scene data (see 2.4).
apps/editor — the example host app
202
169
14
Host-owned. Downstream hosts replace or copy it; it consumes the contract, it does not need to be part of it.
viewer
38
34
7
Small; good first surface.
ui
2
2
2
—
Strings that should NOT go through UI i18n
Audience
Strings
Why it is separate
Content (catalog item names, material library)
411
Data, not chrome: a host with its own catalog brings its own names (and languages).
Exported data (glb-export.ts node names, etc.)
38
Must stay stable — downstream tools key on them. Translating would break consumers.
Agent / MCP tool descriptions
50
Written for models; English is the API.
Validation messages (core/validation)
25
User-visible but produced in core; need keys + params, not literals.
Legal pages in the example app
100
Host content.
Developer errors (new Error)
284
Not user-facing.
2. Proposed contract
2.1 Keys and English as the built-in fallback
Every user-facing string gets a key, and the English literal stays in the code as the default: t('editor.buildPanel.rooms.title', 'Rooms'). With no locale registered, behaviour and screenshots are identical to today — zero risk for English, and migration can go one surface at a time.
Node definitions keep presentation.label / description in English; display goes through t(\node.${kind}.label`, def.presentation.label)`. No node definition has to change shape.
2.2 Registration (no framework required in core)
core exposes a tiny registry: registerMessages(locale, namespace, messages) and setLocale(locale); editor exposes useTranslate() reading it (re-render on locale change).
Host app: calls registerMessages('pt-BR', 'editor', ptBrEditor) before mounting <Editor> — exactly how it already calls registerNode.
Plugins: a plugin manifest may carry translations: { [locale]: messages } for its own plugin.<id>.* / node.<kind>.* namespaces, registered when the plugin loads. Nothing in core packages is touched.
Locale packs for core namespaces can live outside the repo (npm @scope/pascal-locale-xx) or in-repo later; the contract does not care.
2.3 Fallback
pt-BR → pt → English literal in code. Missing keys never throw and never show the key; a dev-only warning lists them.
2.4 Names saved into scene data
Today both patterns exist:
display-time default: getDefaultLevelName() — "a name the user gave wins", otherwise computed when shown. Safe to translate.
write-time default: e.g. liquid-line/tool.tsx creates nodes with name: 'Liquid Line', which is persisted. Translating that literal would store the creator's language in the scene and in exports.
Proposal: default names become display-time (store no name, or a key), following the level-name pattern. Out of scope for the first PRs, but the contract should state it so nobody "translates" write-time names.
2.5 Server rendering and hydration
If the locale is only known in the browser, server HTML (English) and the first client render (translated) differ and React throws Hydration failed — the prototype hit this on the first try. The hook therefore returns the English fallback for the server snapshot (useSyncExternalStore(subscribe, getVersion, () => SERVER)) and re-renders with the client locale right after hydration. Hosts that know the locale on the server (cookie/header) can call setLocale there too and skip the flash.
2.6 Out of scope on purpose
Exported names (GLB/IFC), agent/MCP text, catalog/material content, dev errors.
3. Prototype
Two commits on top of main @ 89e1442f: (1) core/src/i18n registry (88 lines, no dependency) + useTranslate() hook (21 lines) + tests, and the Build panel's Rooms section and "Advanced" header moved to 16 keys (build-panel.tsx +20/−6); (2) a host file registering pt-BR (+32 lines, outside core). With no locale (or ?lang=en) every string is the original English; with pt-BR the section is translated, including the caption that follows the armed shape. No translated text lives in core packages.
4. Suggested rollout
Registry + useTranslate() + fallback in core/editor, no strings migrated (one small PR).
A CI guard in the same PR or right after: flag new user-facing literals that bypass t() (JSX text, label/title/aria-label props, node definition labels). Without it the migration never finishes: between 1.0.2 (2026-09-18) and main (2026-10-02) the UI gained 612 new unique strings in 14 days (~300/week, mostly editor and nodes). Our AST scan already does this detection and could be the starting point; it can start as a warning and only cover migrated surfaces.
One surface per PR from current main: viewer (38 strings) → editor build panel → inspectors per node kind.
Plugin manifest translations once a first plugin wants it.
The scan script and the full per-string CSV are available if useful, and the prototype can be opened as a draft PR from current main.
What we (Kliv, Brazil — pt-BR) can commit to once the contract is agreed: the registry PR (steps 1–2), the migration of two or three surfaces as reference (starting with viewer and the build panel), and the pt-BR locale pack, kept up to date as new keys land. We don't expect to migrate every surface ourselves.
Activity
Aymericr commentedon Aug 28, 2026
Thanks — this is a well-scoped request and the plugin/downstream registration angle is the right frame; any i18n design that requires forking core strings is dead on arrival for us.
I've just closed discussion #272 pointing here, so this issue is now the canonical i18n thread. What would move it forward is exactly what you offered: a short proposal covering (1) an inventory of user-facing string surfaces (editor UI, inspector labels, node/tool labels, plugin-provided strings — they have different owners and lifecycles), (2) how locale packs load and fall back, and (3) how a plugin registers translations without touching core packages. I won't commit to a framework before that contract exists, but I'll engage seriously with the proposal — start it as a comment here or a discussion, whichever you prefer.
Aymericr commentedon Sep 12, 2026
Triage update: I closed #740 because it attempted the full UI migration before this issue’s extensibility contract was settled and mixed that work with unrelated fork repairs and repository artifacts. This remains the canonical open request. The next useful contribution is the short design proposed above: enumerate string owners/surfaces, define locale loading and fallback, and define how plugins register translations. Once that is agreed, implementation should land one package or surface at a time from current main.
luis-caf commentedon Oct 3, 2026
Following up on the request above for (1) a string inventory, (2) locale loading/fallback and (3) plugin registration — here is a proposal backed by a scan of current
mainand a small working prototype.1. Inventory: who owns which strings
Method: TypeScript AST scan (not regex) of
packages/{editor,nodes,viewer,core,ui}/srcandapps/editor, tests/fixtures/distexcluded;cli,mcp,ifc-converterout of scope (terminal/agent text). Every hit is classified by owner (package), audience, and form (JSX text, JSX prop, config object, node definition, label map, default name, constant/generated, toast). Spot checks: ~95% of sampled hits are real UI text; the scan catches ~82% of capitalised multi-word literals, so treat totals as a floor.User-interface strings: ~4.3k (2.7k unique) in 463 files
nodes— one folder per node kind (definition.tslabels/descriptions,parametrics.tsfield labels,panel.tsxinspectors, tool hints)editor— editor chrome (sidebar, panels, build panel, helpers, command palette, dialogs)core— generated display names (getDefaultLevelName), procedural item recipes, schema labelsapps/editor— the example host appvieweruiStrings that should NOT go through UI i18n
glb-export.tsnode names, etc.)core/validation)new Error)2. Proposed contract
2.1 Keys and English as the built-in fallback
t('editor.buildPanel.rooms.title', 'Rooms'). With no locale registered, behaviour and screenshots are identical to today — zero risk for English, and migration can go one surface at a time.editor.*,viewer.*,core.*,node.<kind>.*,plugin.<pluginId>.*.presentation.label/descriptionin English; display goes throught(\node.${kind}.label`, def.presentation.label)`. No node definition has to change shape.2.2 Registration (no framework required in core)
coreexposes a tiny registry:registerMessages(locale, namespace, messages)andsetLocale(locale);editorexposesuseTranslate()reading it (re-render on locale change).registerMessages('pt-BR', 'editor', ptBrEditor)before mounting<Editor>— exactly how it already callsregisterNode.translations: { [locale]: messages }for its ownplugin.<id>.*/node.<kind>.*namespaces, registered when the plugin loads. Nothing in core packages is touched.@scope/pascal-locale-xx) or in-repo later; the contract does not care.2.3 Fallback
pt-BR→pt→ English literal in code. Missing keys never throw and never show the key; a dev-only warning lists them.2.4 Names saved into scene data
Today both patterns exist:
getDefaultLevelName()— "a name the user gave wins", otherwise computed when shown. Safe to translate.liquid-line/tool.tsxcreates nodes withname: 'Liquid Line', which is persisted. Translating that literal would store the creator's language in the scene and in exports.Proposal: default names become display-time (store no name, or a key), following the level-name pattern. Out of scope for the first PRs, but the contract should state it so nobody "translates" write-time names.
2.5 Server rendering and hydration
If the locale is only known in the browser, server HTML (English) and the first client render (translated) differ and React throws Hydration failed — the prototype hit this on the first try. The hook therefore returns the English fallback for the server snapshot (
useSyncExternalStore(subscribe, getVersion, () => SERVER)) and re-renders with the client locale right after hydration. Hosts that know the locale on the server (cookie/header) can callsetLocalethere too and skip the flash.2.6 Out of scope on purpose
Exported names (GLB/IFC), agent/MCP text, catalog/material content, dev errors.
3. Prototype
Two commits on top of
main@89e1442f: (1)core/src/i18nregistry (88 lines, no dependency) +useTranslate()hook (21 lines) + tests, and the Build panel's Rooms section and "Advanced" header moved to 16 keys (build-panel.tsx+20/−6); (2) a host file registering pt-BR (+32 lines, outside core). With no locale (or?lang=en) every string is the original English; with pt-BR the section is translated, including the caption that follows the armed shape. No translated text lives in core packages.4. Suggested rollout
useTranslate()+ fallback incore/editor, no strings migrated (one small PR).t()(JSX text,label/title/aria-labelprops, node definition labels). Without it the migration never finishes: between 1.0.2 (2026-09-18) andmain(2026-10-02) the UI gained 612 new unique strings in 14 days (~300/week, mostlyeditorandnodes). Our AST scan already does this detection and could be the starting point; it can start as a warning and only cover migrated surfaces.main:viewer(38 strings) →editorbuild panel → inspectors per node kind.translationsonce a first plugin wants it.The scan script and the full per-string CSV are available if useful, and the prototype can be opened as a draft PR from current
main.What we (Kliv, Brazil — pt-BR) can commit to once the contract is agreed: the registry PR (steps 1–2), the migration of two or three surfaces as reference (starting with
viewerand the build panel), and the pt-BR locale pack, kept up to date as new keys land. We don't expect to migrate every surface ourselves.