Skip to content

Add extensible i18n/localization support #621

Open

Description

@johnsonkang

Problem or motivation

Hi, thanks for developing Pascal Editor.

Would you consider adding first-class, extensible i18n/localization support?

Ideally, it could cover:

Editor UI, menus, dialogs, tooltips, and commands
Inspector/property labels
Node and tool labels/descriptions
Plugin-provided UI strings
Runtime locale switching
Custom locale packs

It would be especially useful if plugins and downstream applications could register their own translations without modifying Pascal's core packages.

If you're open to this feature and have any related plans or development guidelines, I'd be happy to contribute a PR.

Proposed solution

No response

Alternatives considered

No response

Additional context

No response

Activity

  1. Aymericr commented on Aug 28, 2026

    @Aymericr
    Contributor

    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.

  2. Aymericr commented on Sep 12, 2026

    @Aymericr
    Contributor

    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.

  3. luis-caf commented on Oct 3, 2026

    @luis-caf

    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.
    editor — editor chrome (sidebar, panels, build panel, helpers, command palette, dialogs) 1,394 1,051 156 Changes with the editor UI; the build panel moved here from the app during September.
    core — generated display names (getDefaultLevelName), procedural item recipes, schema labels 325 257 55 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.
    • Namespaces follow ownership: editor.*, viewer.*, core.*, node.<kind>.*, plugin.<pluginId>.*.
    • 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

    1. Registry + useTranslate() + fallback in core/editor, no strings migrated (one small PR).
    2. 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.
    3. One surface per PR from current main: viewer (38 strings) → editor build panel → inspectors per node kind.
    4. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-humanNeeds human input during execution

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions