Restore i18n on remaining UI components (rebase onto upstream/main) - #740
Restore i18n on remaining UI components (rebase onto upstream/main)#740leapar wants to merge 110 commits into
Conversation
Re-applies the i18n integration that was lost when syncing with
upstream's i18n-clean rewrite of these files. Only adds the translation
hooks (useTranslations / useLocale / messages) and replaces the
hardcoded English strings — leaves upstream's structural changes (Build
tab, sidebar tab shape, control-mode set, icon sources) untouched.
- packages/editor/src/index.tsx: re-export I18nProvider, useLocale,
useTranslations, defaultLocale, Locale, messages from lib/i18n so
community shells can compose them again.
- apps/editor/app/page.tsx: use useTranslations for sidebar labels
(scene/build/items/settings) and the local-editor banner; new key
editor.openSavedScenes.
- apps/editor/components/scene-loader.tsx: same pattern for the
scene/build/settings tabs; new key sidebar.build.
- packages/editor/src/components/ui/action-menu/camera-actions.tsx:
viewer.orbitLeft / orbitRight / topView.
- packages/editor/src/components/ui/action-menu/control-modes.tsx:
controlModes.select / zone + common.delete via labelKey.
- apps/editor/components/save-button.tsx: cast locale to Locale so the
indexed access typechecks.
- apps/editor/messages/{en,zh}.json: add sidebar.build,
editor.openSavedScenes, viewer.{orbitLeft,orbitRight,topView}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continues the i18n restoration started in the previous commit. Same pattern: only adds the translation hooks and replaces hardcoded English strings, leaving upstream's structural changes intact. - action-menu/structure-tools.tsx, furnish-tools.tsx: switch the hardcoded 'label' fields to 'labelKey' (cursor-sphere.tsx already expected labelKey, so this resolves the existing inconsistency). - helpers/building-helper.tsx, item-helper.tsx, roof-helper.tsx: useTranslations for the contextual-helper hint labels. - helpers/registered-tool-helper.tsx: localize the new 'Guided constraints bypassed' label. - panels/panel-manager.tsx: localize the 'Selection' fallback label for multi-selection mobile panels. - sidebar/icon-rail.tsx: switch the panels[] entries to labelKey and resolve them inside the component. New translation keys: - structureTools: duct, ductFitting, register, hvacUnit, dwvPipe, trap, pipeFitting, lineset, liquidLine (HVAC/plumbing lines added by upstream since the local fork). - editor: placeBuilding, rotate, chooseRoom, selectCurvedWall, setDiameter, setCorner, rotateRoofDirection, place, forcePlace, guidedConstraintsBypassed. - panel: selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… toggles, viewer toolbar Continues the i18n restore from b8c8704 and af9d9ea. Replaces hardcoded strings across 8 components with useTranslations() calls. - command-palette (index + editor-commands): group headings, command labels, wall/level mode cycle, camera switch, rename footer hint - settings-panel: project id, visibility, export, floorplan, thumbnail, save/load, audio, keyboard, scene graph, danger zone - keyboard-shortcuts-dialog: 7 categories, all action/note keys - load-build-dialog: stat rows, errors, warnings, schema details - floating-level-selector: drag handle, height label, add/insert/delete menus, delete dialog - view-toggles: guide/scan/reference controls, opacity, file errors - viewer-toolbar (apps/editor): view modes, sidebar toggle, level/wall toggles, display menu (grid, measurements, floorplan, units, render, edges, theme), walkthrough/preview buttons Module-level configs converted from inline labels to labelKey/nameKey strings, resolved inside components via t(). Added 100+ translation keys to en.json and zh.json (parity verified).
…ranslations # Conflicts: # packages/editor/src/components/ui/helpers/roof-helper.tsx
…ascalorg#3, pascalorg#6 - pascalorg#1: FitSceneButton now calls useTranslations() (was throwing ReferenceError on render). - pascalorg#2: Command palette group keys use dotted form (commands.group.scene) to match the i18n catalog; previously used the flat form (commands.groupScene) which produced undefined labels. - pascalorg#3: save-button.tsx migrated from the bespoke messages[] lookup to useTranslations() with save.* keys, removing the unprefixed key leak into the global namespace and matching the rest of the codebase. - pascalorg#6: Added missing catalog keys with full en/zh parity: sidebar.build, sidebar.settings editor.openSavedScenes, editor.rotate, editor.place, editor.forcePlace, editor.guidedConstraintsBypassed Skipped pascalorg#4 (resolveBaseUrl) and pascalorg#5 (editor-layout-v2 toolbar) — those predate this branch and are out of scope for the i18n restore PR.
|
Addressed Bugbot findings #1, #2, #3, and #6 in commit 9780af44. #1 FitSceneButton ReferenceError — added the missing useTranslations() call inside the component. |
- A: viewer-toolbar wallModeOrder now includes 'translucent' so the cycle button can reach that mode and saved translucent scenes no longer fall back to 'cutaway'. - B: count-bearing strings pick the _plural variant when count !== 1. Added scenes.sceneCount_plural and switched three call sites (view-toggles guide/scan summaries, scenes-list header) to pick between singular and plural keys. - C: useTranslations() now memoizes the returned t() on locale, so EditorCommands' effect dep is stable across renders and the command registry no longer tears down + re-registers on every parent render. Fixes the root cause for every other consumer that lists t in a deps array.
- command-palette/index.tsx: extend wallModeLabel Record and the option list to include 'translucent', so the palette can pick that mode and the badge no longer falls through to a missing key. - en.json / zh.json: add commands.translucent = 'Translucent' / '半透明'. - packages/viewer/src/store/use-viewer.d.ts: include 'translucent' in the wallMode / setWallMode types to match the runtime store (WallMode already exported 4 modes from use-viewer.ts; the .d.ts was stale).
- E: added viewer.referenceSettings to en.json ('Reference settings')
and zh.json ('参考设置'). The reference-settings popover chevron
was reading the raw key as its aria-label.
- F: ReferenceListSection no longer composes its count line from a
hardcoded English noun + plural 's' + t('viewer.onThisLevel').
Replaced noun prop with countKey + defaultNoun. The count line now
picks the _plural variant via t(countKey + (count === 1 ? '' : '_plural'))
so Chinese renders '此楼层有 2 个扫描' instead of '2 scan s 在此楼层'.
Removed the now-unused viewer.onThisLevel keys from both catalogs.
- G: scenes-list empty-state card now uses scenes.noScenesSaved
instead of duplicating scenes.noScenes from the subtitle.
|
Addressed Bugbot pass-3 findings E, F, G in commit 94419f1d. E (reference settings aria-label) and G (duplicate copy) just needed the existing catalog keys to be wired up; F was a hardcoded-English count copy that now uses the same plural-aware keys the sibling guide/scan controls use. |
- H: added 22 structureTools.* keys to both catalogs (wall, door,
window, stairs, gableRoof, fence, column, elevator, slab, ceiling,
zone, spawnPoint, shelf, duct, ductFitting, register, hvacUnit,
dwvPipe, trap, pipeFitting, lineset, liquidLine). These resolve
the cursor / floorplan indicator labels in structure-tools.tsx.
- I: level-duplicate-dialog.tsx no longer depends on the core
getLevelDisplayName helper (which ignored its locale arg and
returned hardcoded English). Replaced with a local getLevelLabel
that uses t() and the existing level.groundFloor / level.floor /
level.basement / level.thisLevel keys. Also migrated the whole
file from the bespoke messages[] lookup to the standard
useTranslations() hook. Added level.thisLevel to both catalogs.
- J: ReferenceListSection now takes a defaultNameKey instead of an
English defaultNoun and renders the fallback name via
t(defaultNameKey, { index }) so it picks up the same
viewer.scanDefault / viewer.guideImageDefault translations the
sibling popovers already use.
|
Addressed Bugbot pass-4 findings H, I, J in commit 7ea5aad7. H adds the missing22 structureTools.* keys, I fixes the level-duplicate dialog (which was passing locale to a helper that ignored it) and also migrates that file off the bespoke messages[] lookup, J makes ReferenceListSection's default-name fallback go through t() instead of hardcoded English nouns. Only #4 and #5 remain. |
…himney, dormer, downspout, duct-*, eyebrow-vent, gutter, hvac, lean-to, level, lineset, liquid-line, pipe-*, ridge-vent, shelf, site, skylight, solar-panel, structural-grid, turbine-vent, zone)
So registry consumers can resolve i18n keys at render time without re-touching every node definition. Fallback semantics: when a key is set, the catalog value wins over the hardcoded label/description; when the catalog lookup misses, the existing string is used unchanged.
The registry-driven branch of collectBuildTypes now passes presentation.labelKey into the BuildType.labelKey slot, so the existing render path (`t(type.labelKey) ?? type.label`) actually hits the catalog when a definition ships one. Kinds that don't ship a labelKey fall through to presentation.label unchanged — same behavior as before for the still-unwired kinds.
Each definition now ships `labelKey: 'panel.nodeType.<camelCase-kind>'` right under its hardcoded `label:`. Cabinet's two-kind file carries both `panel.nodeType.cabinet` and `panel.nodeType.cabinetModule`. With the core Presentation type field and the build-tab consumer wired in the prior two commits, these keys are what the Build tab now resolves at render time — picking up the locale-specific catalog value instead of the English label hardcoded into the definition.
So the auto-derived inspector can localize group section titles and enum option labels without re-touching every descriptor. - ParamGroup: optional `labelKey?` parallel to `label`. Wins when the catalog has an entry; falls back to the hardcoded string otherwise. - ParamField (every variant): optional `labelKey?` parallel to `label?`. - ParamField (enum only): optional `optionLabelKeys?: Record<string,string>` maps a raw option value to its catalog key. Same fallback semantics. Descriptors stay English-only on disk; the inspector does the lookup at render time. Kinds that don't ship a key see no behavior change.
…lKeys - parametric-inspector: when a ParamGroup ships a `labelKey`, look it up at render time. `t(key)` returns the key itself on a catalog miss (`useTranslations` impl: `messages[locale][key] ?? key`), so the fallback check is `resolved !== key`. - parametric-field-control: same pattern for field labels. Enum options read through `optionLabelKeys[opt]`; misses fall through to the existing prettifier so un-wired options render unchanged.
20 parametrics.ts files now ship i18n keys for the surfaces the
auto-derived inspector renders:
- 28 group sections resolved via existing `common.position` /
`common.style` / `common.dimensions` keys (Position / Style /
Dimensions / Width). Kinds whose group label has no catalog entry
(Body / Shoulder / Cap / Flues / Cricket / Profile / Topology / …)
are left untouched — wiring them requires new translations, which
is a separate audit.
- 16 enum fields carry an `optionLabelKeys` map for the options that
match an existing `nodes.<kind>.<optionCamel>` key (e.g. chimney's
bodyShape `['square','round']` → `nodes.chimney.{square,round}`;
box-vent's `['standard','low-profile','dome']` → only `dome` has
a catalog entry so only that one is mapped).
Descriptors that don't already have matching catalog entries render
their existing English label exactly as before — no regressions for
un-translated kinds.
Apps/ifc-converter used to reach into packages/editor/src/lib/i18n via a
relative path import, with no declared dependency and no transpilePackages
entry — worked in monorepo dev but would break for any publish/install
scenario. Lift a slim copy of the i18n shim into the app itself:
- apps/ifc-converter/lib/i18n.tsx — useTranslations / I18nProvider /
useLocale / Translator, identical semantics to packages/editor's
(useState-driven locale, no persistence, navigator.language fallback).
- apps/ifc-converter/lib/i18n/{en,zh}.json — start with the 6 keys the
app already consumes via the relative-path import (editor.grid /
.fitScene / .copyToClipboard / .showJsonPreview + meta title /
description). Future commits fill in the ifcConverter.* namespace.
- app/layout.tsx — wrap <ClientBootstrap> in <I18nProvider> so the locale
state is owned by this app instead of falling through the Context's
default value.
Expand the local dictionary from 4 keys (the relative-path-leak leftovers)
to the full set used by apps/ifc-converter — every English string in
app/page.tsx, components/IfcConverter.tsx, components/PreviewToolbar.tsx
and lib/test-files.ts gets a counterpart key.
Sub-namespaces:
- meta.* (title / description) — feeds <head> via client layout
- page.* (title / subtitle / banner) — server-component content lifted
to client in the next commit so t() can resolve
- dropzone.* (prompt + browse) — split so the browse span keeps its
blue styling
- section.* (tryIt / tryItSub / examples / types / levels / jsonTitle)
- action.* (downloadIfc / downloadJson / errorPrefix)
- filter.* (all / none) — shared between type + level filter rows
- search.* (placeholder + no-results + showing-first-50 + 5 match
prefixes + the pset/property triple)
- status.* (starting / complete / loading / converting) — both initial
setState messages and the loading-overlay branching
- viewerHint — the Orbit/Pan/Zoom/Inspect hint under the canvas
- inspector.* (18 row labels — Type, Type Name, IFC Type, Global ID,
Express ID, Level, Geometry, Start, End, Thickness, Height, Width,
Position, Elevation, Sill Height, Polygon, Material, Name)
- units.* (m / mm) — concatenated into "2.500 m" / "150 mm"
- toolbar.* (camera modes + Levels:/Walls: labels + 4 levelModes +
4 wallModes)
- errors.* (invalidFile / conversionFailed / failedToLoad /
couldNotLoad with {filename} + {status} placeholders)
- counts.* (totalNodes / totalTypes / types.one / types.other /
polygonPoints) — .one/.other split for the per-type chip so en can
pluralize ("1 wall" / "3 walls") without an ICU parser
- fallback.level ("Level {index}" — last-resort name when an IFC
level ships with a blank name)
- examples.* (10 × {label, description} + one shared heavy warning)
Pluralization strategy: the local useTranslations is the simple {var}
substitution from packages/editor — no ICU MessageFormat — so plural
handling lives in the dict as explicit one/other pairs and the call
site picks which to invoke.
en.json and zh.json key sets verified identical (104 / 104).
Replace every hardcoded English string in the converter app with a
t('ifcConverter.*') lookup, and drop the relative-path import through
packages/editor that was leaking editor-only state.
- app/page.tsx — server component promoted to 'use client' so it can
read useTranslations(). The banner's middle link + em-tagged
'Load Build' word now split around the link/italic spans via
.subtitle.{start,loadBuild,end}; .banner.{title,bodyBefore,link,
bodyAfter} for the same reason.
- components/IfcConverter.tsx — the main event:
* status.* (4 keys) feeds both the initial setConversionMessage
('Starting conversion...' / 'Conversion complete!') and the
loading-overlay branching ('Loading file...' / 'Converting to Pascal')
* errors.* (4 keys) — invalidFile / conversionFailed / failedToLoad /
couldNotLoad. The couldNotLoad case used to throw 'Could not load
X (404)' which then surfaced verbatim in setError; now it throws
a t()-built string so the error message itself is translated
* counts.{totalNodes,totalTypes,types.one,types.other,polygonPoints}
— the per-type filter chip picks .one vs .other by count so en
pluralises correctly without an ICU parser
* search.* (placeholder + noResults + showingFirst50 + 6 match
prefixes) — searchResults now stores matchKey + matchParams, and
the dropdown renders via t(matchKey, params). Property matches
pass {pset, key, value} for the '{pset}: {key} = {value}' form
* inspector.* (18 row labels) — Type / Type Name / IFC Type /
Global ID / Express ID / Level / Geometry / Start / End /
Thickness / Height / Width / Position / Elevation / Sill Height /
Polygon / Material / Name
* units.{m,mm} — concatenated into '2.500 m' / '150 mm'
* section / action / filter — tryIt, tryItSub, examples, types,
levels, jsonTitle, downloadIfc, downloadJson, errorPrefix,
filter.all / filter.none
* example cards render labelKey / descriptionKey / warningKey via
t() — warningKey (e.g. the 'Very large — may slow down…' notice)
is now a single shared key reused by both heavy examples
* local helper function meta() renamed to nodeMeta inside the
inspector render to avoid shadowing the module-level meta()
typed converter-metadata helper
- components/PreviewToolbar.tsx — toolbar buttons:
- levelLabel / wallLabel const objects deleted; tool labels now come
from ifcConverter.toolbar.levelMode.{stacked|solo|exploded|manual}
and wallMode.{up|cutaway|down|translucent}, looked up via a
small levelLabelKey() / wallLabelKey() helper
- 'Levels: {mode}' / 'Walls: {mode}' use .toolbar.levelsLabel /
.toolbar.wallsLabel with the {mode} placeholder
- FitSceneButton reads .editor.fitScene
- LevelSelector uses .fallback.level for the 'Level {i}' last-resort
name when an IFC level ships with a blank .name
- Perspective / Orthographic toggle reads .toolbar.perspective /
.toolbar.orthographic
- lib/test-files.ts — pure-data file. Replaced label / description /
warning strings with labelKey / descriptionKey / warningKey; the
UI resolves them via t(). detail (file size) keeps the raw string
since 'MB' is a unit the user sees but isn't worth translating.
Import path: '../../packages/editor/src/lib/i18n' (a relative import
into a sibling workspace package with no declared dep) replaced with
'@/lib/i18n' across both consumer components. The local dict is now
the single source of truth — no editor package leak, no Turbopack
specificity.
Verified: 65 distinct ifcConverter.* keys referenced across 11 source
files, every one resolves in both en.json and zh.json (104 / 104 keys).
biome lint clean. bun run check-types reports only the pre-existing
PascalSceneViewer three.js type-collision (out of scope; untouched).
Audit found 55 group sites across 22 parametrics.ts files where the literal English label shipped without a labelKey — the inspector fell back to the raw label and the consumer saw untranslated English in zh. Add labelKey + the matching dict entries. Shared concepts lifted to common.* (used in 2+ kinds with the same meaning): - common.placement (downspout / duct-fitting / duct-terminal / pipe-fitting / pipe-trap) - common.construction (duct-segment / pipe-segment) - common.drainage (lean-to-extension / pipe-segment) - common.transform (scan / spawn) - common.connections (duct-fitting / pipe-fitting) - common.fitting (duct-fitting / pipe-fitting) - common.advanced (lean-to-extension) - common.appearance (scan) - common.mounting (solar-panel — currently only one use, hoisted to common.* anyway since it's a generic UI affordance category) Kind-specific stayed in nodes.<kind>.*: - nodes.chimney.body (Body — chimney tabbed UI) - nodes.door.frame (Frame) - nodes.dormer.dormerRoof (Dormer roof — the parent kind's name collides so the label needs its own slug) - nodes.downspout.hardware (Hardware — strap/terminal knobs) - nodes.ductSegment.air (Air) - nodes.ductTerminal.terminal / face / collar - nodes.fence.structure (the fence version of 'Structure' is the post/rail/slat composition; lean-to's is the post/beam composition — different concepts, different keys) - nodes.gutter.profile / endCaps / hangers - nodes.hvacEquipment.equipment / cabinet / supply / return - nodes.leanToExtension.size / connection / structure - nodes.lineset.lines / insulation - nodes.liquidLine.line - nodes.pipeTrap.trap - nodes.shelf.topology (the one the audit started from) - nodes.skylight.type / curb / opening / lantern - nodes.solarPanel.grid / panelDimensions / frame - nodes.turbineVent.motion Keys added: 31 in en.json, 31 in zh.json (2086 / 2086 parity). 51 distinct labelKey values referenced across 45 parametrics.ts files, zero missing — verified.
Dictionary additions covering shared editor UI (~70 keys), kind-specific inspector labels (zone, gutter, duct-fitting, box-vent, elevator, shared, measurement, etc.), and multi-height-mode strings. en/zh kept in perfect parity; CRLF preserved.
Wires 22 component files (16 in packages/editor, 13 in packages/nodes) to use useTranslations() for previously hardcoded English strings: - editor/index.tsx: Expand sidebar, Camera controls hint, Dismiss camera - editor/floorplan-mode-coordinator.tsx: Dismiss - editor/riser-diagram-panel.tsx: DWV riser diagram - editor/snapshot-capture-overlay.tsx: Close capture mode - editor-2d/floorplan-measurement-tool-layer.tsx: Extrusion height - ui/panels/multi-height-mode.tsx: Follows level, Custom height, Currently - ui/panels/reference-panel.tsx: Capture section title - viewer/viewer-controls-bar.tsx: full helper rewrite for level/wall modes - nodes/dormer, zone, gutter, duct-fitting, shared, box-vent, elevator, block, measurement, skylight, spawn, cabinet — all inspector panels Search was driven by title="/label=" JSX attribute audit per user instruction; covered remaining untouched English surface strings.
…review - packages/core/src/lib/level-name.ts: make `t` parameter optional with an English fallback translator so GLB export and level-print export (non-React callers) no longer throw `t is not defined` on unnamed levels. Mirrors the en.json values for level.groundFloor / level.floor / level.basement. - apps/ifc-converter/lib/i18n.tsx: SSR-safe default locale. Always seed with 'en' so the first client render matches the server HTML byte-for-byte; a useEffect flips to 'zh' after mount if the browser locale is Chinese. Fixes React hydration mismatch flagged by Bugbot at 2026-09-01 14:20Z. - apps/ifc-converter/app/layout.tsx: sync <html lang> to the active locale after mount via a small HtmlLangSync client component, and add suppressHydrationWarning so the initial 'en' server render doesn't trip the warning before the effect runs.
Keep both i18n wiring (aria-label={t('editor.closeCaptureMode')}) and
upstream's backdrop-blur-md class addition. Drone mode and capture camera
rig were auto-merged cleanly.
Bugbot pascalorg#36: `handleDrop` was memoized with an empty dep array but calls `t()` and the render-scoped `handleFile` / `loadAndConvert`. After `I18nProvider` swaps en → zh on mount, the next drop used the first-render translator — invalid-file errors and any subsequent messages stayed English. Drop the `useCallback` wrapper for `handleDrop`. It's consumed by a plain `onDrop={handleDrop}` on a div (no memoized child), so the only thing the memoization was buying was identity stability at the cost of a stale closure. Removing it lets the handler re-bind on every render and pick up the current `t` whenever the locale flips.
Upstream restructured the roof UI in two ways:
1. The footprint-source picker moved into a generic <ToolOptionsPanel
kind="roof" /> that reads useRoofFootprintSource directly. The local
picker block and the getRoofFootprint{Sources,Source} helpers in
build-tab-state.ts are now dead code — dropped, along with the test
cases that exercised them. ROOF_TYPE_OPTIONS stays in labelKey form
so the build tab still renders via t().
2. MaterialPicker dropped the 'all' source filter and switched SOURCE_FILTERS
back to { label, value }. Took upstream's 4-source list but kept the
labelKey form so the catalog chrome stays localizable.
Also added buildTab.roofSource.conicalHint (en/zh) for the conical-roof
hint that used to live in the footprint picker.
…ations
After the upstream roof-UI refactor merge, several i18n structural holes
were left in module-level configs that can't read React context:
- `packages/core/src/registry/types.ts`: added optional `labelKey` /
`descriptionKey` to `ToolOption` and `ToolOptionChoice`, plus
`tooltipKey` to `ToolHintChip`. Mirrors the `labelKey` / fallback
pattern used elsewhere in the editor (e.g. build-tab entries).
- `tool-options-panel.tsx`: resolves `option.labelKey` /
`choice.labelKey` / `choice.descriptionKey` via `t()` and falls
back to the static English `label` / `description` when the key is
untranslated. This keeps `<ToolOptionsPanel>` working for both legacy
and new entries without breaking the static fallback contract.
- `contextual-helper-panel.tsx`: same resolution for
`chip.tooltipKey`.
- `roof/definition.ts`: added `labelKey` / `descriptionKey` on the
`footprintSource` tool option and its choices, and `tooltipKey`
on the placement hint chip. Existing `label` / `description` /
`tooltip` strings are kept as English fallbacks.
- `packages/nodes/src/{roof,window}/naming.ts` (new): thin
Translator-shaped helpers `getDefaultRoofName` /
`getDefaultWindowName` / `getRoofPreviewName`, mirroring
`@pascal-app/core::level-name.ts`. Fallback translator returns the
previous English literal (`Roof {count}`, `Roof preview`) so
non-React callers (export tooling) keep working.
- `roof/tool.tsx` + `window/tool.tsx`: thread `t` from the React
tree into the commit helpers and replace the hardcoded
`${Type} ${count+1}` and `'Roof preview'` literals with the new
helpers. `commitRoofPlacement` and `commitRoofFootprint` now
accept `t` as a final argument.
- `en.json` / `zh.json` (CRLF preserved): added three keys under a
new `nodes.roof.*` namespace (`defaultName`, `preview`,
`toolHints.placementTooltip`); window reuses `{count}` in
`nodes.window.defaultName` which already exists in the catalog
through the `levelName` helper convention.
Verification:
- `diff` of sorted en/zh keys: empty (parity holds).
- `tsc --noEmit` on `packages/editor`: clean for the touched files
(tool-options-panel, contextual-helper-panel).
- `tsc --noEmit` on `packages/nodes`: the only remaining errors in
roof/tool.tsx and window/tool.tsx are pre-existing three.js
dual-`@types/three` clone issues from baseline, unrelated to this
change.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e7449cb. Configure here.
Bugbot flagged (Medium): `apps/ifc-converter/app/layout.tsx` is a Server Component (it exports `metadata` and has no `"use client"`) but defines `HtmlLangSync` inline — a function that calls `useLocale()` (a React hook) and writes `document.documentElement.lang` during render. Two failures follow from that: 1. `useLocale()` runs server-side without a real React context, so it returns the SSR default `en` even when the user's browser is `zh`. 2. The `document.documentElement.lang = locale` write is skipped on the server (no DOM), so `<html lang>` stays `en` after the client `I18nProvider` flips to `zh`. Screen readers and crawlers see the wrong language. Fix: - New `apps/ifc-converter/app/html-lang-sync.tsx` (`'use client'`) defines `HtmlLangSync`, which calls `useLocale()` and defers the DOM write to `useEffect` so it runs after hydration (also avoids the React 19 warning about mutating the DOM during render). - `layout.tsx` drops the inline `HtmlLangSync` and imports it from the new file. The `<html lang="en" suppressHydrationWarning>` default stays — it's the SSR-only initial render before the effect runs. Verification: - `tsc --noEmit` on `apps/ifc-converter`: zero new errors in `layout.tsx` / `html-lang-sync.tsx` (remaining errors are the pre-existing three.js dual-`@types/three` baseline noise).
The 4 keys — `editor.grid`, `editor.fitScene`,
`editor.copyToClipboard`, `editor.showJsonPreview` — were originally
added for `apps/ifc-converter` to translate its 3D viewer controls.
After `apps/ifc-converter` got its own self-contained i18n module
(commit `2289305a`, also in this PR), it stopped importing
`@pascal-app/editor`'s dictionary and now resolves these labels
internally.
`grep -rn 'editor\.\(grid\|fitScene\|copyToClipboard\|showJsonPreview\)'`
across the whole monorepo returns zero hits — these keys are now
unreferenced dead weight in `packages/editor/src/lib/i18n/{en,zh}.json`.
Removing them:
- trims 4 lines from each catalog (2306 → 2302 keys)
- keeps en/zh parity intact (both files drop the same 4 keys)
- preserves CRLF line endings
No code or type changes; the keys were never typed — they're a string-keyed
dictionary read by `useTranslations()` at runtime.

Summary
Restores
useTranslations()across UI components whose hardcoded strings were lost when upstream was merged into this fork's main. The work re-applies commitsb8c87042,af9d9eab,d7a991b4and merges the latest upstreammain(commit51f2a53d).Components localized
panel.nodeType.<x>) flow through the mobile panel sheet title, multi-selection breakdown, and selection barb8c87042/af9d9eab): home page, scene-loader, camera actions, action menus, helpers, icon-railPre-existing regressions fixed
The fork had drifted from upstream in three places that were unrelated to the i18n restore but blocked Bugbot from confirming the branch as clean. They are fixed here:
4100aa30— restoredheaders()-based host detection inapps/editor/app/scenes/page.tsx::resolveBaseUrl(was hardcoded tolocalhost:3000, which broke/scenesfetches in deployed envs). Verified identical to upstreammain.4100aa30— restored the flex layout inpackages/editor/src/components/editor/editor-layout-v2.tsx::RightColumn(a broken 3-column grid was placingtoolbarRightin the center column). Verified identical to upstreammain.3e4b0213—node-display.tsandselection-breakdown.tswere returning hardcoded English (Item,Wall,Slab, and"1 slab · 1 stair · 2 fences"). Refactored both to accept a translator, addedpanel.nodeType.<x>keys.Approach
labelKey/nameKey/detailKeystrings, resolved inside components viat().const t = useTranslations()per component (each component needs its own hook call).useEffectdependency arrays updated to includetwhere applicable.<key>/<key>_pluralconvention (e.g.loadBuild.errors,panel.nodeType.fence/_plural).getNodeDisplay,getTypeDisplay,formatSelectionBreakdown) now take aTranslatorargument so the catalog stays the single source of truth.Translation keys
1217 keys in
en.jsonandzh.json(full parity). The 30-key addition for the panel-manager fix (panel.nodeType.<x>× 14 kinds + singular +_plural) brings this from the previously advertised 1155.Verification
@pascal-app/coreexports remain (separate concern: this fork adds types that the core package hasn't shipped yet — e.g.getLevelDisplayName,DEFAULT_LEVEL_HEIGHT,MeasurementNode,SceneState.materials).main(only conflict:roof-helper.tsxdeleted upstream, accepted the deletion).packages/editor/src/components/ui/panels/pass. The 37 pre-existingsfx-player.test.tsfailures are unrelated (audio-context mocking).conclusion: success, "no issues found! ✅", 0 annotations across 12 review passes.Manual test checklist (suggested for reviewer)
The i18n restoration is mostly auto-tested by catalog parity, but reviewers may want to spot-check rendered copy by toggling the language:
navigator.languagetozh-CN(or change the locale viauseLocale().setLocale('zh')in devtools) and verify the following flows render Chinese:Cmd/Ctrl+K): group headings and command namesMobilePanelSheet title), and the multi-selection breakdown string in the docked panel (MultiSelectionPanel,MultiParametricInspector)?key)navigator.languageisen.bun test packages/editor/src/components/ui/panels/to confirm the breakdown still formats as"1 slab · 1 stair · 2 fences"in English.Note
Low Risk
Mostly copy and presentation; core level-name APIs gain an optional translator with backward-compatible defaults. Scenes fetch error handling is slightly more defensive.
Overview
Extends i18n beyond the editor package: the standalone editor app (home, build tab, save/create scene, scene loader, community viewer toolbar), scenes route, IFC converter app, and
@pascal-app/corelevel naming now resolve copy via translation keys instead of hardcoded English.In
apps/editor, sidebar tabs and banners useuseTranslations(); the build tab and roof-type config switch tolabelKey(registry kinds still fall back topresentation.label). The scenes index is a thin server page that delegates UI to a new clientScenesListwith localized strings;scenes/layoutforces dark theme. Scene fetch is wrapped in try/catch so failures return an empty list. Global CSS adds a thin dark scrollbar; new SVG icons (file, scene, settings) supportcurrentColor.The IFC converter gets its own
I18nProvider(en/zh, browser locale after mount),HtmlLangSyncon<html lang>, and localized page, converter, toolbar, example cards (labelKey/descriptionKeyon test files). Drop handling is no longer a staleuseCallbackso locale changes update error strings.packages/core:getDefaultLevelName/getLevelDisplayNameaccept an optionalTranslator(English fallback for non-React callers);'use client'added on a couple of hook/event modules. Save/create flows use translated default scene names and status messages.Reviewed by Cursor Bugbot for commit a11967c. Bugbot is set up for automated code reviews on this repo. Configure here.