Skip to content

Restore i18n on remaining UI components (rebase onto upstream/main) - #740

Open
leapar wants to merge 110 commits into
pascalorg:mainfrom
leapar:i18n/restore-lost-translations
Open

Restore i18n on remaining UI components (rebase onto upstream/main)#740
leapar wants to merge 110 commits into
pascalorg:mainfrom
leapar:i18n/restore-lost-translations

Conversation

@leapar

@leapar leapar commented Aug 31, 2026

Copy link
Copy Markdown

Summary

Restores useTranslations() across UI components whose hardcoded strings were lost when upstream was merged into this fork's main. The work re-applies commits b8c87042, af9d9eab, d7a991b4 and merges the latest upstream main (commit 51f2a53d).

Components localized

  • command-palette (index + editor-commands): group headings, command labels, wall/level mode cycle, camera switch, rename footer hint
  • settings-panel: project id, 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, walkthrough/preview buttons
  • panel-manager + node-display + selection-breakdown: node-type labels (panel.nodeType.<x>) flow through the mobile panel sheet title, multi-selection breakdown, and selection bar
  • Plus earlier restorations (in b8c87042/af9d9eab): home page, scene-loader, camera actions, action menus, helpers, icon-rail

Pre-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 — restored headers()-based host detection in apps/editor/app/scenes/page.tsx::resolveBaseUrl (was hardcoded to localhost:3000, which broke /scenes fetches in deployed envs). Verified identical to upstream main.
  • 4100aa30 — restored the flex layout in packages/editor/src/components/editor/editor-layout-v2.tsx::RightColumn (a broken 3-column grid was placing toolbarRight in the center column). Verified identical to upstream main.
  • 3e4b0213node-display.ts and selection-breakdown.ts were returning hardcoded English (Item, Wall, Slab, and "1 slab · 1 stair · 2 fences"). Refactored both to accept a translator, added panel.nodeType.<x> keys.

Approach

  • Module-level configs converted from inline labels to labelKey / nameKey / detailKey strings, resolved inside components via t().
  • Pattern: const t = useTranslations() per component (each component needs its own hook call).
  • useEffect dependency arrays updated to include t where applicable.
  • Plural-aware keys follow the <key> / <key>_plural convention (e.g. loadBuild.errors, panel.nodeType.fence / _plural).
  • Helper functions that previously baked in English (getNodeDisplay, getTypeDisplay, formatSelectionBreakdown) now take a Translator argument so the catalog stays the single source of truth.

Translation keys

1217 keys in en.json and zh.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

  • Lint: 0 errors
  • Typecheck: 0 new errors caused by this change. Pre-existing type mismatches between local code and @pascal-app/core exports remain (separate concern: this fork adds types that the core package hasn't shipped yet — e.g. getLevelDisplayName, DEFAULT_LEVEL_HEIGHT, MeasurementNode, SceneState.materials).
  • Both JSON files parse cleanly.
  • Rebased cleanly onto upstream main (only conflict: roof-helper.tsx deleted upstream, accepted the deletion).
  • All 16 tests in packages/editor/src/components/ui/panels/ pass. The 37 pre-existing sfx-player.test.ts failures are unrelated (audio-context mocking).
  • Cursor Bugbot: 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:

  • In the editor, set navigator.language to zh-CN (or change the locale via useLocale().setLocale('zh') in devtools) and verify the following flows render Chinese:
    • Sidebar tab labels (Build / Settings / Plugins / …)
    • Settings panel: project id, export, floorplan, thumbnail, audio, keyboard, danger zone sections
    • Command palette (Cmd/Ctrl+K): group headings and command names
    • Selection: title bar of the mobile panel sheet (MobilePanelSheet title), and the multi-selection breakdown string in the docked panel (MultiSelectionPanel, MultiParametricInspector)
    • Viewer toolbar: view mode buttons, sidebar toggle, display menu
    • Keyboard shortcuts dialog (? key)
    • Floating level selector (floor names)
  • Verify English rendering is unchanged when navigator.language is en.
  • Run 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/core level naming now resolve copy via translation keys instead of hardcoded English.

In apps/editor, sidebar tabs and banners use useTranslations(); the build tab and roof-type config switch to labelKey (registry kinds still fall back to presentation.label). The scenes index is a thin server page that delegates UI to a new client ScenesList with localized strings; scenes/layout forces 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) support currentColor.

The IFC converter gets its own I18nProvider (en/zh, browser locale after mount), HtmlLangSync on <html lang>, and localized page, converter, toolbar, example cards (labelKey / descriptionKey on test files). Drop handling is no longer a stale useCallback so locale changes update error strings.

packages/core: getDefaultLevelName / getLevelDisplayName accept an optional Translator (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.

leapar and others added 14 commits May 30, 2026 23:34
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
Comment thread apps/ifc-converter/components/PreviewToolbar.tsx Outdated
Comment thread packages/editor/src/components/ui/command-palette/editor-commands.tsx Outdated
Comment thread apps/editor/components/save-button.tsx Outdated
Comment thread apps/editor/app/scenes/page.tsx
Comment thread packages/editor/src/components/editor/editor-layout-v2.tsx Outdated
Comment thread apps/editor/app/page.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.
@leapar

leapar commented Aug 31, 2026

Copy link
Copy Markdown
Author

Addressed Bugbot findings #1, #2, #3, and #6 in commit 9780af44.

#1 FitSceneButton ReferenceError — added the missing useTranslations() call inside the component.
#2 Command group keys — fixed all 6 group keys to use the dotted form (commands.group.scene, commands.group.levels, etc.) matching the catalog.
#3 Unprefixed keys in save-button.tsx — migrated the component off the bespoke messages[] lookup to the standard useTranslations() hook with a save.* namespace, removing the global-namespace leak.
#6 Missing catalog keys — added 7 keys (sidebar.build, sidebar.settings, editor.openSavedScenes, editor.rotate, editor.place, editor.forcePlace, editor.guidedConstraintsBypassed) to both en.json and zh.json with full parity (1175 keys each).
Skipped #4 (resolveBaseUrl) and #5 (toolbar layout) — both predate this branch and are out of scope for the i18n restore. Happy to address them in a follow-up if you'd like.

Comment thread apps/editor/components/viewer-toolbar.tsx
Comment thread packages/editor/src/components/ui/action-menu/view-toggles.tsx Outdated
- 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.
@leapar

leapar commented Aug 31, 2026

Copy link
Copy Markdown
Author

Addressed Bugbot pass-2 findings A, B, C in commit 5edefe0b. #4 (resolveBaseUrl) and #5 (toolbar layout) still skipped — pre-existing on this branch and out of scope.

Comment thread packages/editor/src/components/ui/command-palette/index.tsx Outdated
- 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).
@leapar

leapar commented Aug 31, 2026

Copy link
Copy Markdown
Author

Addressed Bugbot pass-3 finding D in commit 10f2eed1. Translucent mode is selectable from the command palette and the commands.translucent badge no longer falls through to a missing key. Only #4 and #5 remain — both predate this branch and are out of scope for the i18n restore.

Comment thread packages/editor/src/components/ui/action-menu/view-toggles.tsx
Comment thread packages/editor/src/components/ui/action-menu/view-toggles.tsx Outdated
Comment thread apps/editor/app/scenes/scenes-list.tsx Outdated
- 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.
@leapar

leapar commented Aug 31, 2026

Copy link
Copy Markdown
Author

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.

Comment thread packages/editor/src/components/ui/action-menu/structure-tools.tsx
Comment thread packages/editor/src/components/ui/level-duplicate-dialog.tsx Outdated
Comment thread packages/editor/src/components/ui/action-menu/view-toggles.tsx Outdated
- 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.
@leapar

leapar commented Aug 31, 2026

Copy link
Copy Markdown
Author

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.

Comment thread apps/editor/app/page.tsx
Comment thread apps/editor/messages/en.json Outdated
Comment thread packages/editor/src/components/ui/action-menu/view-toggles.tsx Outdated
…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).
Comment thread apps/ifc-converter/app/layout.tsx
Comment thread apps/ifc-converter/lib/i18n.tsx
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.
Comment thread apps/ifc-converter/components/IfcConverter.tsx
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread apps/ifc-converter/app/layout.tsx Outdated
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant