From 9b89546a3329a40e764966f256298516a24c1a75 Mon Sep 17 00:00:00 2001 From: leapar Date: Sat, 30 May 2026 23:34:44 +0800 Subject: [PATCH 001/106] =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=961?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + apps/editor/app/client-bootstrap.tsx | 3 +- apps/editor/app/page.tsx | 27 +- apps/editor/app/scenes/page.tsx | 109 +-- apps/editor/app/scenes/scenes-list.tsx | 94 ++ apps/editor/components/viewer-toolbar.tsx | 144 +-- apps/editor/messages/en.json | 271 ++++++ apps/editor/messages/zh.json | 271 ++++++ packages/core/src/events/bus.ts | 2 + .../hooks/spatial-grid/use-spatial-query.ts | 2 + packages/core/src/registry/index.ts | 2 + packages/core/src/registry/types.ts | 2 + .../elevator/elevator-runtime-system.tsx | 2 + .../first-person/build-collider-world.ts | 2 + .../editor/first-person/bvh-ecctrl.tsx | 2 + .../editor/src/components/editor/index.tsx | 59 +- .../components/editor/node-action-menu.tsx | 24 +- .../components/editor/selection-manager.tsx | 2 + .../editor/snapshot-capture-overlay.tsx | 19 +- .../systems/ceiling/ceiling-system.tsx | 2 + .../systems/roof/roof-edit-system.tsx | 2 + .../systems/stair/stair-edit-system.tsx | 2 + .../components/systems/zone/zone-system.tsx | 2 + .../components/tools/column/column-tool.tsx | 2 + .../tools/elevator/elevator-tool.tsx | 2 + .../tools/elevator/move-elevator-tool.tsx | 2 + .../src/components/tools/item/move-tool.tsx | 2 + .../components/tools/item/use-draft-node.ts | 2 + .../tools/item/use-placement-coordinator.tsx | 2 + .../components/tools/roof/move-roof-tool.tsx | 2 + .../src/components/tools/roof/roof-tool.tsx | 2 + .../tools/select/box-select-tool.tsx | 2 + .../tools/shared/affordance-dispatch.ts | 2 + .../components/tools/shared/cursor-sphere.tsx | 7 +- .../tools/shared/polygon-editor.tsx | 2 + .../tools/site/site-boundary-editor.tsx | 2 + .../src/components/tools/stair/stair-tool.tsx | 2 + .../src/components/tools/tool-manager.tsx | 2 + .../tools/zone/zone-boundary-editor.tsx | 2 + .../src/components/tools/zone/zone-tool.tsx | 2 + .../ui/action-menu/action-button.tsx | 2 + .../ui/action-menu/camera-actions.tsx | 16 +- .../ui/action-menu/control-modes.tsx | 37 +- .../ui/action-menu/furnish-tools.tsx | 12 +- .../ui/action-menu/structure-tools.tsx | 40 +- .../ui/action-menu/view-toggles.tsx | 63 +- .../ui/command-palette/editor-commands.tsx | 119 +-- .../components/ui/command-palette/index.tsx | 47 +- .../components/ui/floating-level-selector.tsx | 38 +- .../src/components/ui/helpers/item-helper.tsx | 14 +- .../components/ui/level-duplicate-dialog.tsx | 45 +- .../ui/panels/mobile-selection-bar.tsx | 5 +- .../src/components/ui/panels/node-display.ts | 59 +- .../components/ui/panels/panel-manager.tsx | 7 +- .../src/components/ui/primitives/button.tsx | 2 + .../src/components/ui/primitives/card.tsx | 2 + .../src/components/ui/primitives/input.tsx | 2 + .../ui/primitives/shortcut-token.tsx | 2 + .../src/components/ui/primitives/skeleton.tsx | 2 +- .../src/components/ui/sidebar/icon-rail.tsx | 21 +- .../items-panel/function-tree-panel.tsx | 25 +- .../ui/sidebar/panels/items-panel/index.tsx | 29 +- .../settings-panel/audio-settings-dialog.tsx | 17 +- .../sidebar/panels/settings-panel/index.tsx | 67 +- .../keyboard-shortcuts-dialog.tsx | 101 +- .../settings-panel/load-build-dialog.tsx | 60 +- .../panels/site-panel/box-vent-tree-node.tsx | 4 +- .../panels/site-panel/building-tree-node.tsx | 2 + .../panels/site-panel/ceiling-tree-node.tsx | 6 +- .../panels/site-panel/chimney-tree-node.tsx | 4 +- .../panels/site-panel/column-tree-node.tsx | 4 +- .../panels/site-panel/door-tree-node.tsx | 2 +- .../panels/site-panel/dormer-tree-node.tsx | 4 +- .../panels/site-panel/elevator-tree-node.tsx | 4 +- .../panels/site-panel/fence-tree-node.tsx | 4 +- .../ui/sidebar/panels/site-panel/index.tsx | 125 ++- .../panels/site-panel/inline-rename-input.tsx | 33 +- .../panels/site-panel/item-tree-node.tsx | 4 +- .../panels/site-panel/level-tree-node.tsx | 6 +- .../site-panel/ridge-vent-tree-node.tsx | 4 +- .../panels/site-panel/roof-tree-node.tsx | 20 +- .../panels/site-panel/shelf-tree-node.tsx | 2 +- .../panels/site-panel/slab-tree-node.tsx | 6 +- .../site-panel/solar-panel-tree-node.tsx | 4 +- .../panels/site-panel/spawn-tree-node.tsx | 2 +- .../panels/site-panel/stair-tree-node.tsx | 20 +- .../panels/site-panel/tree-node-actions.tsx | 16 +- .../sidebar/panels/site-panel/tree-node.tsx | 2 + .../panels/site-panel/wall-tree-node.tsx | 4 +- .../panels/site-panel/window-tree-node.tsx | 2 +- .../panels/site-panel/zone-tree-node.tsx | 6 +- .../ui/sidebar/panels/zone-panel/index.tsx | 21 +- .../editor/src/components/ui/slider-demo.tsx | 2 + packages/editor/src/components/ui/slider.tsx | 2 + .../editor/src/components/viewer-overlay.tsx | 130 +-- .../editor/src/hooks/use-contextual-tools.ts | 2 + packages/editor/src/hooks/use-grid-events.ts | 2 + packages/editor/src/hooks/use-keyboard.ts | 2 + packages/editor/src/hooks/use-mobile.ts | 2 + .../editor/src/hooks/use-reduced-motion.ts | 2 + packages/editor/src/index.tsx | 1 + packages/editor/src/lib/i18n.tsx | 60 ++ packages/editor/src/lib/i18n/en.json | 861 ++++++++++++++++++ packages/editor/src/lib/i18n/zh.json | 861 ++++++++++++++++++ packages/editor/src/lib/level-name.ts | 14 +- .../editor/src/store/use-command-registry.ts | 2 + .../src/store/use-palette-view-registry.ts | 2 + packages/nodes/src/box-vent/definition.ts | 6 +- packages/nodes/src/box-vent/geometry.ts | 2 + packages/nodes/src/box-vent/panel.tsx | 4 +- packages/nodes/src/box-vent/tool.tsx | 6 +- packages/nodes/src/building/definition.ts | 4 +- packages/nodes/src/ceiling/definition.ts | 8 +- packages/nodes/src/ceiling/panel.tsx | 34 +- packages/nodes/src/ceiling/tool.tsx | 19 +- packages/nodes/src/chimney/definition.ts | 6 +- packages/nodes/src/chimney/geometry.ts | 2 + packages/nodes/src/chimney/holes.ts | 2 + packages/nodes/src/chimney/panel.tsx | 4 +- packages/nodes/src/chimney/roof-trim.ts | 2 + packages/nodes/src/chimney/tool.tsx | 6 +- packages/nodes/src/column/definition.ts | 2 +- packages/nodes/src/column/panel.tsx | 42 +- packages/nodes/src/door/definition.ts | 8 +- packages/nodes/src/door/move-tool.tsx | 2 + packages/nodes/src/door/panel.tsx | 205 +++-- packages/nodes/src/door/tool.tsx | 6 +- packages/nodes/src/dormer/csg-geometry.ts | 2 + packages/nodes/src/dormer/definition.ts | 8 +- .../src/dormer/panel-actions-section.tsx | 11 +- .../src/dormer/panel-position-section.tsx | 5 +- .../nodes/src/dormer/panel-window-section.tsx | 59 +- packages/nodes/src/dormer/panel.tsx | 18 +- packages/nodes/src/dormer/tool.tsx | 5 +- .../nodes/src/dormer/use-dormer-placement.ts | 2 + packages/nodes/src/elevator/definition.ts | 2 +- packages/nodes/src/elevator/panel.tsx | 14 +- packages/nodes/src/fence/definition.ts | 8 +- packages/nodes/src/fence/tool.tsx | 19 +- packages/nodes/src/guide/definition.ts | 2 +- packages/nodes/src/item/definition.ts | 14 +- packages/nodes/src/level/definition.ts | 4 +- packages/nodes/src/ridge-vent/definition.ts | 6 +- packages/nodes/src/ridge-vent/geometry.ts | 2 + packages/nodes/src/roof-segment/definition.ts | 2 +- packages/nodes/src/roof/definition.ts | 4 +- packages/nodes/src/roof/panel.tsx | 46 +- packages/nodes/src/scan/definition.ts | 2 +- packages/nodes/src/shelf/definition.ts | 6 +- packages/nodes/src/site/definition.ts | 2 +- packages/nodes/src/skylight/definition.ts | 6 +- packages/nodes/src/slab/definition.ts | 10 +- packages/nodes/src/solar-panel/definition.ts | 8 +- packages/nodes/src/solar-panel/geometry.ts | 2 + packages/nodes/src/solar-panel/panel.tsx | 4 +- packages/nodes/src/solar-panel/tool.tsx | 6 +- packages/nodes/src/spawn/definition.ts | 6 +- .../nodes/src/stair-segment/definition.ts | 2 +- packages/nodes/src/stair/definition.ts | 5 +- packages/nodes/src/stair/panel.tsx | 96 +- packages/nodes/src/wall/definition.ts | 10 +- packages/nodes/src/wall/tool.tsx | 15 +- packages/nodes/src/window/definition.ts | 8 +- packages/nodes/src/window/move-tool.tsx | 2 + packages/nodes/src/window/panel.tsx | 152 ++-- packages/nodes/src/window/tool.tsx | 6 +- packages/nodes/src/zone/definition.ts | 2 +- .../viewer/src/components/error-boundary.tsx | 2 + .../src/components/viewer/frame-limiter.tsx | 2 + .../src/components/viewer/ground-occluder.tsx | 2 + .../viewer/src/components/viewer/lights.tsx | 2 + .../src/components/viewer/perf-monitor.tsx | 2 + .../src/components/viewer/post-processing.tsx | 2 + .../src/components/viewer/scene-bvh.tsx | 2 + .../src/components/viewer/viewer-camera.tsx | 2 + packages/viewer/src/hooks/use-asset-url.ts | 2 + packages/viewer/src/hooks/use-gltf-ktx2.tsx | 2 + packages/viewer/src/hooks/use-node-events.ts | 2 + packages/viewer/src/lib/ink-edges.ts | 2 + packages/viewer/src/lib/materials.ts | 2 + .../viewer/src/lib/merged-outline-node.ts | 1 + .../src/systems/ceiling/ceiling-system.tsx | 2 + .../src/systems/column/column-geometry.ts | 2 + .../systems/door/door-animation-system.tsx | 2 + .../viewer/src/systems/door/door-system.tsx | 2 + .../elevator/elevator-interaction-system.tsx | 2 + .../viewer/src/systems/fence/fence-system.tsx | 2 + .../floor-elevation-system.tsx | 2 + .../viewer/src/systems/guide/guide-system.tsx | 2 + .../systems/item-light/item-light-system.tsx | 2 + .../viewer/src/systems/item/item-system.tsx | 2 + .../viewer/src/systems/level/level-system.tsx | 2 + .../viewer/src/systems/roof/roof-system.tsx | 2 + .../viewer/src/systems/scan/scan-system.tsx | 2 + .../viewer/src/systems/slab/slab-system.tsx | 2 + .../viewer/src/systems/stair/stair-system.tsx | 2 + .../viewer/src/systems/wall/wall-cutout.tsx | 2 + .../viewer/src/systems/wall/wall-materials.ts | 2 + .../viewer/src/systems/wall/wall-system.tsx | 2 + .../window/window-animation-system.tsx | 2 + .../src/systems/window/window-system.tsx | 2 + .../viewer/src/systems/zone/zone-system.tsx | 2 + 202 files changed, 4055 insertions(+), 1143 deletions(-) create mode 100644 apps/editor/app/scenes/scenes-list.tsx create mode 100644 apps/editor/messages/en.json create mode 100644 apps/editor/messages/zh.json create mode 100644 packages/editor/src/lib/i18n.tsx create mode 100644 packages/editor/src/lib/i18n/en.json create mode 100644 packages/editor/src/lib/i18n/zh.json diff --git a/.gitignore b/.gitignore index d01fc0bdd8..52bb31afcd 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ og-test # Worktrees .worktrees +/bin +*.lock diff --git a/apps/editor/app/client-bootstrap.tsx b/apps/editor/app/client-bootstrap.tsx index 610eac4e65..2ff6ce5d48 100644 --- a/apps/editor/app/client-bootstrap.tsx +++ b/apps/editor/app/client-bootstrap.tsx @@ -9,6 +9,7 @@ // `loaded` guard inside `../lib/bootstrap` keeps the side effect // idempotent under HMR. import '../lib/bootstrap' +import { I18nProvider } from '@pascal-app/editor' import { type ReactNode, useEffect } from 'react' export function ClientBootstrap({ children }: { children: ReactNode }) { @@ -19,5 +20,5 @@ export function ClientBootstrap({ children }: { children: ReactNode }) { // is already a direct dep, so we don't need the CDN auto-global. import('react-scan').then(({ scan }) => scan({ enabled: true })) }, []) - return children + return {children} } diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index 80fb1a9621..b406604d1d 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Editor, ItemsPanel } from '@pascal-app/editor' +import { Editor, ItemsPanel, useLocale, messages } from '@pascal-app/editor' import { Layers, Package, Settings } from 'lucide-react' import Link from 'next/link' import { @@ -11,21 +11,21 @@ import { const SIDEBAR_TABS = [ { id: 'site', - label: 'Scene', + labelKey: 'sidebar.scene', component: () => null, mobileDefaultSnap: 0.5, mobileIcon: , }, { id: 'items', - label: 'Items', + labelKey: 'sidebar.items', component: ItemsPanel, mobileDefaultSnap: 0.5, mobileIcon: , }, { id: 'settings', - label: 'Settings', + labelKey: 'common.settings', component: () => null, mobileDefaultSnap: 0.5, mobileIcon: , @@ -35,20 +35,31 @@ const SIDEBAR_TABS = [ const PROJECT_ID = 'local-editor' export default function Home() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + + const sidebarTabs = SIDEBAR_TABS.map((tab) => ({ + id: tab.id, + label: t(tab.labelKey), + component: tab.component, + mobileDefaultSnap: tab.mobileDefaultSnap, + mobileIcon: tab.mobileIcon, + })) + return (
{PROJECT_ID === 'local-editor' && (
- Local editor — scenes are not saved. + {t('editor.localWarning')} - Open recent scenes + {t('editor.openRecent')} · - Create new + {t('editor.createNew')}
@@ -56,7 +67,7 @@ export default function Home() { } viewerToolbarRight={} /> diff --git a/apps/editor/app/scenes/page.tsx b/apps/editor/app/scenes/page.tsx index 39b8dbf07a..ebd0bfc959 100644 --- a/apps/editor/app/scenes/page.tsx +++ b/apps/editor/app/scenes/page.tsx @@ -1,7 +1,7 @@ -import { headers } from 'next/headers' import Link from 'next/link' import { CreateSceneButton } from '@/components/save-button' import type { SceneMeta } from '@/components/scene-loader' +import { ScenesList } from './scenes-list' export const dynamic = 'force-dynamic' @@ -9,109 +9,30 @@ async function resolveBaseUrl(): Promise { if (process.env.NEXT_PUBLIC_APP_URL) { return process.env.NEXT_PUBLIC_APP_URL } - const h = await headers() - const host = h.get('x-forwarded-host') ?? h.get('host') - const proto = h.get('x-forwarded-proto') ?? 'http' - if (!host) { - return 'http://localhost:3000' - } - return `${proto}://${host}` + return 'http://localhost:3000' } async function fetchScenes(): Promise { const base = await resolveBaseUrl() - const response = await fetch(`${base}/api/scenes?limit=50`, { - cache: 'no-store', - }) - if (!response.ok) { - return [] - } - const payload = (await response.json()) as { scenes?: SceneMeta[] } | SceneMeta[] - if (Array.isArray(payload)) { - return payload - } - return payload.scenes ?? [] -} - -function formatDate(iso: string): string { try { - return new Date(iso).toLocaleString() + const response = await fetch(`${base}/api/scenes?limit=50`, { + cache: 'no-store', + }) + if (!response.ok) { + return [] + } + const payload = (await response.json()) as { scenes?: SceneMeta[] } | SceneMeta[] + if (Array.isArray(payload)) { + return payload + } + return payload.scenes ?? [] } catch { - return iso + return [] } } export default async function ScenesPage() { const scenes = await fetchScenes() - return ( -
-
-
- - -
-
- -
-

Your scenes

-

- {scenes.length === 0 - ? 'No scenes yet. Create one to get started.' - : `${scenes.length} scene${scenes.length === 1 ? '' : 's'}.`} -

- - {scenes.length === 0 ? ( -
-

You haven't saved any scenes yet.

-
- -
-
- ) : ( -
    - {scenes.map((scene) => ( -
  • - -
    - {scene.thumbnailUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - {scene.name} - ) : ( - No thumbnail - )} -
    -
    -

    - {scene.name} -

    -
    - {scene.nodeCount} nodes - -
    -
    - -
  • - ))} -
- )} -
-
- ) + return } diff --git a/apps/editor/app/scenes/scenes-list.tsx b/apps/editor/app/scenes/scenes-list.tsx new file mode 100644 index 0000000000..2abdbb7d61 --- /dev/null +++ b/apps/editor/app/scenes/scenes-list.tsx @@ -0,0 +1,94 @@ +'use client' + +import Link from 'next/link' +import { CreateSceneButton } from '@/components/save-button' +import type { SceneMeta } from '@/components/scene-loader' +import { useLocale, messages } from '../../../../packages/editor/src/lib/i18n' + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleString() + } catch { + return iso + } +} + +export function ScenesList({ scenes }: { scenes: SceneMeta[] }) { + const { locale } = useLocale() + const t = (key: string, params?: Record) => { + const text = (messages[locale] as Record)[key] || key + if (!params) return text + return text.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`)) + } + + return ( +
+
+
+ + +
+
+ +
+

{t('scenes.title')}

+

+ {scenes.length === 0 + ? t('scenes.noScenes') + : t('scenes.sceneCount', { count: scenes.length })} +

+ + {scenes.length === 0 ? ( +
+

{t('scenes.noScenesSaved')}

+
+ +
+
+ ) : ( +
    + {scenes.map((scene) => ( +
  • + +
    + {scene.thumbnailUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {scene.name} + ) : ( + {t('scenes.noThumbnail')} + )} +
    +
    +

    + {scene.name} +

    +
    + {t('scenes.nodes', { count: scene.nodeCount })} + +
    +
    + +
  • + ))} +
+ )} +
+
+ ) +} diff --git a/apps/editor/components/viewer-toolbar.tsx b/apps/editor/components/viewer-toolbar.tsx index 0428bbe417..94b394dc38 100644 --- a/apps/editor/components/viewer-toolbar.tsx +++ b/apps/editor/components/viewer-toolbar.tsx @@ -7,6 +7,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, useEditor, + useLocale, + messages, useSidebarStore, type ViewMode, } from '@pascal-app/editor' @@ -52,10 +54,9 @@ function ToolbarTooltip({ children, label }: { children: ReactNode; label: strin ) } -const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [ +const VIEW_MODES: { id: ViewMode; icon: React.ReactNode }[] = [ { id: '3d', - label: '3D', icon: ( , }, ] const levelModeOrder = ['stacked', 'exploded', 'solo'] as const -const levelModeLabels: Record = { - manual: 'Stack', - stacked: 'Stack', - exploded: 'Exploded', - solo: 'Solo', -} const wallModeOrder = ['cutaway', 'up', 'down'] as const -const wallModeConfig: Record = { - up: { icon: '/icons/room.png', label: 'Full height' }, - cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' }, - down: { icon: '/icons/walllow.png', label: 'Low' }, +const wallModeConfig: Record = { + up: { icon: '/icons/room.png' }, + cutaway: { icon: '/icons/wallcut.png' }, + down: { icon: '/icons/walllow.png' }, } const SHADING_OPTIONS = [ - { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box }, - { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles }, + { id: 'solid', nameKey: 'viewer.solid', detailKey: 'viewer.flatAndFast', icon: Box }, + { id: 'rendered', nameKey: 'viewer.rendered', detailKey: 'viewer.fullAO', icon: Sparkles }, ] as const function ViewModeControl() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const viewMode = useEditor((state) => state.viewMode) const setViewMode = useEditor((state) => state.setViewMode) + const modeLabels: Record = { + '3d': t('viewer.viewMode3D'), + '2d': t('viewer.viewMode2D'), + split: t('viewer.viewModeSplit'), + } + return (
{VIEW_MODES.map((mode) => { const isActive = viewMode === mode.id + const label = modeLabels[mode.id] return ( - + ) @@ -139,6 +141,8 @@ function ViewModeControl() { } function CollapseSidebarButton() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const isCollapsed = useSidebarStore((state) => state.isCollapsed) const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed) @@ -148,9 +152,9 @@ function CollapseSidebarButton() { return (
- + ) } function WallModeToggle() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const wallMode = useViewer((state) => state.wallMode) const setWallMode = useViewer((state) => state.setWallMode) const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway! @@ -219,8 +228,10 @@ function WallModeToggle() { if (next) setWallMode(next) } + const labelKey = wallMode === 'up' ? 'toolbar.fullHeight' : wallMode === 'down' ? 'toolbar.low' : 'toolbar.cutaway' + return ( - + ) } function RenderModeMenu() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const shading = useViewer((state) => state.shading) const setShading = useViewer((state) => state.setShading) const active = SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0] @@ -247,10 +260,10 @@ function RenderModeMenu() { return ( - + @@ -270,8 +283,8 @@ function RenderModeMenu() { setShading(option.id)}>
- {option.name} - {option.detail} + {t(option.nameKey)} + {t(option.detailKey)}
{shading === option.id ? : null}
@@ -283,16 +296,18 @@ function RenderModeMenu() { } function SceneThemeMenu() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const sceneTheme = useViewer((state) => state.sceneTheme) const setSceneTheme = useViewer((state) => state.setSceneTheme) const active = getSceneTheme(sceneTheme) return ( - + ) diff --git a/apps/editor/messages/en.json b/apps/editor/messages/en.json new file mode 100644 index 0000000000..91d14f11b7 --- /dev/null +++ b/apps/editor/messages/en.json @@ -0,0 +1,271 @@ +{ + "common": { + "save": "Save", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm", + "close": "Close", + "settings": "Settings", + "home": "Home", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "search": "Search", + "noResults": "No results found", + "untitled": "Untitled", + "edit": "Edit", + "move": "Move", + "duplicate": "Duplicate", + "copy": "Copy", + "paste": "Paste", + "cut": "Cut", + "undo": "Undo", + "redo": "Redo", + "rotate": "Rotate", + "select": "Select", + "deselect": "Deselect", + "show": "Show", + "hide": "Hide", + "open": "Open", + "add": "Add", + "remove": "Remove", + "reset": "Reset", + "apply": "Apply", + "done": "Done", + "back": "Back", + "next": "Next", + "yes": "Yes", + "no": "No" + }, + "nav": { + "home": "Home", + "scenes": "Scenes", + "privacy": "Privacy", + "terms": "Terms" + }, + "sidebar": { + "scene": "Scene", + "items": "Items", + "settings": "Settings", + "site": "Site", + "structure": "Structure", + "furnish": "Furnish", + "view": "View", + "paint": "Paint" + }, + "editor": { + "localWarning": "Local editor — scenes are not saved.", + "openRecent": "Open recent scenes", + "createNew": "Create new" + }, + "scenes": { + "title": "Your scenes", + "noScenes": "No scenes yet. Create one to get started.", + "noScenesSaved": "You haven't saved any scenes yet.", + "sceneCount": "{count} scene | {count} scenes", + "noThumbnail": "No thumbnail", + "nodes": "{count} nodes", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this scene?", + "createScene": "Create scene", + "allScenes": "All scenes" + }, + "toolbar": { + "cutaway": "Cutaway", + "low": "Low", + "up": "Up", + "fullHeight": "Full Height" + }, + "errors": { + "loadFailed": "Failed to load", + "saveFailed": "Failed to save", + "deleteFailed": "Failed to delete", + "networkError": "Network error. Please try again.", + "conflict": "Another session saved first — refresh?", + "anotherSessionSaved": "Another session saved first — refresh?", + "noActiveProject": "No active project. Please open a project first." + }, + "privacy": { + "title": "Privacy Policy", + "lastUpdated": "Last updated" + }, + "terms": { + "title": "Terms of Service", + "lastUpdated": "Last updated" + }, + "nodeTypes": { + "site": "Site", + "building": "Building", + "level": "Level", + "wall": "Wall", + "door": "Door", + "window": "Window", + "slab": "Slab", + "ceiling": "Ceiling", + "roof": "Roof", + "zone": "Zone", + "item": "Item", + "fence": "Fence", + "column": "Column", + "elevator": "Elevator", + "stair": "Stair", + "stairs": "Stairs", + "gableRoof": "Gable Roof", + "spawn": "Spawn Point", + "shelf": "Shelf", + "scan": "Scan", + "guide": "Guide", + "chimney": "Chimney", + "skyLight": "Skylight" + }, + "commands": { + "wallTool": "Wall Tool", + "slabTool": "Slab Tool", + "ceilingTool": "Ceiling Tool", + "doorTool": "Door Tool", + "windowTool": "Window Tool", + "itemTool": "Item Tool", + "stairTool": "Stair Tool", + "zoneTool": "Zone Tool", + "deleteSelection": "Delete Selection", + "materialPaint": "Material Paint", + "goToLevel": "Go to Level", + "addLevel": "Add Level", + "renameLevel": "Rename Level", + "deleteLevel": "Delete Level", + "wallMode": "Wall Mode", + "levelMode": "Level Mode", + "switchToSolid": "Switch to Solid", + "switchToRendered": "Switch to Rendered", + "takeSnapshot": "Take Snapshot", + "enterPreview": "Enter Preview", + "exitPreview": "Exit Preview", + "toggleFullscreen": "Toggle Fullscreen", + "exportSceneJson": "Export Scene (JSON)", + "export3DModel": "Export 3D Model (GLB)", + "copyShareLink": "Copy Share Link", + "takeScreenshot": "Take Screenshot" + }, + "controlModes": { + "select": "Select", + "boxSelect": "Box select", + "editSite": "Edit site", + "build": "Build", + "materialPaint": "Material Paint", + "furnish": "Furnish", + "zone": "Zone", + "exitSiteEditing": "Exit site editing", + "siteEditGroundOnly": "Site editing only available on ground floor" + }, + "structureTools": { + "wall": "Wall", + "door": "Door", + "window": "Window", + "stairs": "Stairs", + "gableRoof": "Gable Roof", + "fence": "Fence", + "column": "Column", + "elevator": "Elevator", + "slab": "Slab", + "ceiling": "Ceiling", + "zone": "Zone", + "spawnPoint": "Spawn Point", + "shelf": "Shelf" + }, + "furnishTools": { + "furniture": "Furniture", + "appliance": "Appliance", + "kitchen": "Kitchen", + "bathroom": "Bathroom", + "outdoor": "Outdoor" + }, + "itemSources": { + "library": "Library", + "community": "Community", + "mine": "Mine" + }, + "shortcuts": { + "editorNavigation": "Editor Navigation", + "modesAndHistory": "Modes & History", + "selection": "Selection", + "drawingTools": "Drawing Tools", + "itemPlacement": "Item Placement", + "camera": "Camera", + "cancelTool": "Cancel the active tool and return to Select mode", + "deleteSelected": "Delete selected objects", + "undo": "Undo", + "redo": "Redo", + "addToSelection": "Add or remove an object from multi-selection", + "rotateClockwise": "Rotate item clockwise, or toggle selected door open/closed", + "rotateCounterClockwise": "Rotate item counter-clockwise, or close selected door", + "selectNextLevel": "Select next level in the active building", + "selectPreviousLevel": "Select previous level in the active building" + }, + "panel": { + "scaleAndOpacity": "Scale & Opacity", + "editScale": "Edit Scale", + "setScale": "Set Scale", + "hideScale": "Hide Scale", + "showScale": "Show Scale", + "duplicateLevel": "Duplicate level", + "duplicateLevelWithOptions": "Duplicate level with options", + "deleteLevel": "Delete level", + "groundLevelCannotBeDeleted": "The ground level cannot be deleted", + "deleteGuideImage": "Delete guide image", + "deleteScan": "Delete scan", + "showReferenceFloor": "Show reference floor", + "hideReferenceFloor": "Hide reference floor", + "addLevelAbove": "Add level above", + "addLevelBelow": "Add level below", + "noEdgeLines": "No edge lines", + "off": "Off", + "cutOut": "Cut Out", + "editProperties": "Edit properties", + "closeCaptureMode": "Close capture mode", + "close": "Close", + "move": "Move", + "rename": "Rename", + "intensity": "Intensity" + }, + "dialog": { + "editorNavigation": "Editor Navigation", + "modesAndHistory": "Modes & History", + "selection": "Selection", + "drawingTools": "Drawing Tools", + "itemPlacement": "Item Placement", + "camera": "Camera" + }, + "levelDuplicate": { + "everything": "Everything", + "structureOnly": "Structure only", + "structurePlusMaterials": "Structure + materials", + "structurePlusFurniture": "Structure + furniture" + }, + "stats": { + "sites": "Sites", + "buildings": "Buildings", + "levels": "Levels", + "walls": "Walls", + "doors": "Doors", + "windows": "Windows", + "items": "Items", + "slabs": "Slabs", + "ceilings": "Ceilings", + "zones": "Zones", + "scans": "Scans" + }, + "camera": { + "leftClick": "Left click", + "middleClick": "Middle click", + "rightClick": "Right click", + "scrollWheel": "Scroll wheel", + "space": "Space" + }, + "viewer": { + "building": "Building", + "item": "Item", + "fullHeight": "Full Height", + "cutaway": "Cutaway", + "low": "Low" + } +} diff --git a/apps/editor/messages/zh.json b/apps/editor/messages/zh.json new file mode 100644 index 0000000000..0e1fbd647d --- /dev/null +++ b/apps/editor/messages/zh.json @@ -0,0 +1,271 @@ +{ + "common": { + "save": "保存", + "delete": "删除", + "cancel": "取消", + "confirm": "确认", + "close": "关闭", + "settings": "设置", + "home": "首页", + "loading": "加载中...", + "error": "错误", + "success": "成功", + "search": "搜索", + "noResults": "未找到结果", + "untitled": "未命名", + "edit": "编辑", + "move": "移动", + "duplicate": "复制", + "copy": "复制", + "paste": "粘贴", + "cut": "剪切", + "undo": "撤销", + "redo": "重做", + "rotate": "旋转", + "select": "选择", + "deselect": "取消选择", + "show": "显示", + "hide": "隐藏", + "open": "打开", + "add": "添加", + "remove": "移除", + "reset": "重置", + "apply": "应用", + "done": "完成", + "back": "返回", + "next": "下一步", + "yes": "是", + "no": "否" + }, + "nav": { + "home": "首页", + "scenes": "场景", + "privacy": "隐私", + "terms": "条款" + }, + "sidebar": { + "scene": "场景", + "items": "物品", + "settings": "设置", + "site": "场地", + "structure": "结构", + "furnish": "家具", + "view": "视图", + "paint": "绘制" + }, + "editor": { + "localWarning": "本地编辑器 — 场景不会保存。", + "openRecent": "打开最近场景", + "createNew": "创建新场景" + }, + "scenes": { + "title": "您的场景", + "noScenes": "还没有场景。创建一个开始吧。", + "noScenesSaved": "您还没有保存任何场景。", + "sceneCount": "{count} 个场景", + "noThumbnail": "无缩略图", + "nodes": "{count} 个节点", + "delete": "删除", + "deleteConfirm": "确定要删除这个场景吗?", + "createScene": "创建场景", + "allScenes": "所有场景" + }, + "toolbar": { + "cutaway": "剖面", + "low": "低", + "up": "高", + "fullHeight": "全高" + }, + "errors": { + "loadFailed": "加载失败", + "saveFailed": "保存失败", + "deleteFailed": "删除失败", + "networkError": "网络错误,请重试。", + "conflict": "保存失败", + "anotherSessionSaved": "另一个会话已保存 — 刷新?", + "noActiveProject": "没有活动项目。请先打开一个项目。" + }, + "privacy": { + "title": "隐私政策", + "lastUpdated": "最后更新" + }, + "terms": { + "title": "服务条款", + "lastUpdated": "最后更新" + }, + "nodeTypes": { + "site": "场地", + "building": "建筑", + "level": "楼层", + "wall": "墙", + "door": "门", + "window": "窗", + "slab": "楼板", + "ceiling": "天花板", + "roof": "屋顶", + "zone": "区域", + "item": "物品", + "fence": "围栏", + "column": "柱子", + "elevator": "电梯", + "stair": "楼梯", + "stairs": "楼梯", + "gableRoof": "人字屋顶", + "spawn": "出生点", + "shelf": "搁架", + "scan": "扫描", + "guide": "参考图", + "chimney": "烟囱", + "skyLight": "天窗" + }, + "commands": { + "wallTool": "墙体工具", + "slabTool": "楼板工具", + "ceilingTool": "天花板工具", + "doorTool": "门工具", + "windowTool": "窗工具", + "itemTool": "物品工具", + "stairTool": "楼梯工具", + "zoneTool": "区域工具", + "deleteSelection": "删除选中", + "materialPaint": "材质绘制", + "goToLevel": "跳转楼层", + "addLevel": "添加楼层", + "renameLevel": "重命名楼层", + "deleteLevel": "删除楼层", + "wallMode": "墙体模式", + "levelMode": "楼层模式", + "switchToSolid": "切换到实体", + "switchToRendered": "切换到渲染", + "takeSnapshot": "快照", + "enterPreview": "进入预览", + "exitPreview": "退出预览", + "toggleFullscreen": "切换全屏", + "exportSceneJson": "导出场景 (JSON)", + "export3DModel": "导出 3D 模型 (GLB)", + "copyShareLink": "复制分享链接", + "takeScreenshot": "截图" + }, + "controlModes": { + "select": "选择", + "boxSelect": "框选", + "editSite": "编辑场地", + "build": "构建", + "materialPaint": "材质绘制", + "furnish": "家具", + "zone": "区域", + "exitSiteEditing": "退出场地编辑", + "siteEditGroundOnly": "场地编辑仅在地面层可用" + }, + "structureTools": { + "wall": "墙", + "door": "门", + "window": "窗", + "stairs": "楼梯", + "gableRoof": "人字屋顶", + "fence": "围栏", + "column": "柱子", + "elevator": "电梯", + "slab": "楼板", + "ceiling": "天花板", + "zone": "区域", + "spawnPoint": "出生点", + "shelf": "搁架" + }, + "furnishTools": { + "furniture": "家具", + "appliance": "电器", + "kitchen": "厨房", + "bathroom": "浴室", + "outdoor": "户外" + }, + "itemSources": { + "library": "库", + "community": "社区", + "mine": "我的" + }, + "shortcuts": { + "editorNavigation": "编辑器导航", + "modesAndHistory": "模式与历史", + "selection": "选择", + "drawingTools": "绘图工具", + "itemPlacement": "物品放置", + "camera": "相机", + "cancelTool": "取消当前工具并返回选择模式", + "deleteSelected": "删除选中对象", + "undo": "撤销", + "redo": "重做", + "addToSelection": "添加或移除多选中的对象", + "rotateClockwise": "顺时针旋转物品,或切换门的开/关", + "rotateCounterClockwise": "逆时针旋转物品,或关闭选中的门", + "selectNextLevel": "选择活动建筑中的下一个楼层", + "selectPreviousLevel": "选择活动建筑中的上一个楼层" + }, + "panel": { + "scaleAndOpacity": "比例与不透明度", + "editScale": "编辑比例", + "setScale": "设置比例", + "hideScale": "隐藏比例", + "showScale": "显示比例", + "duplicateLevel": "复制楼层", + "duplicateLevelWithOptions": "带选项复制楼层", + "deleteLevel": "删除楼层", + "groundLevelCannotBeDeleted": "地面楼层无法删除", + "deleteGuideImage": "删除参考图", + "deleteScan": "删除扫描", + "showReferenceFloor": "显示参考地面", + "hideReferenceFloor": "隐藏参考地面", + "addLevelAbove": "上方添加楼层", + "addLevelBelow": "下方添加楼层", + "noEdgeLines": "无边缘线", + "off": "关闭", + "cutOut": "切除", + "editProperties": "编辑属性", + "closeCaptureMode": "关闭捕捉模式", + "close": "关闭", + "move": "移动", + "rename": "重命名", + "intensity": "强度" + }, + "dialog": { + "editorNavigation": "编辑器导航", + "modesAndHistory": "模式与历史", + "selection": "选择", + "drawingTools": "绘图工具", + "itemPlacement": "物品放置", + "camera": "相机" + }, + "levelDuplicate": { + "everything": "全部", + "structureOnly": "仅结构", + "structurePlusMaterials": "结构 + 材质", + "structurePlusFurniture": "结构 + 家具" + }, + "stats": { + "sites": "场地", + "buildings": "建筑", + "levels": "楼层", + "walls": "墙", + "doors": "门", + "windows": "窗", + "items": "物品", + "slabs": "楼板", + "ceilings": "天花板", + "zones": "区域", + "scans": "扫描" + }, + "camera": { + "leftClick": "左键", + "middleClick": "中键", + "rightClick": "右键", + "scrollWheel": "滚轮", + "space": "空格" + }, + "viewer": { + "building": "建筑", + "item": "物品", + "fullHeight": "全高", + "cutaway": "剖面", + "low": "低" + } +} diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index 1e92b7ca11..1d5ddf35b1 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -1,3 +1,5 @@ +'use client' + import type { ThreeEvent } from '@react-three/fiber' import mitt from 'mitt' import type { Object3D } from 'three' diff --git a/packages/core/src/hooks/spatial-grid/use-spatial-query.ts b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts index 014ae23292..f9015eb4f6 100644 --- a/packages/core/src/hooks/spatial-grid/use-spatial-query.ts +++ b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts @@ -1,3 +1,5 @@ +'use client' + import { useCallback } from 'react' import type { CeilingNode, LevelNode, WallNode } from '../../schema' import { spatialGridManager } from './spatial-grid-manager' diff --git a/packages/core/src/registry/index.ts b/packages/core/src/registry/index.ts index b3226b31eb..1bf49c9fee 100644 --- a/packages/core/src/registry/index.ts +++ b/packages/core/src/registry/index.ts @@ -1,3 +1,5 @@ +'use client' + export type { ArcResizeHandle, Cursor, diff --git a/packages/core/src/registry/types.ts b/packages/core/src/registry/types.ts index 5492da86d1..1b26c260d1 100644 --- a/packages/core/src/registry/types.ts +++ b/packages/core/src/registry/types.ts @@ -1,3 +1,5 @@ +'use client' + import type { ComponentType } from 'react' import type { BufferGeometry, Object3D } from 'three' import type { ZodObject, z } from 'zod' diff --git a/packages/core/src/systems/elevator/elevator-runtime-system.tsx b/packages/core/src/systems/elevator/elevator-runtime-system.tsx index 91c897a23a..429e96d05b 100644 --- a/packages/core/src/systems/elevator/elevator-runtime-system.tsx +++ b/packages/core/src/systems/elevator/elevator-runtime-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { useFrame } from '@react-three/fiber' import { stepElevatorRuntimes } from './elevator-runtime' diff --git a/packages/editor/src/components/editor/first-person/build-collider-world.ts b/packages/editor/src/components/editor/first-person/build-collider-world.ts index f977aab872..4280bc5d54 100644 --- a/packages/editor/src/components/editor/first-person/build-collider-world.ts +++ b/packages/editor/src/components/editor/first-person/build-collider-world.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type DoorNode, diff --git a/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx b/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx index 3c45965992..7b4e689acc 100644 --- a/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx +++ b/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx @@ -1,3 +1,5 @@ +'use client' + import '../../../three-types' import { TransformControls, useKeyboardControls } from '@react-three/drei' import { type ThreeElements, useFrame, useThree } from '@react-three/fiber' diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index f21499a087..f8366bad5f 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -28,6 +28,7 @@ import { writePersistedSelection, } from '../../lib/scene' import { initSFXBus } from '../../lib/sfx-bus' +import { messages, useLocale } from '../../lib/i18n' import useEditor from '../../store/use-editor' import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system' import { CeilingSystem } from '../systems/ceiling/ceiling-system' @@ -328,46 +329,46 @@ type ShortcutKey = { } type CameraControlHint = { - action: string + actionKey: string keys: ShortcutKey[] alternativeKeys?: ShortcutKey[] } const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [ { - action: 'Pan', + actionKey: 'editor.cameraPan', keys: [{ value: 'Space' }, { value: 'Left click' }], }, - { action: 'Rotate', keys: [{ value: 'Right click' }] }, - { action: 'Zoom', keys: [{ value: 'Scroll' }] }, + { actionKey: 'editor.cameraRotate', keys: [{ value: 'Right click' }] }, + { actionKey: 'editor.cameraZoom', keys: [{ value: 'Scroll' }] }, ] const PREVIEW_CAMERA_CONTROL_HINTS: CameraControlHint[] = [ - { action: 'Pan', keys: [{ value: 'Left click' }] }, - { action: 'Rotate', keys: [{ value: 'Right click' }] }, - { action: 'Zoom', keys: [{ value: 'Scroll' }] }, + { actionKey: 'editor.cameraPan', keys: [{ value: 'Left click' }] }, + { actionKey: 'editor.cameraRotate', keys: [{ value: 'Right click' }] }, + { actionKey: 'editor.cameraZoom', keys: [{ value: 'Scroll' }] }, ] -const CAMERA_SHORTCUT_KEY_META: Record = { +const CAMERA_SHORTCUT_KEY_META: Record = { 'Left click': { icon: 'ph:mouse-left-click-fill', - label: 'Left click', + labelKey: 'editor.leftClick', }, 'Middle click': { icon: 'qlementine-icons:mouse-middle-button-16', - label: 'Middle click', + labelKey: 'editor.middleClick', }, 'Right click': { icon: 'ph:mouse-right-click-fill', - label: 'Right click', + labelKey: 'editor.rightClick', }, Scroll: { icon: 'qlementine-icons:mouse-middle-button-16', - label: 'Scroll wheel', + labelKey: 'editor.scrollWheel', }, Space: { icon: 'lucide:space', - label: 'Space', + labelKey: 'editor.space', }, } @@ -398,55 +399,55 @@ function writeCameraControlsHintDismissed(dismissed: boolean) { } catch {} } -function InlineShortcutKey({ shortcutKey }: { shortcutKey: ShortcutKey }) { +function InlineShortcutKey({ shortcutKey, t }: { shortcutKey: ShortcutKey; t: (key: string) => string }) { const meta = CAMERA_SHORTCUT_KEY_META[shortcutKey.value] if (meta?.icon) { return ( ) } return ( - {meta?.text ?? shortcutKey.value} + {shortcutKey.value} ) } -function ShortcutSequence({ keys }: { keys: ShortcutKey[] }) { +function ShortcutSequence({ keys, t }: { keys: ShortcutKey[]; t: (key: string) => string }) { return (
{keys.map((key, index) => (
{index > 0 ? + : null} - +
))}
) } -function CameraControlHintItem({ hint }: { hint: CameraControlHint }) { +function CameraControlHintItem({ hint, t }: { hint: CameraControlHint; t: (key: string) => string }) { return (
- {hint.action} + {t(hint.actionKey)}
- + {hint.alternativeKeys ? ( <> / - + ) : null}
@@ -457,9 +458,11 @@ function CameraControlHintItem({ hint }: { hint: CameraControlHint }) { function ViewerCanvasControlsHint({ isPreviewMode, onDismiss, + t, }: { isPreviewMode: boolean onDismiss: () => void + t: (key: string) => string }) { const hints = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS @@ -471,7 +474,7 @@ function ViewerCanvasControlsHint({ >
{hints.map((hint) => ( - + ))}
@@ -492,7 +495,7 @@ function ViewerCanvasControlsHint({ - Dismiss + {t('editor.dismiss')} @@ -810,6 +813,9 @@ const ViewerCanvas = memo(function ViewerCanvas({ showLoader: boolean onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void }) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + const viewMode = useEditor((s) => s.viewMode) const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio) const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio) @@ -905,6 +911,7 @@ const ViewerCanvas = memo(function ViewerCanvas({ ) : null} diff --git a/packages/editor/src/components/editor/node-action-menu.tsx b/packages/editor/src/components/editor/node-action-menu.tsx index 95b86dd17a..5a8309c2ae 100644 --- a/packages/editor/src/components/editor/node-action-menu.tsx +++ b/packages/editor/src/components/editor/node-action-menu.tsx @@ -3,6 +3,7 @@ import { Icon } from '@iconify/react' import { Copy, Move, Spline, Trash2 } from 'lucide-react' import type { MouseEventHandler, PointerEventHandler } from 'react' +import { messages, useLocale } from '../../lib/i18n' type NodeActionMenuProps = { onAddHole?: MouseEventHandler @@ -27,6 +28,9 @@ export function NodeActionMenu({ onPointerEnter, onPointerLeave, }: NodeActionMenuProps) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + return (
{onMove && (
@@ -415,7 +418,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) { active={mode === 'standard'} badge="16:9" icon={} - label="Standard" + label={t('snapshot.standard')} onClick={() => { setMode('standard') setDrag(null) @@ -424,7 +427,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) { } - label="Viewport" + label={t('snapshot.viewport')} onClick={() => { setMode('viewport') setDrag(null) @@ -433,7 +436,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) { } - label="Area" + label={t('snapshot.area')} onClick={() => setMode('area')} /> @@ -455,17 +458,17 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) { {captureState === 'capturing' ? ( <> - Capturing + {t('snapshot.capturing')} ) : captureState === 'saved' ? ( <> - Saved + {t('snapshot.saved')} ) : ( <> - Capture + {t('snapshot.capture')} )} diff --git a/packages/editor/src/components/systems/ceiling/ceiling-system.tsx b/packages/editor/src/components/systems/ceiling/ceiling-system.tsx index 71917ba44e..60c4525f8a 100644 --- a/packages/editor/src/components/systems/ceiling/ceiling-system.tsx +++ b/packages/editor/src/components/systems/ceiling/ceiling-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useEffect } from 'react' diff --git a/packages/editor/src/components/systems/roof/roof-edit-system.tsx b/packages/editor/src/components/systems/roof/roof-edit-system.tsx index e0055dd09c..59f15b0b67 100644 --- a/packages/editor/src/components/systems/roof/roof-edit-system.tsx +++ b/packages/editor/src/components/systems/roof/roof-edit-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type RoofNode, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef } from 'react' diff --git a/packages/editor/src/components/systems/stair/stair-edit-system.tsx b/packages/editor/src/components/systems/stair/stair-edit-system.tsx index 1831aaaa01..e6f0b4be5e 100644 --- a/packages/editor/src/components/systems/stair/stair-edit-system.tsx +++ b/packages/editor/src/components/systems/stair/stair-edit-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type StairNode, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useRef } from 'react' diff --git a/packages/editor/src/components/systems/zone/zone-system.tsx b/packages/editor/src/components/systems/zone/zone-system.tsx index e42ac38583..a360289cf6 100644 --- a/packages/editor/src/components/systems/zone/zone-system.tsx +++ b/packages/editor/src/components/systems/zone/zone-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useFrame } from '@react-three/fiber' diff --git a/packages/editor/src/components/tools/column/column-tool.tsx b/packages/editor/src/components/tools/column/column-tool.tsx index 837b1729b1..be99a02331 100644 --- a/packages/editor/src/components/tools/column/column-tool.tsx +++ b/packages/editor/src/components/tools/column/column-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import '../../../three-types' import { diff --git a/packages/editor/src/components/tools/elevator/elevator-tool.tsx b/packages/editor/src/components/tools/elevator/elevator-tool.tsx index 4b81bd917e..e29f673f74 100644 --- a/packages/editor/src/components/tools/elevator/elevator-tool.tsx +++ b/packages/editor/src/components/tools/elevator/elevator-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type BuildingNode, diff --git a/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx b/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx index 5a82db0ca0..c279ff2294 100644 --- a/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx +++ b/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type BuildingNode, diff --git a/packages/editor/src/components/tools/item/move-tool.tsx b/packages/editor/src/components/tools/item/move-tool.tsx index d7c86be966..940d86fdab 100644 --- a/packages/editor/src/components/tools/item/move-tool.tsx +++ b/packages/editor/src/components/tools/item/move-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import type { AnyNodeId, BuildingNode, diff --git a/packages/editor/src/components/tools/item/use-draft-node.ts b/packages/editor/src/components/tools/item/use-draft-node.ts index 67b72149e5..6de42234d3 100644 --- a/packages/editor/src/components/tools/item/use-draft-node.ts +++ b/packages/editor/src/components/tools/item/use-draft-node.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type AssetInput, diff --git a/packages/editor/src/components/tools/item/use-placement-coordinator.tsx b/packages/editor/src/components/tools/item/use-placement-coordinator.tsx index 8edc1207de..92fc0662b0 100644 --- a/packages/editor/src/components/tools/item/use-placement-coordinator.tsx +++ b/packages/editor/src/components/tools/item/use-placement-coordinator.tsx @@ -1,3 +1,5 @@ +'use client' + import type { AssetInput } from '@pascal-app/core' import { type AnyNodeId, diff --git a/packages/editor/src/components/tools/roof/move-roof-tool.tsx b/packages/editor/src/components/tools/roof/move-roof-tool.tsx index be05f382a4..4a4c894c51 100644 --- a/packages/editor/src/components/tools/roof/move-roof-tool.tsx +++ b/packages/editor/src/components/tools/roof/move-roof-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, diff --git a/packages/editor/src/components/tools/roof/roof-tool.tsx b/packages/editor/src/components/tools/roof/roof-tool.tsx index b5ec6930dd..7b527ecdc9 100644 --- a/packages/editor/src/components/tools/roof/roof-tool.tsx +++ b/packages/editor/src/components/tools/roof/roof-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, diff --git a/packages/editor/src/components/tools/select/box-select-tool.tsx b/packages/editor/src/components/tools/select/box-select-tool.tsx index 6275874d6d..76f36d0930 100644 --- a/packages/editor/src/components/tools/select/box-select-tool.tsx +++ b/packages/editor/src/components/tools/select/box-select-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import '../../../three-types' import { Icon } from '@iconify/react' diff --git a/packages/editor/src/components/tools/shared/affordance-dispatch.ts b/packages/editor/src/components/tools/shared/affordance-dispatch.ts index 0fc35c6a10..0cd8df6449 100644 --- a/packages/editor/src/components/tools/shared/affordance-dispatch.ts +++ b/packages/editor/src/components/tools/shared/affordance-dispatch.ts @@ -1,3 +1,5 @@ +'use client' + import { nodeRegistry } from '@pascal-app/core' import { type ComponentType, lazy } from 'react' diff --git a/packages/editor/src/components/tools/shared/cursor-sphere.tsx b/packages/editor/src/components/tools/shared/cursor-sphere.tsx index 10af0c084a..6f269573be 100644 --- a/packages/editor/src/components/tools/shared/cursor-sphere.tsx +++ b/packages/editor/src/components/tools/shared/cursor-sphere.tsx @@ -1,3 +1,5 @@ +'use client' + import { Html } from '@react-three/drei' import type { ThreeElements } from '@react-three/fiber' import { forwardRef } from 'react' @@ -5,6 +7,7 @@ import type { Group } from 'three' import { furnishTools } from '../../../components/ui/action-menu/furnish-tools' import { tools } from '../../../components/ui/action-menu/structure-tools' import { EDITOR_LAYER } from '../../../lib/constants' +import { messages, useLocale } from '../../../lib/i18n' import useEditor from '../../../store/use-editor' interface CursorSphereProps extends Omit { @@ -20,6 +23,8 @@ export const CursorSphere = forwardRef(function Cursor { color = '#818cf8', showTooltip = true, height = 2.5, visible = true, tooltipContent, ...props }, ref, ) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const tool = useEditor((s) => s.tool) const mode = useEditor((s) => s.mode) const catalogCategory = useEditor((s) => s.catalogCategory) @@ -102,7 +107,7 @@ export const CursorSphere = forwardRef(function Cursor {tooltipContent || ( // eslint-disable-next-line @next/next/no-img-element {activeToolConfig!.label} (messages[locale] as Record)[key] || key + const goToTopView = () => { emitter.emit('camera-controls:top-view') } @@ -24,13 +28,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) { {/* Orbit CCW */} Orbit Left Orbit Right Top View (messages[locale] as Record)[key] || key const mode = useEditor((state) => state.mode) const phase = useEditor((state) => state.phase) const selectionTool = useEditor((state) => state.floorplanSelectionTool) @@ -237,11 +240,11 @@ export function ControlModes() { label={ isSiteButton ? isActive - ? 'Exit site editing' + ? t('controlModes.exitSiteEditing') : canEnterSiteEdit - ? 'Edit site' - : 'Site editing (ground level only)' - : c.label + ? t('controlModes.editSite') + : t('controlModes.siteEditGroundOnly') + : t(c.labelKey) } onClick={() => handleClick(c.id)} shortcut={c.shortcut} @@ -250,7 +253,7 @@ export function ControlModes() { > {c.imageSrc ? ( {c.label} ) : c.iconifyIcon ? ( - ) : ( - ModeIcon && - )} + ) : ModeIcon ? ( + + ) : null} ) })} diff --git a/packages/editor/src/components/ui/action-menu/furnish-tools.tsx b/packages/editor/src/components/ui/action-menu/furnish-tools.tsx index da2c8dfb65..c406c5a7da 100644 --- a/packages/editor/src/components/ui/action-menu/furnish-tools.tsx +++ b/packages/editor/src/components/ui/action-menu/furnish-tools.tsx @@ -3,14 +3,14 @@ import type { CatalogCategory } from './../../../store/use-editor' export type FurnishToolConfig = { id: 'item' iconSrc: string - label: string + labelKey: string catalogCategory: CatalogCategory } export const furnishTools: FurnishToolConfig[] = [ - { id: 'item', iconSrc: '/icons/couch.png', label: 'Furniture', catalogCategory: 'furniture' }, - { id: 'item', iconSrc: '/icons/appliance.png', label: 'Appliance', catalogCategory: 'appliance' }, - { id: 'item', iconSrc: '/icons/kitchen.png', label: 'Kitchen', catalogCategory: 'kitchen' }, - { id: 'item', iconSrc: '/icons/bathroom.png', label: 'Bathroom', catalogCategory: 'bathroom' }, - { id: 'item', iconSrc: '/icons/tree.png', label: 'Outdoor', catalogCategory: 'outdoor' }, + { id: 'item', iconSrc: '/icons/couch.png', labelKey: 'furnishTools.furniture', catalogCategory: 'furniture' }, + { id: 'item', iconSrc: '/icons/appliance.png', labelKey: 'furnishTools.appliance', catalogCategory: 'appliance' }, + { id: 'item', iconSrc: '/icons/kitchen.png', labelKey: 'furnishTools.kitchen', catalogCategory: 'kitchen' }, + { id: 'item', iconSrc: '/icons/bathroom.png', labelKey: 'furnishTools.bathroom', catalogCategory: 'bathroom' }, + { id: 'item', iconSrc: '/icons/tree.png', labelKey: 'furnishTools.outdoor', catalogCategory: 'outdoor' }, ] diff --git a/packages/editor/src/components/ui/action-menu/structure-tools.tsx b/packages/editor/src/components/ui/action-menu/structure-tools.tsx index 081f4a96f4..631d7a6234 100644 --- a/packages/editor/src/components/ui/action-menu/structure-tools.tsx +++ b/packages/editor/src/components/ui/action-menu/structure-tools.tsx @@ -4,6 +4,7 @@ import NextImage from 'next/image' import { useContextualTools } from '../../../hooks/use-contextual-tools' import { cn } from '../../../lib/utils' +import { messages, useLocale } from '../../../lib/i18n' import useEditor, { type CatalogCategory, type StructureTool, @@ -14,35 +15,38 @@ import { ActionButton } from './action-button' export type ToolConfig = { id: StructureTool iconSrc: string - label: string + labelKey: string catalogCategory?: CatalogCategory } export const tools: ToolConfig[] = [ - { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' }, - { id: 'door', iconSrc: '/icons/door.png', label: 'Door' }, - { id: 'window', iconSrc: '/icons/window.png', label: 'Window' }, - { id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' }, - { id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' }, - { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' }, - { id: 'column', iconSrc: '/icons/column.png', label: 'Column' }, - { id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' }, - // { id: 'room', iconSrc: '/icons/room.png', label: 'Room' }, - // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' }, - { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' }, - { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' }, - { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' }, - { id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' }, - { id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' }, + { id: 'wall', iconSrc: '/icons/wall.png', labelKey: 'tools.wall' }, + { id: 'door', iconSrc: '/icons/door.png', labelKey: 'tools.door' }, + { id: 'window', iconSrc: '/icons/window.png', labelKey: 'tools.window' }, + { id: 'stair', iconSrc: '/icons/stairs.png', labelKey: 'tools.stairs' }, + { id: 'roof', iconSrc: '/icons/roof.png', labelKey: 'tools.gableRoof' }, + { id: 'fence', iconSrc: '/icons/fence.png', labelKey: 'tools.fence' }, + { id: 'column', iconSrc: '/icons/column.png', labelKey: 'tools.column' }, + { id: 'elevator', iconSrc: '/icons/elevator.png', labelKey: 'tools.elevator' }, + // { id: 'room', iconSrc: '/icons/room.png', labelKey: 'tools.room' }, + // { id: 'custom-room', iconSrc: '/icons/custom-room.png', labelKey: 'tools.customRoom' }, // Roof-mounted accessories (box-vent / ridge-vent / chimney / // solar-panel / skylight / dormer) are intentionally NOT in the top // palette — they only make sense in the context of a selected roof. // The roof inspector's "Add element" section is the entry point // (`packages/nodes/src/roof/panel.tsx`), which activates the same // registry-driven placement tools via `setTool(kind)`. + { id: 'slab', iconSrc: '/icons/floor.png', labelKey: 'tools.slab' }, + { id: 'ceiling', iconSrc: '/icons/ceiling.png', labelKey: 'tools.ceiling' }, + { id: 'zone', iconSrc: '/icons/zone.png', labelKey: 'tools.zone' }, + { id: 'spawn', iconSrc: '/icons/site.png', labelKey: 'tools.spawnPoint' }, + { id: 'shelf', iconSrc: '/icons/shelf.png', labelKey: 'tools.shelf' }, ] export function StructureTools() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + const activeTool = useEditor((state) => state.tool) const catalogCategory = useEditor((state) => state.catalogCategory) const structureLayer = useEditor((state) => state.structureLayer) @@ -81,7 +85,7 @@ export function StructureTools() { : 'scale-95 bg-transparent opacity-60 grayscale hover:bg-black/20 hover:opacity-100 hover:grayscale-0', )} key={`${tool.id}-${tool.catalogCategory ?? index}`} - label={tool.label} + label={t(tool.labelKey)} onClick={() => { if (!isActive) { setTool(tool.id) @@ -97,7 +101,7 @@ export function StructureTools() { variant="ghost" > void }) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const fileInputRef = useRef(null) const levelId = useViewer((s) => s.selection.levelId) const setSelection = useViewer((s) => s.setSelection) @@ -105,7 +108,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } onError(null) if (file.size > MAX_FILE_SIZE) { - onError('File is too large. Maximum size is 200 MB.') + onError(t('viewer.fileTooLarge')) return } @@ -113,7 +116,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } file.name.toLowerCase().endsWith('.glb') || file.name.toLowerCase().endsWith('.gltf') const isImage = file.type.startsWith('image/') if (!(isScan || isImage)) { - onError('Upload a .glb/.gltf scan or an image.') + onError(t('viewer.uploadGlbOrImage')) return } @@ -125,7 +128,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } setSelectedReferenceId(guide.id) setSelection({ selectedIds: [], zoneId: null }) } catch { - onError('Could not add that guide image.') + onError(t('viewer.couldNotAddGuideImage')) } finally { setIsAddingGuide(false) } @@ -134,26 +137,26 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } const { uploadHandler } = useUploadStore.getState() if (!uploadHandler) { - onError('Scan upload is unavailable.') + onError(t('viewer.scanUploadUnavailable')) return } const projectId = window.location.pathname.split('/editor/')[1]?.split('/')[0] if (!projectId) { - onError('Open a project before uploading a scan.') + onError(t('viewer.openProjectBeforeUpload')) return } useUploadStore.getState().clearUpload(levelId) uploadHandler(projectId, levelId, file, 'scan') }, - [createNode, levelId, onError, setSelectedReferenceId, setSelection, setShowGuides], + [createNode, levelId, onError, setSelectedReferenceId, setSelection, setShowGuides, t], ) return ( <>
handleOpacityChange(guide.id, value)} @@ -342,7 +347,7 @@ function GuidesControl() {
) : (
- No guide images on this level yet. + {t('viewer.noGuideImagesOnLevel')}
)}
@@ -354,6 +359,8 @@ function GuidesControl() { // ── Grid snap toggle ──────────────────────────────────────────────────────── function GridSnapControl() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const [isOpen, setIsOpen] = useState(false) const gridSnapStep = useEditor((state) => state.gridSnapStep) const setGridSnapStep = useEditor((state) => state.setGridSnapStep) @@ -365,7 +372,7 @@ function GridSnapControl() {
handleOpacityChange(scan.id, value)} @@ -581,7 +590,7 @@ function ScansControl() { ) : (
- No scans on this level yet. + {t('viewer.noScansOnLevel')}
)} @@ -593,6 +602,8 @@ function ScansControl() { // ── Reference floor control ──────────────────────────────────────────────────────────────────── function ReferenceFloorControl() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const showReferenceFloor = useEditor((state) => state.showReferenceFloor) const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor) const referenceFloorOffset = useEditor((state) => state.referenceFloorOffset) @@ -638,7 +649,7 @@ function ReferenceFloorControl() { )} @@ -337,7 +340,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm {(!emptyAction || page) && ( - No commands found. + {t('commands.noCommandsFound')} )} {emptyAction && !page && } @@ -445,7 +448,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm Rename to "{inputValue.trim()}" ) : ( - Type a new name above… + {t('commands.typeNewNameAbove')} )} @@ -456,18 +459,18 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm {/* Footer hint */}
- navigate + {t('commands.navigate')} - select + {t('commands.select')} {page ? ( - back + {t('commands.back')} ) : ( - close + {t('commands.close')} )}
diff --git a/packages/editor/src/components/ui/floating-level-selector.tsx b/packages/editor/src/components/ui/floating-level-selector.tsx index 1faa7b0ddf..3e8d5c535f 100644 --- a/packages/editor/src/components/ui/floating-level-selector.tsx +++ b/packages/editor/src/components/ui/floating-level-selector.tsx @@ -41,6 +41,7 @@ import { buildLevelDuplicateCreateOps, type LevelDuplicatePreset, } from '../../lib/level-duplication' +import { useLocale, messages } from '../../lib/i18n' import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name' import { deleteLevelWithFallbackSelection } from '../../lib/level-selection' import { @@ -72,8 +73,9 @@ function LevelInlineRename({ isEditing: boolean onStopEditing: () => void }) { + const { locale } = useLocale() const updateNode = useScene((s) => s.updateNode) - const defaultName = getDefaultLevelName(level.level) + const defaultName = getDefaultLevelName(level.level, locale) const [value, setValue] = useState(level.name || '') const inputRef = useRef(null) @@ -143,6 +145,8 @@ function LevelRow({ onPaste?: () => void onRequestDelete: () => void }) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false) const [isEditing, setIsEditing] = useState(false) @@ -176,7 +180,7 @@ function LevelRow({ dragHandleProps?.onClick?.(e) }} ref={dragHandleRef} - title="Drag to reorder" + title={t('level.dragToReorder')} type="button" > @@ -189,7 +193,7 @@ function LevelRow({ e.stopPropagation() setIsEditing(true) }} - title={getLevelDisplayName(level)} + title={getLevelDisplayName(level, locale)} type="button" > {getLevelDisplayName(level)} @@ -227,7 +231,7 @@ function LevelRow({ type="button" > - Duplicate with options... + {t('level.duplicateOptions')} {onPaste && ( @@ -323,6 +327,14 @@ function SortableLevelRow({ // ── Main component ────────────────────────────────────────────────────────── export function FloatingLevelSelector() { + const { locale } = useLocale() + const t = (key: string, params?: Record) => { + const str = (messages[locale as 'en' | 'zh'] as Record)[key] || key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), str, + ) + } const selectedBuildingId = useViewer((s) => s.selection.buildingId) const levelId = useViewer((s) => s.selection.levelId) const setSelection = useViewer((s) => s.setSelection) @@ -517,7 +529,7 @@ export function FloatingLevelSelector() { diff --git a/packages/editor/src/components/ui/helpers/item-helper.tsx b/packages/editor/src/components/ui/helpers/item-helper.tsx index 7bf9009880..f021f9a207 100644 --- a/packages/editor/src/components/ui/helpers/item-helper.tsx +++ b/packages/editor/src/components/ui/helpers/item-helper.tsx @@ -1,3 +1,4 @@ +import { useIntl } from 'react-intl' import { ShortcutToken } from '../primitives/shortcut-token' interface ItemHelperProps { @@ -5,34 +6,35 @@ interface ItemHelperProps { } export function ItemHelper({ showEsc }: ItemHelperProps) { + const { formatMessage } = useIntl() return (
- Place item + {formatMessage({ id: 'itemHelper.placeItem' })}
- Rotate counterclockwise + {formatMessage({ id: 'itemHelper.rotateCounterclockwise' })}
- Rotate clockwise + {formatMessage({ id: 'itemHelper.rotateClockwise' })}
- Free place + {formatMessage({ id: 'itemHelper.freePlace' })}
{showEsc && (
- Cancel + {formatMessage({ id: 'common.cancel' })}
)} {!showEsc && (
- Cancel + {formatMessage({ id: 'common.cancel' })}
)}
diff --git a/packages/editor/src/components/ui/level-duplicate-dialog.tsx b/packages/editor/src/components/ui/level-duplicate-dialog.tsx index 5b2b9f4749..d9702d15cf 100644 --- a/packages/editor/src/components/ui/level-duplicate-dialog.tsx +++ b/packages/editor/src/components/ui/level-duplicate-dialog.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react' import type { LevelDuplicatePreset } from '../../lib/level-duplication' import { getLevelDisplayName } from '../../lib/level-name' import { cn } from '../../lib/utils' +import { messages, useLocale } from '../../lib/i18n' import { Dialog, DialogContent, @@ -16,34 +17,34 @@ import { const DUPLICATE_PRESETS: Array<{ id: LevelDuplicatePreset - label: string - description: string + labelKey: string + descKey: string }> = [ { id: 'everything', - label: 'Everything', - description: 'Structure, materials, furniture, and references.', + labelKey: 'levelDuplicate.everything', + descKey: 'levelDuplicate.everythingDesc', }, { id: 'structure', - label: 'Structure only', - description: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.', + labelKey: 'levelDuplicate.structure', + descKey: 'levelDuplicate.structureDesc', }, { id: 'structure-materials', - label: 'Structure + materials', - description: 'Structure with the current material and finish assignments.', + labelKey: 'levelDuplicate.structureMaterials', + descKey: 'levelDuplicate.structureMaterialsDesc', }, { id: 'structure-furniture', - label: 'Structure + furniture', - description: 'Structure, finishes, and placed items, without guide references.', + labelKey: 'levelDuplicate.structureFurniture', + descKey: 'levelDuplicate.structureFurnitureDesc', }, ] -function getLevelLabel(level: LevelNode | null) { +function getLevelLabel(level: LevelNode | null, locale: string) { if (!level) return 'this level' - return getLevelDisplayName(level) + return getLevelDisplayName(level, locale as 'en' | 'zh') } export function LevelDuplicateDialog({ @@ -58,6 +59,14 @@ export function LevelDuplicateDialog({ onOpenChange: (open: boolean) => void }) { const [preset, setPreset] = useState('everything') + const { locale } = useLocale() + const t = (key: string, params?: Record) => { + const str = (messages[locale as 'en' | 'zh'] as Record)[key] || key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}}`, 'g'), String(v)), str, + ) + } useEffect(() => { if (open) { @@ -69,8 +78,8 @@ export function LevelDuplicateDialog({ - Duplicate Level - Choose what to copy from {getLevelLabel(level)}. + {t('levelDuplicate.duplicateLevel')} + {t('levelDuplicate.chooseWhatToCopy', { level: getLevelLabel(level, locale) })}
@@ -86,8 +95,8 @@ export function LevelDuplicateDialog({ onClick={() => setPreset(option.id)} type="button" > -
{option.label}
-
{option.description}
+
{t(option.labelKey)}
+
{t(option.descKey)}
))}
@@ -98,14 +107,14 @@ export function LevelDuplicateDialog({ onClick={() => onOpenChange(false)} type="button" > - Cancel + {t('levelDuplicate.cancel')}
diff --git a/packages/editor/src/components/ui/panels/mobile-selection-bar.tsx b/packages/editor/src/components/ui/panels/mobile-selection-bar.tsx index a9d49ffd50..6d6e52370f 100644 --- a/packages/editor/src/components/ui/panels/mobile-selection-bar.tsx +++ b/packages/editor/src/components/ui/panels/mobile-selection-bar.tsx @@ -5,6 +5,7 @@ import { Copy, Move, SlidersHorizontal, Trash2 } from 'lucide-react' import Image from 'next/image' import type { MouseEventHandler } from 'react' import { cn } from '../../../lib/utils' +import { messages, useLocale } from '../../../lib/i18n' import { getNodeDisplay } from './node-display' interface MobileSelectionBarProps { @@ -25,7 +26,9 @@ export function MobileSelectionBar({ onDelete, onEdit, }: MobileSelectionBarProps) { - const { icon, label } = getNodeDisplay(node) + const { locale } = useLocale() + const { icon, labelKey } = getNodeDisplay(node) + const label = labelKey.includes('.') ? (messages[locale] as Record)[labelKey] || labelKey : labelKey const stop: MouseEventHandler = (e) => e.stopPropagation() diff --git a/packages/editor/src/components/ui/panels/node-display.ts b/packages/editor/src/components/ui/panels/node-display.ts index be0fd1c882..dcc2217419 100644 --- a/packages/editor/src/components/ui/panels/node-display.ts +++ b/packages/editor/src/components/ui/panels/node-display.ts @@ -1,40 +1,59 @@ import type { AnyNode } from '@pascal-app/core' +import { messages } from '../../../lib/i18n' export type NodeDisplay = { icon: string - label: string + /** i18n key or custom name */ + labelKey: string } const TYPE_DEFAULTS: Record = { - item: { icon: '/icons/furniture.png', label: 'Item' }, - wall: { icon: '/icons/wall.png', label: 'Wall' }, - door: { icon: '/icons/door.png', label: 'Door' }, - window: { icon: '/icons/window.png', label: 'Window' }, - slab: { icon: '/icons/floor.png', label: 'Slab' }, - ceiling: { icon: '/icons/ceiling.png', label: 'Ceiling' }, - column: { icon: '/icons/column.png', label: 'Column' }, - elevator: { icon: '/icons/elevator.png', label: 'Elevator' }, - fence: { icon: '/icons/fence.png', label: 'Fence' }, - roof: { icon: '/icons/roof.png', label: 'Roof' }, - 'roof-segment': { icon: '/icons/roof.png', label: 'Roof segment' }, - stair: { icon: '/icons/stair.png', label: 'Stair' }, - 'stair-segment': { icon: '/icons/stair.png', label: 'Stair segment' }, - scan: { icon: '/icons/mesh.png', label: '3D Scan' }, - guide: { icon: '/icons/floorplan.png', label: 'Guide image' }, + item: { icon: '/icons/furniture.png', labelKey: 'nodeTypes.item' }, + wall: { icon: '/icons/wall.png', labelKey: 'nodeTypes.wall' }, + door: { icon: '/icons/door.png', labelKey: 'nodeTypes.door' }, + window: { icon: '/icons/window.png', labelKey: 'nodeTypes.window' }, + slab: { icon: '/icons/floor.png', labelKey: 'nodeTypes.slab' }, + ceiling: { icon: '/icons/ceiling.png', labelKey: 'nodeTypes.ceiling' }, + column: { icon: '/icons/column.png', labelKey: 'nodeTypes.column' }, + elevator: { icon: '/icons/elevator.png', labelKey: 'nodeTypes.elevator' }, + fence: { icon: '/icons/fence.png', labelKey: 'nodeTypes.fence' }, + roof: { icon: '/icons/roof.png', labelKey: 'nodeTypes.roof' }, + 'roof-segment': { icon: '/icons/roof.png', labelKey: 'nodeTypes.roofSegment' }, + stair: { icon: '/icons/stair.png', labelKey: 'nodeTypes.stairs' }, + 'stair-segment': { icon: '/icons/stair.png', labelKey: 'nodeTypes.stairSegment' }, + scan: { icon: '/icons/mesh.png', labelKey: 'nodeTypes.scan' }, + guide: { icon: '/icons/floorplan.png', labelKey: 'nodeTypes.guide' }, + shelf: { icon: '/icons/shelf.png', labelKey: 'nodeTypes.shelf' }, + spawn: { icon: '/icons/site.png', labelKey: 'nodeTypes.spawn' }, + boxVent: { icon: '/icons/box-vent.png', labelKey: 'nodeTypes.boxVent' }, + chimney: { icon: '/icons/chimney.png', labelKey: 'nodeTypes.chimney' }, + dormer: { icon: '/icons/dormer.png', labelKey: 'nodeTypes.dormer' }, + ridgeVent: { icon: '/icons/ridge-vent.png', labelKey: 'nodeTypes.ridgeVent' }, + solarPanel: { icon: '/icons/solar-panel.png', labelKey: 'nodeTypes.solarPanel' }, } +/** + * Returns display info for a node. + * labelKey is either an i18n key (nodeTypes.XXX) or a custom name set by the user. + */ export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay { - if (!node) return { icon: '/icons/select.png', label: 'Selection' } - const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.png', label: node.type } + if (!node) return { icon: '/icons/select.png', labelKey: 'Selection' } + const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.png', labelKey: node.type } // Item nodes carry an asset with its own thumbnail/name if (node.type === 'item') { + const name = node.name || node.asset?.name return { icon: node.asset?.thumbnail || fallback.icon, - label: node.name || node.asset?.name || fallback.label, + labelKey: name || fallback.labelKey, } } return { icon: fallback.icon, - label: node.name || fallback.label, + labelKey: node.name || fallback.labelKey, } } + +/** Resolve a node type label key to a translated string */ +export function resolveNodeLabel(labelKey: string, locale: string): string { + return (messages[locale as 'en' | 'zh'] as Record)[labelKey] || labelKey +} diff --git a/packages/editor/src/components/ui/panels/panel-manager.tsx b/packages/editor/src/components/ui/panels/panel-manager.tsx index 1d58f585ba..9bee1baa9c 100644 --- a/packages/editor/src/components/ui/panels/panel-manager.tsx +++ b/packages/editor/src/components/ui/panels/panel-manager.tsx @@ -26,6 +26,7 @@ import { useCallback, useEffect, useState } from 'react' import { useIsMobile } from '../../../hooks/use-mobile' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' +import { messages, useLocale } from '../../../lib/i18n' import { MobilePanelSheet } from './mobile-panel-sheet' import { MobileSelectionBar } from './mobile-selection-bar' import { getNodeDisplay } from './node-display' @@ -100,6 +101,7 @@ function MobilePanelLayer({ const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId) const setMovingNode = useEditor((s) => s.setMovingNode) const deleteNode = useScene((s) => s.deleteNode) + const { locale } = useLocale() const [isSheetOpen, setIsSheetOpen] = useState(false) // Reset sheet open state when the selection changes / clears @@ -144,6 +146,9 @@ function MobilePanelLayer({ if (!(node || isReference)) return null const display = getNodeDisplay(node) + const resolvedTitle = display.labelKey.includes('.') + ? (messages[locale] as Record)[display.labelKey] || display.labelKey + : display.labelKey return ( <> @@ -160,7 +165,7 @@ function MobilePanelLayer({ icon={display.icon} onClose={() => setIsSheetOpen(false)} open={isSheetOpen} - title={display.label} + title={resolvedTitle} > {panel} diff --git a/packages/editor/src/components/ui/primitives/button.tsx b/packages/editor/src/components/ui/primitives/button.tsx index 2b3d05648b..fc201742a9 100644 --- a/packages/editor/src/components/ui/primitives/button.tsx +++ b/packages/editor/src/components/ui/primitives/button.tsx @@ -1,3 +1,5 @@ +'use client' + import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import type * as React from 'react' diff --git a/packages/editor/src/components/ui/primitives/card.tsx b/packages/editor/src/components/ui/primitives/card.tsx index d19c15ecaf..879a0fa9bd 100644 --- a/packages/editor/src/components/ui/primitives/card.tsx +++ b/packages/editor/src/components/ui/primitives/card.tsx @@ -1,3 +1,5 @@ +'use client' + import type * as React from 'react' import { cn } from '../../../lib/utils' diff --git a/packages/editor/src/components/ui/primitives/input.tsx b/packages/editor/src/components/ui/primitives/input.tsx index e1be92daf4..00e07f8790 100644 --- a/packages/editor/src/components/ui/primitives/input.tsx +++ b/packages/editor/src/components/ui/primitives/input.tsx @@ -1,3 +1,5 @@ +'use client' + import type * as React from 'react' import { cn } from '../../../lib/utils' diff --git a/packages/editor/src/components/ui/primitives/shortcut-token.tsx b/packages/editor/src/components/ui/primitives/shortcut-token.tsx index 9878daabe3..51b71ea69a 100644 --- a/packages/editor/src/components/ui/primitives/shortcut-token.tsx +++ b/packages/editor/src/components/ui/primitives/shortcut-token.tsx @@ -1,3 +1,5 @@ +'use client' + import { Icon } from '@iconify/react' import type * as React from 'react' diff --git a/packages/editor/src/components/ui/primitives/skeleton.tsx b/packages/editor/src/components/ui/primitives/skeleton.tsx index bc05858f3b..7de2005609 100644 --- a/packages/editor/src/components/ui/primitives/skeleton.tsx +++ b/packages/editor/src/components/ui/primitives/skeleton.tsx @@ -1,6 +1,6 @@ import { cn } from '../../../lib/utils' -function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { +function Skeleton({ className, ...props }: React.HTMLAttributes) { return (
(messages[locale] as Record)[key] || key return (
{panel.label} - {panel.label} + {t(panel.labelKey)} ) })} @@ -127,7 +130,7 @@ export function IconRail({ type="button" > {panel.label} - {panel.label} + {t(panel.labelKey)} ) })} diff --git a/packages/editor/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx b/packages/editor/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx index 851170b894..d9b388b1bf 100644 --- a/packages/editor/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx @@ -11,6 +11,7 @@ import { TooltipProvider, TooltipTrigger, } from '../../../../../components/ui/primitives/tooltip' +import { messages, useLocale } from '../../../../../lib/i18n' /** A function-axis taxonomy node, assembled into a tree by the embedder. */ export type FunctionTreeNode = { @@ -20,10 +21,10 @@ export type FunctionTreeNode = { children: FunctionTreeNode[] } -const SOURCE_CHIPS: Array<{ id: NonNullable; label: string }> = [ - { id: 'library', label: 'Library' }, - { id: 'community', label: 'Community' }, - { id: 'mine', label: 'Mine' }, +const SOURCE_CHIPS: Array<{ id: NonNullable; labelKey: string }> = [ + { id: 'library', labelKey: 'items.library' }, + { id: 'community', labelKey: 'items.community' }, + { id: 'mine', labelKey: 'items.mine' }, ] /** Every slug at or below `node`, so a non-leaf selection matches descendants. */ @@ -69,6 +70,14 @@ export function FunctionTreePanel({ const [activeChildSlug, setActiveChildSlug] = useState(null) const [activeSource, setActiveSource] = useState('library') const [search, setSearch] = useState('') + const { locale } = useLocale() + const t = (key: string, params?: Record) => { + const str = (messages[locale] as Record)[key] || key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), str, + ) + } const isServerSearch = onSearchChange !== undefined const isSearchPending = isServerSearch && search.length > 0 && searchResults === null @@ -168,7 +177,7 @@ export function FunctionTreePanel({ setSearch(e.target.value) onSearchChange?.(e.target.value) }} - placeholder="Search..." + placeholder={t('items.search')} type="text" value={search} /> @@ -187,7 +196,7 @@ export function FunctionTreePanel({ onClick={() => setActiveSource(isActive ? null : chip.id)} type="button" > - {chip.label} + {t(chip.labelKey)} ) })} @@ -207,7 +216,7 @@ export function FunctionTreePanel({ onClick={() => setActiveChildSlug(null)} type="button" > - All + {t('items.all')} {activeRoot.children.map((child) => { const isActive = activeChildSlug === child.slug @@ -240,7 +249,7 @@ export function FunctionTreePanel({ ) : isServerSearch && search && searchResults?.length === 0 ? ( (emptyState ?? (
- No results for “{search}” + {t('items.noResults', { search })}
)) ) : ( diff --git a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx index 73d58092c7..0fd68598a2 100644 --- a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx @@ -7,6 +7,7 @@ import { cn } from '../../../../../lib/utils' import type { CatalogCategory } from '../../../../../store/use-editor' import useEditor from '../../../../../store/use-editor' import { furnishTools } from '../../../action-menu/furnish-tools' +import { messages, useLocale } from '../../../../../lib/i18n' import { CATALOG_ITEMS } from '../../../item-catalog/catalog-items' import { ItemCatalog } from '../../../item-catalog/item-catalog' import { type FunctionTreeNode, FunctionTreePanel } from './function-tree-panel' @@ -79,6 +80,14 @@ function LegacyItemsPanel({ leadingTile?: React.ReactNode emptyState?: React.ReactNode }) { + const { locale } = useLocale() + const t = (key: string, params?: Record) => { + const str = (messages[locale] as Record)[key] || key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), str, + ) + } const mode = useEditor((s) => s.mode) const catalogCategory = useEditor((s) => s.catalogCategory) const setMode = useEditor((s) => s.setMode) @@ -144,10 +153,10 @@ function LegacyItemsPanel({ // The three source chips are always shown so users can discover the // filter even before they own any items. Selecting "Mine" with no // matching items falls through to the empty/no-results state. - const sourceChips: Array<{ id: AssetInput['source']; label: string }> = [ - { id: 'library', label: 'Library' }, - { id: 'community', label: 'Community' }, - { id: 'mine', label: 'Mine' }, + const sourceChips: Array<{ id: AssetInput['source']; labelKey: string }> = [ + { id: 'library', labelKey: 'items.library' }, + { id: 'community', labelKey: 'items.community' }, + { id: 'mine', labelKey: 'items.mine' }, ] const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? []))) const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t)) @@ -189,13 +198,13 @@ function LegacyItemsPanel({ type="button" > - {cat.label} + {t(cat.labelKey)} ) })} @@ -212,7 +221,7 @@ function LegacyItemsPanel({ setSearch(e.target.value) onSearchChange?.(e.target.value) }} - placeholder="Search..." + placeholder={t('items.search')} type="text" value={search} /> @@ -232,7 +241,7 @@ function LegacyItemsPanel({ onClick={() => setActiveSource(isActive ? null : chip.id)} type="button" > - {chip.label} + {t(chip.labelKey)} ) })} @@ -254,7 +263,7 @@ function LegacyItemsPanel({ onClick={() => setActivePlacementTag(null)} type="button" > - All + {t('items.all')} {placementTags.map((tag) => { const count = placementCount(tag) @@ -346,7 +355,7 @@ function LegacyItemsPanel({ ) : isServerSearch && search && searchResults?.length === 0 ? ( (emptyState ?? (
- No results for “{search}” + {t('items.noResults', { search })}
)) ) : ( diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx index dbe1446cea..6438b99d5a 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx @@ -9,9 +9,12 @@ import { DialogTrigger, } from '../../../../../components/ui/primitives/dialog' import { Slider } from '../../../../../components/ui/slider' +import { messages, useLocale } from '../../../../../lib/i18n' import useAudio from '../../../../../store/use-audio' export function AudioSettingsDialog() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key const { masterVolume, sfxVolume, @@ -28,19 +31,19 @@ export function AudioSettingsDialog() { - Audio Settings - Adjust volume levels and mute settings + {t('settings.audioSettings')} + {t('settings.adjustVolume')}
{/* Master Volume */}
- + {masterVolume}%
- + {radioVolume}%
- + {sfxVolume}%
{muted ? : } - {muted ? 'Unmute All Sounds' : 'Mute All Sounds'} + {muted ? t('settings.unmuteAllSounds') : t('settings.muteAllSounds')}
diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx index c1c7e6fa56..ebf6bd23ee 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { emitter, useScene, validateBuildJson } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { TreeView, VisualJson } from '@visual-json/react' @@ -10,15 +12,16 @@ import { useRef, useState, } from 'react' -import { Button } from './../../../../../components/ui/primitives/button' +import { Button } from '../../../../../components/ui/primitives/button' import { Dialog, DialogContent, DialogTitle, DialogTrigger, -} from './../../../../../components/ui/primitives/dialog' -import { Switch } from './../../../../../components/ui/primitives/switch' -import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor' +} from '../../../../../components/ui/primitives/dialog' +import { Switch } from '../../../../../components/ui/primitives/switch' +import { messages, useLocale } from '../../../../../lib/i18n' +import useEditor, { selectDefaultBuildingAndLevel } from '../../../../../store/use-editor' import { AudioSettingsDialog } from './audio-settings-dialog' import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog' import { LoadBuildDialog, type PendingImport } from './load-build-dialog' @@ -176,6 +179,8 @@ export function SettingsPanel({ projectVisibility, onVisibilityChange, }: SettingsPanelProps = {}) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale as 'en' | 'zh'] as Record)[key] || key const fileInputRef = useRef(null) const nodes = useScene((state) => state.nodes) const rootNodeIds = useScene((state) => state.rootNodeIds) @@ -298,12 +303,12 @@ export function SettingsPanel({ {/* Visibility Section (only for cloud projects) */} {projectId && !isLocalProject && (
- +
-
Public
+
{t('settings.visibilityPublic')}
- {projectVisibility?.isPrivate ? 'Only you' : 'Anyone'} can view + {projectVisibility?.isPrivate ? t('settings.visibilityOnlyYou') : t('settings.visibilityAnyone')} {t('settings.visibilityCanView')}
-
Show 3D Scans
-
Visible to public viewers
+
{t('settings.visibilityShow3DScans')}
+
{t('settings.visibilityVisiblePublic')}
-
Show Floorplans
-
Visible to public viewers
+
{t('settings.visibilityShowFloorplans')}
+
{t('settings.visibilityVisiblePublic')}
-
Show Grid
-
Visible only in the editor
+
{t('settings.visibilityShowGrid')}
+
{t('settings.visibilityEditorOnly')}
-
Shadows
-
Cast shadows from lights
+
{t('settings.shadows')}
+
{t('settings.visibilityCastShadows')}
- +
{/* Thumbnail Section (only for cloud projects) */} {projectId && !isLocalProject && (
- +
)} {/* Save/Load Section */}
- + - +
{/* Keyboard Section */}
- +
{/* Scene Graph */}
- + - Scene Graph + {t('settings.sceneGraph')}
- +
diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx index 6fb4f2e000..3a037b180d 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx @@ -1,6 +1,8 @@ +'use client' + import { Keyboard } from 'lucide-react' import { useEffect, useState } from 'react' -import { Button } from './../../../../../components/ui/primitives/button' +import { Button } from '../../../../../components/ui/primitives/button' import { Dialog, DialogContent, @@ -8,17 +10,18 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from './../../../../../components/ui/primitives/dialog' -import { ShortcutToken } from './../../../../../components/ui/primitives/shortcut-token' +} from '../../../../../components/ui/primitives/dialog' +import { messages, useLocale } from '../../../../../lib/i18n' +import { ShortcutToken } from '../../../../../components/ui/primitives/shortcut-token' type Shortcut = { keys: string[] - action: string - note?: string + actionKey: string + noteKey?: string } type ShortcutCategory = { - title: string + titleKey: string shortcuts: Shortcut[] } @@ -32,83 +35,83 @@ const KEY_DISPLAY_MAP: Record = { const SHORTCUT_CATEGORIES: ShortcutCategory[] = [ { - title: 'Editor Navigation', + titleKey: 'shortcuts.editorNavigation', shortcuts: [ - { keys: ['1'], action: 'Switch to Site phase' }, - { keys: ['2'], action: 'Switch to Structure phase' }, - { keys: ['3'], action: 'Switch to Furnish phase' }, - { keys: ['S'], action: 'Switch to Structure layer' }, - { keys: ['F'], action: 'Switch to Furnish layer' }, - { keys: ['Z'], action: 'Switch to Zones layer' }, + { keys: ['1'], actionKey: 'shortcuts.switchToSitePhase' }, + { keys: ['2'], actionKey: 'shortcuts.switchToStructurePhase' }, + { keys: ['3'], actionKey: 'shortcuts.switchToFurnishPhase' }, + { keys: ['S'], actionKey: 'shortcuts.switchToStructureLayer' }, + { keys: ['F'], actionKey: 'shortcuts.switchToFurnishLayer' }, + { keys: ['Z'], actionKey: 'shortcuts.switchToZonesLayer' }, { keys: ['Cmd/Ctrl', 'Arrow Up'], - action: 'Select next level in the active building', + actionKey: 'shortcuts.selectNextLevel', }, { keys: ['Cmd/Ctrl', 'Arrow Down'], - action: 'Select previous level in the active building', + actionKey: 'shortcuts.selectPreviousLevel', }, - { keys: ['Cmd/Ctrl', 'B'], action: 'Toggle sidebar' }, + { keys: ['Cmd/Ctrl', 'B'], actionKey: 'shortcuts.toggleSidebar' }, ], }, { - title: 'Modes & History', + titleKey: 'shortcuts.modesAndHistory', shortcuts: [ - { keys: ['V'], action: 'Switch to Select mode' }, - { keys: ['B'], action: 'Switch to Build mode' }, + { keys: ['V'], actionKey: 'shortcuts.switchToSelectMode' }, + { keys: ['B'], actionKey: 'shortcuts.switchToBuildMode' }, { keys: ['Esc'], - action: 'Cancel the active tool and return to Select mode', + actionKey: 'shortcuts.cancelTool', }, - { keys: ['Delete / Backspace'], action: 'Delete selected objects' }, - { keys: ['Cmd/Ctrl', 'Z'], action: 'Undo' }, - { keys: ['Cmd/Ctrl', 'Shift', 'Z'], action: 'Redo' }, + { keys: ['Delete / Backspace'], actionKey: 'shortcuts.deleteOrBackspace' }, + { keys: ['Cmd/Ctrl', 'Z'], actionKey: 'shortcuts.undo' }, + { keys: ['Cmd/Ctrl', 'Shift', 'Z'], actionKey: 'shortcuts.redo' }, ], }, { - title: 'Selection', + titleKey: 'shortcuts.selection', shortcuts: [ { keys: ['Cmd/Ctrl', 'Left click'], - action: 'Add or remove an object from multi-selection', - note: 'Works while in Select mode.', + actionKey: 'shortcuts.addOrRemoveFromSelection', + noteKey: 'shortcuts.worksWhileInSelectMode', }, ], }, { - title: 'Drawing Tools', + titleKey: 'shortcuts.drawingTools', shortcuts: [ { keys: ['Shift'], - action: 'Temporarily disable angle snapping while drawing walls, slabs, and ceilings', - note: 'Hold while drawing.', + actionKey: 'shortcuts.disableAngleSnapping', + noteKey: 'shortcuts.holdWhileDrawing', }, ], }, { - title: 'Item Placement', + titleKey: 'shortcuts.itemPlacement', shortcuts: [ - { keys: ['R'], action: 'Rotate item clockwise, or toggle selected door open/closed' }, - { keys: ['T'], action: 'Rotate item counter-clockwise, or close selected door' }, + { keys: ['R'], actionKey: 'shortcuts.rotateItemClockwise' }, + { keys: ['T'], actionKey: 'shortcuts.rotateItemCounterClockwise' }, { keys: ['Shift'], - action: 'Temporarily bypass placement validation constraints', - note: 'Hold while placing.', + actionKey: 'shortcuts.bypassPlacementValidation', + noteKey: 'shortcuts.holdWhilePlacing', }, ], }, { - title: 'Camera', + titleKey: 'shortcuts.camera', shortcuts: [ { keys: ['Middle click'], - action: 'Pan camera', - note: 'Drag with the middle mouse button, or hold Space while dragging with the left mouse button.', + actionKey: 'shortcuts.panCamera', + noteKey: 'shortcuts.dragMiddleMouseOrHoldSpace', }, { keys: ['Right click'], - action: 'Orbit camera', - note: 'Drag with the right mouse button.', + actionKey: 'shortcuts.orbitCamera', + noteKey: 'shortcuts.dragRightMouse', }, ], }, @@ -140,37 +143,39 @@ function ShortcutKeys({ keys }: { keys: string[] }) { } export function KeyboardShortcutsDialog() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale as 'en' | 'zh'] as Record)[key] || key return ( - Keyboard Shortcuts + {t('shortcuts.keyboardShortcuts')} - Shortcuts are context-aware and depend on the current phase or tool. + {t('shortcuts.contextAware')}
{SHORTCUT_CATEGORIES.map((category) => ( -
-

{category.title}

+
+

{t(category.titleKey)}

{category.shortcuts.map((shortcut, index) => (
-

{shortcut.action}

- {shortcut.note ? ( -

{shortcut.note}

+

{t(shortcut.actionKey)}

+ {shortcut.noteKey ? ( +

{t(shortcut.noteKey)}

) : null}
{index < category.shortcuts.length - 1 ? ( diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx index 28424179e3..c04f3dabef 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx @@ -1,3 +1,5 @@ +'use client' + import type { BuildStats, SchemaIssue, ValidateBuildJsonResult } from '@pascal-app/core' import { AlertTriangle, @@ -22,6 +24,7 @@ import { DialogHeader, DialogTitle, } from '../../../../../components/ui/primitives/dialog' +import { messages, useLocale } from '../../../../../lib/i18n' export type PendingImport = { fileName: string @@ -37,24 +40,24 @@ type Props = { type StatRow = { icon: typeof Building2 - label: string + labelKey: string count: number } -function statsRows(stats: BuildStats): StatRow[] { +function statsRows(stats: BuildStats, t: (key: string) => string): StatRow[] { return ( [ - { icon: MapPin, label: 'Sites', count: stats.byType.site ?? 0 }, - { icon: Building2, label: 'Buildings', count: stats.byType.building ?? 0 }, - { icon: Layers, label: 'Levels', count: stats.byType.level ?? 0 }, - { icon: Square, label: 'Walls', count: stats.byType.wall ?? 0 }, - { icon: DoorOpen, label: 'Doors', count: stats.byType.door ?? 0 }, - { icon: AppWindow, label: 'Windows', count: stats.byType.window ?? 0 }, - { icon: Box, label: 'Items', count: stats.byType.item ?? 0 }, - { icon: Square, label: 'Slabs', count: stats.byType.slab ?? 0 }, - { icon: Square, label: 'Ceilings', count: stats.byType.ceiling ?? 0 }, - { icon: Square, label: 'Zones', count: stats.byType.zone ?? 0 }, - { icon: Scan, label: 'Scans', count: stats.byType.scan ?? 0 }, + { icon: MapPin, labelKey: 'loadBuild.sites', count: stats.byType.site ?? 0 }, + { icon: Building2, labelKey: 'loadBuild.buildings', count: stats.byType.building ?? 0 }, + { icon: Layers, labelKey: 'loadBuild.levels', count: stats.byType.level ?? 0 }, + { icon: Square, labelKey: 'loadBuild.walls', count: stats.byType.wall ?? 0 }, + { icon: DoorOpen, labelKey: 'loadBuild.doors', count: stats.byType.door ?? 0 }, + { icon: AppWindow, labelKey: 'loadBuild.windows', count: stats.byType.window ?? 0 }, + { icon: Box, labelKey: 'loadBuild.items', count: stats.byType.item ?? 0 }, + { icon: Square, labelKey: 'loadBuild.slabs', count: stats.byType.slab ?? 0 }, + { icon: Square, labelKey: 'loadBuild.ceilings', count: stats.byType.ceiling ?? 0 }, + { icon: Square, labelKey: 'loadBuild.zones', count: stats.byType.zone ?? 0 }, + { icon: Scan, labelKey: 'loadBuild.scans', count: stats.byType.scan ?? 0 }, ] satisfies StatRow[] ).filter((row) => row.count > 0) } @@ -87,6 +90,8 @@ function formatFloorArea(m2: number): string { } export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { + const { locale } = useLocale() + const t = (key: string) => (messages[locale as 'en' | 'zh'] as Record)[key] || key const [showAllWarnings, setShowAllWarnings] = useState(false) const [showSchemaIssues, setShowSchemaIssues] = useState(false) @@ -94,7 +99,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { const { fileName, fileSizeBytes, result } = pending const { ok, parsed, stats, errors, warnings, schemaIssues, schemaIssueCount } = result - const rows = statsRows(stats) + const rows = statsRows(stats, t) const visibleWarnings = showAllWarnings ? warnings : warnings.slice(0, 3) const hiddenWarningCount = warnings.length - visibleWarnings.length const schemaIssuesByType = groupSchemaIssuesByType(schemaIssues) @@ -114,7 +119,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { ) : ( )} - {ok ? 'Ready to import' : 'Cannot import this file'} + {ok ? t('loadBuild.readyToImport') : t('loadBuild.cannotImport')} {fileName} · {formatFileSize(fileSizeBytes)} · {stats.total} node @@ -127,7 +132,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
- {errors.length} error{errors.length === 1 ? '' : 's'} + {t('loadBuild.errors').replace('{count}', String(errors.length))}
    {errors.map((e) => ( @@ -140,7 +145,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { {stats.total > 0 && (
    - Structure + {t('loadBuild.structure')}
    {rows.length > 0 ? (
    @@ -151,11 +156,11 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { className={`flex items-center justify-between px-3 py-2 ${ i === rows.length - 1 ? '' : 'border-b' }`} - key={row.label} + key={row.labelKey} >
    - {row.label} + {t(row.labelKey)}
    {row.count}
    @@ -163,7 +168,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { })} {stats.floorAreaM2 > 0 && (
    - Floor area + {t('loadBuild.floorArea')} {formatFloorArea(stats.floorAreaM2)} @@ -172,7 +177,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
    ) : (
    - The file contains no recognised nodes. + {t('loadBuild.noRecognisedNodes')}
    )}
    @@ -182,7 +187,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
    - {warnings.length} warning{warnings.length === 1 ? '' : 's'} + {t('loadBuild.warnings').replace('{count}', String(warnings.length))}
      {visibleWarnings.map((w, i) => ( @@ -195,7 +200,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { onClick={() => setShowAllWarnings(true)} type="button" > - Show {hiddenWarningCount} more + {t('loadBuild.showMore').replace('{count}', String(hiddenWarningCount))} )}
    @@ -209,11 +214,10 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { type="button" > - Schema details ({schemaIssueCount} node - {schemaIssueCount === 1 ? '' : 's'}) + {t('loadBuild.schemaDetails').replace('{count}', String(schemaIssueCount))} - {showSchemaIssues ? 'Hide' : 'Show'} + {showSchemaIssues ? t('loadBuild.hide') : t('loadBuild.showMore').replace('{count}', String(schemaIssueCount))} {showSchemaIssues && ( @@ -242,7 +246,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/box-vent-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/box-vent-tree-node.tsx index f3fc010ce0..03827ea8c0 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/box-vent-tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/box-vent-tree-node.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type BoxVentNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import Image from 'next/image' @@ -42,7 +44,7 @@ export const BoxVentTreeNode = memo(function BoxVentTreeNode({ [nodeId, setSelection], ) - const defaultName = node?.name || 'Box Vent' + const defaultName = node?.name || 'nodeTypes.boxVent' return ( setIsEditing(false), []) const area = calculatePolygonArea(polygon).toFixed(1) - const defaultName = `Ceiling (${area}m²)` + const defaultName = 'nodeTypes.ceilingWithArea' + const defaultNameParams = { area } return ( setIsEditing(true)} diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx index c6635e8319..de46b4a0a4 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type FenceNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import Image from 'next/image' @@ -51,7 +53,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({ isVisible={node.visible !== false} label={ setIsEditing(true)} diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx index 4bc8ffea47..1e75a008bb 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, @@ -39,6 +41,7 @@ import { import { getDefaultLevelName } from './../../../../../lib/level-name' import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection' import { createLocalGuideImage } from './../../../../../lib/local-guide-image' +import { useTranslations } from '../../../../../lib/i18n' import { cn } from './../../../../../lib/utils' import useEditor from './../../../../../store/use-editor' import { useUploadStore } from '../../../../../store/use-upload' @@ -47,6 +50,18 @@ import { InlineRenameInput } from './inline-rename-input' import { focusTreeNode, TreeNode } from './tree-node' import { TreeNodeDragProvider } from './tree-node-drag' +// ============================================================================ +// I18N CONTEXT +// ============================================================================ + +import { createContext, useContext, type ReactNode } from 'react' + +const SiteI18nContext = createContext<(key: string) => string>((key) => key) + +export function useSiteI18n() { + return useContext(SiteI18nContext) +} + // ============================================================================ // PROPERTY LINE SECTION // ============================================================================ @@ -89,6 +104,7 @@ function useSiteNode(): SiteNode | null { } const PropertyLineSection = memo(function PropertyLineSection() { + const t = useTranslations() const siteNode = useSiteNode() const updateNode = useScene((state) => state.updateNode) const mode = useEditor((state) => state.mode) @@ -166,10 +182,10 @@ const PropertyLineSection = memo(function PropertyLineSection() { {/* Measurements */}
    - Area: {area.toFixed(1)} m² + {t('site.area')}: {area.toFixed(1)} m²
    - Perimeter: {perimeter.toFixed(1)} m + {t('site.perimeter')}: {perimeter.toFixed(1)} m
    @@ -245,6 +261,7 @@ const CameraPopover = memo(function CameraPopover({ onOpenChange: (open: boolean) => void buttonClassName?: string }) { + const t = useTranslations() const updateNode = useScene((state) => state.updateNode) return ( @@ -255,7 +272,7 @@ const CameraPopover = memo(function CameraPopover({ buttonClassName, )} onClick={(e) => e.stopPropagation()} - title="Camera snapshot" + title={t('treeActions.cameraSnapshot')} > {hasCamera && ( @@ -280,7 +297,7 @@ const CameraPopover = memo(function CameraPopover({ }} > - View snapshot + {t('treeActions.viewSnapshot')} )} {hasCamera && ( )}
@@ -324,6 +341,7 @@ const ReferenceItem = memo(function ReferenceItem({ setSelectedReferenceId: (id: string) => void handleDelete: (id: string, e: React.MouseEvent) => void }) { + const t = useTranslations() const [isEditing, setIsEditing] = useState(false) const handleSelect = () => { setSelectedReferenceId(refNode.id) @@ -354,19 +372,19 @@ const ReferenceItem = memo(function ReferenceItem({
{refNode.type === 'scan' ? ( Scan ) : ( Guide )} setIsEditing(true)} @@ -377,7 +395,7 @@ const ReferenceItem = memo(function ReferenceItem({ @@ -402,6 +420,7 @@ const LevelReferences = memo(function LevelReferences({ onUploadAsset, onDeleteAsset, }: LevelReferencesProps) { + const t = useTranslations() const createNode = useScene((s) => s.createNode) const deleteNode = useScene((s) => s.deleteNode) const setSelection = useViewer((s) => s.setSelection) @@ -470,14 +489,14 @@ const LevelReferences = memo(function LevelReferences({ useUploadStore.getState().setResult(levelId, guide.url) window.setTimeout(() => useUploadStore.getState().clearUpload(levelId), 600) } catch { - useUploadStore.getState().setError(levelId, 'Could not add that guide image.') + useUploadStore.getState().setError(levelId, t('viewer.couldNotAddGuideImage')) } return } if (!projectId) { useUploadStore.getState().startUpload(levelId, 'scan', file.name) - useUploadStore.getState().setError(levelId, 'No active project. Please open a project first.') + useUploadStore.getState().setError(levelId, t('viewer.openProjectBeforeUpload')) return } @@ -544,7 +563,7 @@ const LevelReferences = memo(function LevelReferences({ ) : ( )} - {uploading ? `Uploading ${uploadingType}... ${progress}%` : 'Upload scan/floorplan'} + {uploading ? `${t('viewer.uploading')} ${uploadingType}... ${progress}%` : t('viewer.uploadScanOrGuide')} void onDeleteAsset?: (projectId: string, url: string) => void }) { + const t = useTranslations() const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false) const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false) const [isEditing, setIsEditing] = useState(false) @@ -716,7 +736,7 @@ const LevelItem = memo(function LevelItem({
Level e.stopPropagation()} - title="Camera snapshot" + title={t('treeActions.cameraSnapshot')} > {level.camera && ( @@ -779,7 +799,7 @@ const LevelItem = memo(function LevelItem({ }} > - {level.camera ? 'Update snapshot' : 'Take snapshot'} + {level.camera ? t('treeActions.updateSnapshot') : t('treeActions.takeSnapshot')} {level.camera && ( )}
@@ -815,7 +835,7 @@ const LevelItem = memo(function LevelItem({ @@ -878,6 +898,7 @@ const LevelsSection = memo(function LevelsSection({ onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void onDeleteAsset?: (projectId: string, url: string) => void } = {}) { + const t = useTranslations() const createNode = useScene((state) => state.createNode) const updateNode = useScene((state) => state.updateNode) const selectedBuildingId = useViewer((state) => state.selection.buildingId) @@ -926,7 +947,7 @@ const LevelsSection = memo(function LevelsSection({
- Add level + {t('site.addLevel')} {levels.length === 0 && (
@@ -934,7 +955,7 @@ const LevelsSection = memo(function LevelsSection({
{/* Horizontal branch line */}
- No levels yet + {t('site.noLevelsYet')}
)} {[...levels].reverse().map((level, index) => ( @@ -957,6 +978,7 @@ const LevelsSection = memo(function LevelsSection({ }) const LayerToggle = memo(function LayerToggle() { + const t = useTranslations() const structureLayer = useEditor((state) => state.structureLayer) const setStructureLayer = useEditor((state) => state.setStructureLayer) const phase = useEditor((state) => state.phase) @@ -994,14 +1016,14 @@ const LayerToggle = memo(function LayerToggle() { )}
Structure - Structure + {t('site.structure')}
@@ -1030,14 +1052,14 @@ const LayerToggle = memo(function LayerToggle() { )}
Furnish - Furnish + {t('site.furnish')}
@@ -1067,14 +1089,14 @@ const LayerToggle = memo(function LayerToggle() { )}
Zones - Zones + {t('site.zones')}
@@ -1087,6 +1109,7 @@ const LayerToggle = memo(function LayerToggle() { }) const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) { + const t = useTranslations() const [isEditing, setIsEditing] = useState(false) const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false) const deleteNode = useScene((state) => state.deleteNode) @@ -1110,7 +1133,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa }, [isSelected]) const area = calculatePolygonArea(zone.polygon).toFixed(1) - const defaultName = `Zone (${area}m²)` + const defaultName = t('nodeTypes.zoneWithArea').replace('{area}', area) const handleClick = () => { setSelection({ zoneId: zone.id }) @@ -1183,7 +1206,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa {zone.camera && ( )}
@@ -1250,6 +1273,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa }) const MultiSelectionBadge = memo(function MultiSelectionBadge() { + const t = useTranslations() const selectedIds = useViewer((state) => state.selection.selectedIds) const setSelection = useViewer((state) => state.setSelection) @@ -1258,11 +1282,11 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() { return (
- {selectedIds.length} objects selected + {selectedIds.length} {t('common.objectsSelected')} @@ -1273,6 +1297,7 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() { const ContentSection = memo(function ContentSection() { const selectedLevelId = useViewer((state) => state.selection.levelId) + const t = useTranslations() const structureLayer = useEditor((state) => state.structureLayer) const phase = useEditor((state) => state.phase) const setPhase = useEditor((state) => state.setPhase) @@ -1301,7 +1326,7 @@ const ContentSection = memo(function ContentSection() { if (!level) { return ( -
Select a level to view content
+
{t('level.selectLevelToView')}
) } @@ -1315,9 +1340,9 @@ const ContentSection = memo(function ContentSection() { if (levelZones.length === 0) { return (
- No zones on this level.{' '} + {t('level.noZonesOnLevel')}{' '}
) @@ -1333,7 +1358,7 @@ const ContentSection = memo(function ContentSection() { } if (elementChildren.length === 0) { - return
No elements on this level
+ return
{t('level.noElementsOnLevel')}
} return ( @@ -1368,6 +1393,7 @@ const BuildingItem = memo(function BuildingItem({ onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void onDeleteAsset?: (projectId: string, url: string) => void }) { + const t = useTranslations() const setSelection = useViewer((state) => state.setSelection) const phase = useEditor((state) => state.phase) const setPhase = useEditor((state) => state.setPhase) @@ -1408,14 +1434,14 @@ const BuildingItem = memo(function BuildingItem({ >
Building - {building.name || 'Building'} + {building.name || t('nodeTypes.building')}
setBuildingCameraOpen(open ? building.id : null)} @@ -1430,7 +1456,7 @@ const BuildingItem = memo(function BuildingItem({ : 'text-muted-foreground hover:bg-accent hover:text-foreground', )} onClick={(e) => e.stopPropagation()} - title="Camera snapshot" + title={t('treeActions.cameraSnapshot')} > {building.camera && ( @@ -1467,7 +1493,7 @@ const BuildingItem = memo(function BuildingItem({ }} > - {building.camera ? 'Update snapshot' : 'Take snapshot'} + {building.camera ? t('treeActions.updateSnapshot') : t('treeActions.takeSnapshot')} {building.camera && ( )}
@@ -1531,6 +1557,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel const setSelection = useViewer((state) => state.setSelection) const phase = useEditor((state) => state.phase) const setPhase = useEditor((state) => state.setPhase) + const t = useTranslations() const [siteCameraOpen, setSiteCameraOpen] = useState(false) const [buildingCameraOpen, setBuildingCameraOpen] = useState(null) @@ -1548,6 +1575,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel ) return ( +
{/* Site Header */} @@ -1564,14 +1592,14 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel >
Site - {siteNode.name || 'Site'} + {siteNode.name || t('nodeTypes.site')}
- No buildings yet + {t('site.noBuildingsYet')} ) : (
@@ -1636,5 +1664,6 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
+ ) } diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx index 2003314ee4..6afaaea3ab 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx @@ -1,13 +1,37 @@ +'use client' + import { type AnyNodeId, useScene } from '@pascal-app/core' import { Pencil } from 'lucide-react' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { cn } from './../../../../../lib/utils' +import { messages, useLocale } from '../../../../../lib/i18n' + +/** + * Resolve a label string. + * - If it contains a dot (.) it is treated as an i18n key. + * - If it contains {param} placeholders, pass `params` to fill them. + * - Otherwise returned as-is (custom user name). + */ +function resolveLabel(label: string, locale: string, params?: Record): string { + if (label.includes('.')) { + const str = (messages[locale as 'en' | 'zh'] as Record)[label] || label + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), + str, + ) + } + return label +} interface InlineRenameInputProps { nodeId: AnyNodeId isEditing: boolean onStopEditing: () => void + /** e.g. 'nodeTypes.wall' or 'nodeTypes.zoneWithArea' (params fills {area}) */ defaultName: string + /** Params for resolving parameterized i18n keys (e.g. { area: 23 }) */ + defaultNameParams?: Record className?: string onStartEditing?: () => void } @@ -17,14 +41,17 @@ export const InlineRenameInput = memo(function InlineRenameInput({ isEditing, onStopEditing, defaultName, + defaultNameParams, className, onStartEditing, }: InlineRenameInputProps) { + const { locale } = useLocale() const updateNode = useScene((s) => s.updateNode) const name = useScene((s) => s.nodes[nodeId]?.name) const [value, setValue] = useState(name || '') const inputRef = useRef(null) - const inputSize = Math.max((value || defaultName).length, 1) + const resolvedDefault = resolveLabel(defaultName, locale, defaultNameParams) + const inputSize = Math.max((value || resolvedDefault).length, 1) useEffect(() => { if (isEditing) { @@ -61,7 +88,7 @@ export const InlineRenameInput = memo(function InlineRenameInput({ return (
- {name || defaultName} + {name || resolvedDefault} {onStartEditing && ( @@ -67,7 +73,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod )} {hasCamera && ( )}
diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx index 6835a62dd4..7ee067c970 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, useScene } from '@pascal-app/core' import { ChevronRight } from 'lucide-react' import { AnimatePresence, motion } from 'motion/react' diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx index e040ccbaeb..789ea09bae 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, useScene, type WallNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import Image from 'next/image' @@ -87,7 +89,7 @@ export const WallTreeNode = memo(function WallTreeNode({ isVisible={isVisible} label={ (messages[locale as 'en' | 'zh'] as Record)[key] || key const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false) const deleteNode = useScene((state) => state.deleteNode) const updateNode = useScene((state) => state.updateNode) @@ -57,7 +62,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) { )} {zone.camera && ( )}
@@ -123,6 +128,8 @@ function ZoneItem({ zone }: { zone: ZoneNode }) { } export function ZonePanel() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale as 'en' | 'zh'] as Record)[key] || key const nodes = useScene((state) => state.nodes) const currentLevelId = useViewer((state) => state.selection.levelId) const setPhase = useEditor((state) => state.setPhase) @@ -145,7 +152,7 @@ export function ZonePanel() { if (!currentLevelId) { return (
- Select a level to view and create zones + {t('zone.selectLevelToView')}
) } @@ -154,9 +161,9 @@ export function ZonePanel() {
{levelZones.length === 0 ? (
- No zones on this level.{' '} + {t('zone.noZonesOnLevel')}{' '}
) : ( diff --git a/packages/editor/src/components/ui/slider-demo.tsx b/packages/editor/src/components/ui/slider-demo.tsx index d7c3bcb18b..8c71d2010c 100644 --- a/packages/editor/src/components/ui/slider-demo.tsx +++ b/packages/editor/src/components/ui/slider-demo.tsx @@ -1,3 +1,5 @@ +'use client' + import NumberFlow from '@number-flow/react' import { useState } from 'react' diff --git a/packages/editor/src/components/ui/slider.tsx b/packages/editor/src/components/ui/slider.tsx index d83475848e..fe2fd44855 100644 --- a/packages/editor/src/components/ui/slider.tsx +++ b/packages/editor/src/components/ui/slider.tsx @@ -1,3 +1,5 @@ +'use client' + import * as SliderPrimitive from '@radix-ui/react-slider' import { cva, type VariantProps } from 'class-variance-authority' import type * as React from 'react' diff --git a/packages/editor/src/components/viewer-overlay.tsx b/packages/editor/src/components/viewer-overlay.tsx index 7d60a23b19..0446beed4c 100644 --- a/packages/editor/src/components/viewer-overlay.tsx +++ b/packages/editor/src/components/viewer-overlay.tsx @@ -30,6 +30,7 @@ import { } from 'lucide-react' import Link from 'next/link' import { useShallow } from 'zustand/react/shallow' +import { messages, useLocale } from '../lib/i18n' import { getLevelDisplayName } from '../lib/level-name' import { cn } from '../lib/utils' import { ActionButton } from './ui/action-menu/action-button' @@ -48,47 +49,48 @@ type ProjectOwner = { image: string | null } -const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = { - stacked: 'Stacked', - exploded: 'Exploded', - solo: 'Solo', -} +const levelModeLabels = (t: (key: string) => string) => ({ + stacked: t('viewer.stack'), + exploded: t('viewer.exploded'), + solo: t('viewer.solo'), +}) -const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = { - manual: 'Stack', - stacked: 'Stack', - exploded: 'Exploded', - solo: 'Solo', -} +const levelModeBadgeLabels = (t: (key: string) => string) => ({ + manual: t('viewer.stack'), + stacked: t('viewer.stack'), + exploded: t('viewer.exploded'), + solo: t('viewer.solo'), +}) -const wallModeConfig = { +const wallModeConfig = (t: (key: string) => string) => ({ up: { icon: (props: any) => ( - Full Height + {t('viewer.fullHeight')} ), - label: 'Full Height', + label: t('viewer.fullHeight'), }, cutaway: { icon: (props: any) => ( - Cutaway + {t('viewer.cutaway')} ), - label: 'Cutaway', + label: t('viewer.cutaway'), }, down: { icon: (props: any) => ( - Low + {t('viewer.low')} ), - label: 'Low', + label: t('viewer.low'), }, -} +}) -const SHADING_OPTIONS = [ - { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box }, - { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles }, +const getShadingOptions = (t: (key: string) => string) => [ + { id: 'solid', name: t('viewer.solid'), detail: t('viewer.flatAndFast'), icon: Box }, + { id: 'rendered', name: t('viewer.rendered'), detail: t('viewer.fullAO'), icon: Sparkles }, ] as const -function RenderModeMenu() { +function RenderModeMenu({ t }: { t: (key: string) => string }) { const shading = useViewer((s) => s.shading) + const SHADING_OPTIONS = getShadingOptions(t) const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0] const ActiveIcon = active.icon return ( @@ -96,7 +98,7 @@ function RenderModeMenu() { string }) { const sceneTheme = useViewer((s) => s.sceneTheme) const active = getSceneTheme(sceneTheme) return ( @@ -134,7 +136,7 @@ function SceneThemeMenu() { string) => [ + { id: 'off', name: t('viewer.off_edge'), detail: t('viewer.noEdgeLines') }, + { id: 'soft', name: t('viewer.soft'), detail: t('viewer.faintOutline') }, + { id: 'strong', name: t('viewer.strong'), detail: t('viewer.crispEdges') }, ] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[] -function EdgesMenu() { +function EdgesMenu({ t }: { t: (key: string) => string }) { const edges = useViewer((s) => s.edges) + const EDGE_OPTIONS = getEdgeOptions(t) const active = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0] return ( @@ -219,15 +222,15 @@ function EdgesMenu() { ) } -const getNodeName = (node: AnyNode): string => { +const getNodeName = (node: AnyNode, t: (key: string) => string): string => { if ('name' in node && node.name) return node.name - if (node.type === 'wall') return 'Wall' - if (node.type === 'fence') return 'Fence' - if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item' - if (node.type === 'slab') return 'Slab' - if (node.type === 'ceiling') return 'Ceiling' - if (node.type === 'roof') return 'Roof' - if (node.type === 'roof-segment') return 'Roof Segment' + if (node.type === 'wall') return t('nodeTypes.wall') + if (node.type === 'fence') return t('nodeTypes.fence') + if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || t('nodeTypes.item') + if (node.type === 'slab') return t('nodeTypes.slab') + if (node.type === 'ceiling') return t('nodeTypes.ceiling') + if (node.type === 'roof') return t('nodeTypes.roof') + if (node.type === 'roof-segment') return t('nodeTypes.roofSegment') return node.type } @@ -246,6 +249,9 @@ export const ViewerOverlay = ({ canShowGuides = true, onBack, }: ViewerOverlayProps) => { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + const selection = useViewer((s) => s.selection) const showScans = useViewer((s) => s.showScans) const showGuides = useViewer((s) => s.showGuides) @@ -321,7 +327,7 @@ export const ViewerOverlay = ({ )}
- {projectName || 'Untitled'} + {projectName || t('common.untitled')}
{owner?.username && ( handleBreadcrumbClick('building')} > - {building.name || 'Building'} + {building.name || t('viewer.building')} )} @@ -364,7 +370,7 @@ export const ViewerOverlay = ({ className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`} onClick={() => handleBreadcrumbClick('level')} > - {getLevelDisplayName(level)} + {getLevelDisplayName(level, locale)} )} @@ -384,7 +390,7 @@ export const ViewerOverlay = ({ <> - {getNodeName(selectedNode)} + {getNodeName(selectedNode, t)} )} @@ -397,7 +403,7 @@ export const ViewerOverlay = ({ {building && levels.length > 0 && (
- Levels + {t('viewer.levels')}
{levels.map((lvl) => { @@ -446,14 +452,14 @@ export const ViewerOverlay = ({ ? 'bg-white/10' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0' } - label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`} + label={`Scans: ${showScans ? t('viewer.visible') : t('viewer.hidden')}`} onClick={() => useViewer.getState().setShowScans(!showScans)} size="icon" tooltipSide="top" variant="ghost" > Scans @@ -467,14 +473,14 @@ export const ViewerOverlay = ({ ? 'bg-white/10' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0' } - label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`} + label={`Guides: ${showGuides ? t('viewer.visible') : t('viewer.hidden')}`} onClick={() => useViewer.getState().setShowGuides(!showGuides)} size="icon" tooltipSide="top" variant="ghost" > Guides @@ -490,7 +496,7 @@ export const ViewerOverlay = ({ ? 'bg-violet-500/20 text-violet-400' : 'hover:bg-white/5 hover:text-violet-400' } - label={`Camera: ${cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}`} + label={`Camera: ${cameraMode === 'perspective' ? t('viewer.perspective') : t('viewer.orthographic')}`} onClick={() => useViewer .getState() @@ -503,11 +509,11 @@ export const ViewerOverlay = ({ - + - + - + {/* Level Mode */} ]}`} onClick={() => { if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked') const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo'] @@ -540,7 +546,7 @@ export const ViewerOverlay = ({ aria-hidden="true" className="pointer-events-none absolute right-1 bottom-1 left-1 rounded border border-border/50 bg-background/70 px-0.5 py-[2px] text-center font-medium font-pixel text-[8px] text-foreground/85 leading-none tracking-[-0.02em] backdrop-blur-sm" > - {levelModeBadgeLabels[levelMode]} + {levelModeBadgeLabels(t)[levelMode as keyof ReturnType]} @@ -552,7 +558,7 @@ export const ViewerOverlay = ({ ? 'bg-white/10' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0' } - label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`} + label={`Walls: ${wallModeConfig(t)[wallMode as keyof ReturnType].label}`} onClick={() => { const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down'] const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length @@ -563,7 +569,7 @@ export const ViewerOverlay = ({ variant="ghost" > {(() => { - const Icon = wallModeConfig[wallMode as keyof typeof wallModeConfig].icon + const Icon = wallModeConfig(t)[wallMode as keyof ReturnType].icon return })()} @@ -573,14 +579,14 @@ export const ViewerOverlay = ({ {/* Camera Actions */} emitter.emit('camera-controls:orbit-ccw')} size="icon" tooltipSide="top" variant="ghost" > Orbit Left @@ -588,14 +594,14 @@ export const ViewerOverlay = ({ emitter.emit('camera-controls:orbit-cw')} size="icon" tooltipSide="top" variant="ghost" > Orbit Right @@ -603,14 +609,14 @@ export const ViewerOverlay = ({ emitter.emit('camera-controls:top-view')} size="icon" tooltipSide="top" variant="ghost" > Top View diff --git a/packages/editor/src/hooks/use-contextual-tools.ts b/packages/editor/src/hooks/use-contextual-tools.ts index e51121b19a..31c0bfd867 100644 --- a/packages/editor/src/hooks/use-contextual-tools.ts +++ b/packages/editor/src/hooks/use-contextual-tools.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useMemo } from 'react' diff --git a/packages/editor/src/hooks/use-grid-events.ts b/packages/editor/src/hooks/use-grid-events.ts index 9c1a22c85f..3395fedd87 100644 --- a/packages/editor/src/hooks/use-grid-events.ts +++ b/packages/editor/src/hooks/use-grid-events.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type EventSuffix, diff --git a/packages/editor/src/hooks/use-keyboard.ts b/packages/editor/src/hooks/use-keyboard.ts index eeba55ccf3..8596c41ead 100644 --- a/packages/editor/src/hooks/use-keyboard.ts +++ b/packages/editor/src/hooks/use-keyboard.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, nodeRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useEffect } from 'react' diff --git a/packages/editor/src/hooks/use-mobile.ts b/packages/editor/src/hooks/use-mobile.ts index 9a549a245d..8aef232bb7 100644 --- a/packages/editor/src/hooks/use-mobile.ts +++ b/packages/editor/src/hooks/use-mobile.ts @@ -1,3 +1,5 @@ +'use client' + import * as React from 'react' const MOBILE_BREAKPOINT = 768 diff --git a/packages/editor/src/hooks/use-reduced-motion.ts b/packages/editor/src/hooks/use-reduced-motion.ts index b46e63ddb7..9f234c83d4 100644 --- a/packages/editor/src/hooks/use-reduced-motion.ts +++ b/packages/editor/src/hooks/use-reduced-motion.ts @@ -1,3 +1,5 @@ +'use client' + import { useEffect, useState } from 'react' /** diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 6d7e442ad3..9546515108 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -217,3 +217,4 @@ export { } from './store/use-palette-view-registry' export { useUploadStore } from './store/use-upload' export { useWallMoveGhosts, type WallMoveGhostBridge } from './store/use-wall-move-ghosts' +export { I18nProvider, useLocale, useTranslations, defaultLocale, type Locale, messages } from './lib/i18n' diff --git a/packages/editor/src/lib/i18n.tsx b/packages/editor/src/lib/i18n.tsx new file mode 100644 index 0000000000..dd90339627 --- /dev/null +++ b/packages/editor/src/lib/i18n.tsx @@ -0,0 +1,60 @@ +'use client' + +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react' +import { IntlProvider } from 'react-intl' +import en from './i18n/en.json' +import zh from './i18n/zh.json' + +export type Locale = 'en' | 'zh' +export const defaultLocale: Locale = 'zh' + +const messages: Record> = { en, zh } + +interface I18nContextType { + locale: Locale + setLocale: (locale: Locale) => void +} + +const I18nContext = createContext({ + locale: defaultLocale, + setLocale: () => {}, +}) + +export function useLocale() { + return useContext(I18nContext) +} + +export function useTranslations() { + const { locale } = useContext(I18nContext) + return (key: string, params?: Record): string => { + const str = messages[locale][key] ?? key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), + str, + ) + } +} + +export function I18nProvider({ children }: { children: ReactNode }) { + const [locale, setLocale] = useState(defaultLocale) + + useEffect(() => { + const browserLang = navigator.language.toLowerCase() + if (browserLang.startsWith('zh')) { + setLocale('zh') + } else { + setLocale('en') + } + }, []) + + return ( + + + {children} + + + ) +} + +export { messages } diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json new file mode 100644 index 0000000000..50f8dc286a --- /dev/null +++ b/packages/editor/src/lib/i18n/en.json @@ -0,0 +1,861 @@ +{ + "catalog.intensity": "Intensity", + "commands.addLevel": "Add Level", + "commands.back": "back", + "commands.ceilingTool": "Ceiling Tool", + "commands.close": "close", + "commands.commandPalette": "Command Palette", + "commands.copyShareLink": "Copy Share Link", + "commands.cutaway": "Cutaway", + "commands.deleteLevel": "Delete Level", + "commands.deleteSelection": "Delete Selection", + "commands.doorTool": "Door Tool", + "commands.down": "Down", + "commands.enterPreview": "Enter Preview", + "commands.exitPreview": "Exit Preview", + "commands.exploded": "Exploded", + "commands.exportGLB": "Export 3D Model (GLB)", + "commands.exportJSON": "Export Scene (JSON)", + "commands.filterOptions": "Filter options…", + "commands.gotoLevel": "Go to Level", + "commands.group.exportShare": "Export & Share", + "commands.group.history": "History", + "commands.group.levels": "Levels", + "commands.group.scene": "Scene", + "commands.group.view": "View", + "commands.group.viewerControls": "Viewer Controls", + "commands.itemTool": "Item Tool", + "commands.levelMode": "Level Mode", + "commands.manual": "Manual", + "commands.materialPaint": "Material Paint", + "commands.navigate": "navigate", + "commands.noCommandsFound": "No commands found.", + "commands.redo": "Redo", + "commands.renameLevel": "Rename Level", + "commands.searchActions": "Search actions…", + "commands.select": "select", + "commands.slabTool": "Slab Tool", + "commands.solo": "Solo", + "commands.stacked": "Stacked", + "commands.stairTool": "Stair Tool", + "commands.switchTo": "Switch to", + "commands.switchToRendered": "Switch to Rendered", + "commands.switchToSolid": "Switch to Solid", + "commands.takeScreenshot": "Take Screenshot", + "commands.takeSnapshot": "Take Snapshot", + "commands.toggleFullscreen": "Toggle Fullscreen", + "commands.typeNewName": "Type a new name…", + "commands.typeNewNameAbove": "Type a new name above…", + "commands.undo": "Undo", + "commands.up": "Up", + "commands.wallMode": "Wall Mode", + "commands.wallTool": "Wall Tool", + "commands.windowTool": "Window Tool", + "commands.zoneTool": "Zone Tool", + "common.actions": "Actions", + "common.add": "Add", + "common.addOne": "Add one", + "common.all": "All", + "common.apply": "Apply", + "common.back": "Back", + "common.cancel": "Cancel", + "common.close": "Close", + "common.confirm": "Confirm", + "common.copy": "Copy", + "common.corners.bottomLeft": "Bottom Left", + "common.corners.bottomRight": "Bottom Right", + "common.corners.topLeft": "Top Left", + "common.corners.topRight": "Top Right", + "common.cut": "Cut", + "common.delete": "Delete", + "common.depth": "Depth", + "common.deselect": "Deselect", + "common.dimensions": "Dimensions", + "common.directions.down": "Down", + "common.directions.up": "Up", + "common.done": "Done", + "common.duplicate": "Duplicate", + "common.edit": "Edit", + "common.error": "Error", + "common.height": "Height", + "common.hide": "Hide", + "common.home": "Home", + "common.length": "Length", + "common.loading": "Loading...", + "common.move": "Move", + "common.next": "Next", + "common.noResults": "No results found", + "common.objectsSelected": "objects selected", + "common.open": "Open", + "common.paste": "Paste", + "common.position": "Position", + "common.redo": "Redo", + "common.remove": "Remove", + "common.reset": "Reset", + "common.rotate": "Rotate", + "common.save": "Save", + "common.search": "Search", + "common.select": "Select", + "common.settings": "Settings", + "common.show": "Show", + "common.success": "Success", + "common.thickness": "Thickness", + "common.undo": "Undo", + "common.untitled": "Untitled", + "common.width": "Width", + "controlModes.boxSelect": "Box select", + "controlModes.build": "Build", + "controlModes.editSite": "Edit site", + "controlModes.exitSiteEditing": "Exit site editing", + "controlModes.furnish": "Furnish", + "controlModes.materialPaint": "Material Paint", + "controlModes.preview": "Preview", + "controlModes.select": "Select", + "controlModes.siteEditGroundOnly": "Site editing only available on ground floor", + "controlModes.zone": "Zone", + "editor.cameraPan": "Pan", + "editor.cameraRotate": "Rotate", + "editor.cameraZoom": "Zoom", + "editor.createNew": "Create new", + "editor.dismiss": "Dismiss", + "editor.leftClick": "Left click", + "editor.localWarning": "Local editor — scenes are not saved.", + "editor.middleClick": "Middle click", + "editor.openRecent": "Open recent scenes", + "editor.rightClick": "Right click", + "editor.scrollWheel": "Scroll wheel", + "editor.space": "Space", + "furnishTools.appliance": "Appliance", + "furnishTools.bathroom": "Bathroom", + "furnishTools.furniture": "Furniture", + "furnishTools.kitchen": "Kitchen", + "furnishTools.outdoor": "Outdoor", + "itemHelper.freePlace": "Free place", + "itemHelper.placeItem": "Place item", + "itemHelper.rotateClockwise": "Rotate clockwise", + "itemHelper.rotateCounterclockwise": "Rotate counterclockwise", + "items.all": "All", + "items.community": "Community", + "items.library": "Library", + "items.mine": "Mine", + "items.noResults": "No results for \"{search}\"", + "items.search": "Search...", + "level.addLevelAbove": "Add level above", + "level.addLevelBelow": "Add level below", + "level.basement": "Basement {n}", + "level.cancel": "Cancel", + "level.cannotDeleteGround": "The ground level cannot be deleted", + "level.confirmDelete": "Are you sure you want to delete {name}? All walls, floors, and objects on this level will be permanently removed.", + "level.delete": "Delete", + "level.deleteLevel": "Delete level", + "level.deleteLevelTitle": "Delete level", + "level.dragToReorder": "Drag to reorder", + "level.duplicateLevel": "Duplicate level", + "level.duplicateOptions": "Duplicate with options...", + "level.floor": "Floor {n}", + "level.groundFloor": "Ground Floor", + "level.insertLevelHere": "Insert level here", + "level.noElementsOnLevel": "No elements on this level", + "level.noZonesOnLevel": "No zones on this level.", + "level.pasteCopied": "Paste copied selection", + "level.reorder": "Reorder", + "level.selectLevelToView": "Select a level to view content", + "levelDuplicate.cancel": "Cancel", + "levelDuplicate.chooseWhatToCopy": "Choose what to copy from {level}.", + "levelDuplicate.duplicate": "Duplicate", + "levelDuplicate.duplicateLevel": "Duplicate Level", + "levelDuplicate.everything": "Everything", + "levelDuplicate.everythingDesc": "Structure, materials, furniture, and references.", + "levelDuplicate.structure": "Structure only", + "levelDuplicate.structureDesc": "Walls, slabs, roofs, stairs, windows, and doors without finishes.", + "levelDuplicate.structureFurniture": "Structure + furniture", + "levelDuplicate.structureFurnitureDesc": "Structure, finishes, and placed items, without guide references.", + "levelDuplicate.structureMaterials": "Structure + materials", + "levelDuplicate.structureMaterialsDesc": "Structure with the current material and finish assignments.", + "loadBuild.buildings": "Buildings", + "loadBuild.cannotImport": "Cannot import this file", + "loadBuild.ceilings": "Ceilings", + "loadBuild.doors": "Doors", + "loadBuild.errors": "{count} error", + "loadBuild.floorArea": "Floor area", + "loadBuild.hide": "Hide", + "loadBuild.items": "Items", + "loadBuild.levels": "Levels", + "loadBuild.noRecognisedNodes": "The file contains no recognised nodes.", + "loadBuild.readyToImport": "Ready to import", + "loadBuild.replaceScene": "Replace current scene", + "loadBuild.scans": "Scans", + "loadBuild.schemaDetails": "Schema details ({count} nodes)", + "loadBuild.showMore": "Show {count} more", + "loadBuild.sites": "Sites", + "loadBuild.slabs": "Slabs", + "loadBuild.structure": "Structure", + "loadBuild.walls": "Walls", + "loadBuild.warnings": "{count} warning", + "loadBuild.windows": "Windows", + "loadBuild.zones": "Zones", + "nav.home": "Home", + "nav.privacy": "Privacy", + "nav.scenes": "Scenes", + "nav.terms": "Terms", + "nodeActions.curve": "Curve", + "nodeActions.cutOut": "Cut Out", + "nodeActions.delete": "Delete", + "nodeActions.duplicate": "Duplicate", + "nodeActions.move": "Move", + "nodeTypes.boxVent": "Box Vent", + "nodeTypes.building": "Building", + "nodeTypes.ceiling": "Ceiling", + "nodeTypes.ceilingWithArea": "Ceiling ({area}m²)", + "nodeTypes.chimney": "Chimney", + "nodeTypes.column": "Column", + "nodeTypes.door": "Door", + "nodeTypes.dormer": "Dormer", + "nodeTypes.elevator": "Elevator", + "nodeTypes.fence": "Fence", + "nodeTypes.flat": "Flat", + "nodeTypes.flight": "Flight", + "nodeTypes.gable": "Gable", + "nodeTypes.guide": "Guide", + "nodeTypes.hip": "Hip", + "nodeTypes.item": "Item", + "nodeTypes.landing": "Landing", + "nodeTypes.level": "Level", + "nodeTypes.ridgeVent": "Ridge Vent", + "nodeTypes.roof": "Roof", + "nodeTypes.roofSegment": "Roof segment", + "nodeTypes.roofSegmentWithDims": "{type} ({width}×{depth}m)", + "nodeTypes.roofWithSegments": "Roof ({count} segments)", + "nodeTypes.scan": "Scan", + "nodeTypes.selection": "Selection", + "nodeTypes.shed": "Shed", + "nodeTypes.shelf": "Shelf", + "nodeTypes.site": "Site", + "nodeTypes.skyLight": "Skylight", + "nodeTypes.slab": "Slab", + "nodeTypes.slabWithArea": "Slab ({area}m²)", + "nodeTypes.solarPanel": "Solar Panel", + "nodeTypes.spawn": "Spawn Point", + "nodeTypes.stairSegment": "Stair segment", + "nodeTypes.stairSegmentWithDims": "{type} ({width}×{depth}m)", + "nodeTypes.staircaseWithSegments": "Staircase ({count} segments)", + "nodeTypes.stairs": "Stairs", + "nodeTypes.wall": "Wall", + "nodeTypes.window": "Window", + "nodeTypes.zone": "Zone", + "nodeTypes.zoneWithArea": "Zone ({area}m²)", + "nodes.boxVent.baseHeight": "Base Height", + "nodes.boxVent.baseInset": "Base Inset", + "nodes.boxVent.box": "Box", + "nodes.boxVent.cap": "Cap", + "nodes.boxVent.capHeight": "Cap Height", + "nodes.boxVent.cornerBevel": "Corner Bevel", + "nodes.boxVent.dome": "Dome", + "nodes.boxVent.domeCurvature": "Dome Curvature", + "nodes.boxVent.gapHeight": "Gap Height", + "nodes.boxVent.hoodOverhang": "Hood Overhang", + "nodes.boxVent.topTaper": "Top Taper", + "nodes.building.description": "A building container holding one or more levels.", + "nodes.building.label": "Building", + "nodes.ceiling.addHole": "Add Hole", + "nodes.ceiling.auto": "Auto", + "nodes.ceiling.autoHoleLabel.elevator": "Auto elevator cutout", + "nodes.ceiling.autoHoleLabel.stair": "Auto stair cutout", + "nodes.ceiling.heightPresets.high": "High (3.0m)", + "nodes.ceiling.heightPresets.low": "Low (2.4m)", + "nodes.ceiling.heightPresets.standard": "Standard (2.5m)", + "nodes.ceiling.holeLabel": "Hole {index}", + "nodes.ceiling.manual": "Manual", + "nodes.ceiling.noHoles": "No holes", + "nodes.chimney.aboveRidge": "Above Ridge", + "nodes.chimney.back": "Back", + "nodes.chimney.bandExtent": "Extent", + "nodes.chimney.bandOffset": "Offset", + "nodes.chimney.bandThickness": "Thickness", + "nodes.chimney.bands": "Bands", + "nodes.chimney.cap": "Cap", + "nodes.chimney.capThickness": "Thickness", + "nodes.chimney.corbeled": "Corbeled", + "nodes.chimney.cornerBevel": "Corner Bevel", + "nodes.chimney.count": "Count", + "nodes.chimney.cricket": "Cricket", + "nodes.chimney.cricketHeight": "Height", + "nodes.chimney.cricketLength": "Length", + "nodes.chimney.cutoutOffset": "Cutout Offset", + "nodes.chimney.diameter": "Diameter", + "nodes.chimney.double": "Double", + "nodes.chimney.flat": "Flat", + "nodes.chimney.flueDiameter": "Diameter", + "nodes.chimney.flueHeight": "Height", + "nodes.chimney.flueShape": "Shape", + "nodes.chimney.flueSpacing": "Spacing", + "nodes.chimney.flueWallThickness": "Wall Thickness", + "nodes.chimney.flues": "Flues", + "nodes.chimney.front": "Front", + "nodes.chimney.hollowDepth": "Hollow Depth", + "nodes.chimney.none": "None", + "nodes.chimney.overhang": "Overhang", + "nodes.chimney.panelDepth": "Depth", + "nodes.chimney.panelHeight": "Height", + "nodes.chimney.panelMargin": "Side Margin", + "nodes.chimney.panelOffsetTop": "Top Offset", + "nodes.chimney.panels": "Panels", + "nodes.chimney.rectangular": "Rectangular", + "nodes.chimney.round": "Round", + "nodes.chimney.shoulder": "Shoulder", + "nodes.chimney.shoulderExtent": "Extent", + "nodes.chimney.shoulderHeight": "Height", + "nodes.chimney.simple": "Simple", + "nodes.chimney.single": "Single", + "nodes.chimney.sloped": "Sloped", + "nodes.chimney.square": "Square", + "nodes.chimney.stepped": "Stepped", + "nodes.chimney.tapered": "Tapered", + "nodes.column.aFrame": "A-Frame", + "nodes.column.applyPreset": "Apply preset...", + "nodes.column.applyProportion": "Apply proportion...", + "nodes.column.bottomDepth": "Bottom Depth", + "nodes.column.bottomHeight": "Bottom Height", + "nodes.column.bottomSpread": "Bottom Spread", + "nodes.column.bottomStepSpread": "Bottom Step Spread", + "nodes.column.bottomTiers": "Bottom Tiers", + "nodes.column.bottomWidth": "Bottom Width", + "nodes.column.boxFrame": "Box Frame", + "nodes.column.braceDepth": "Brace Depth", + "nodes.column.braceWidth": "Brace Width", + "nodes.column.bulge": "Bulge", + "nodes.column.bulged": "Bulged", + "nodes.column.connectorPlates": "Connector Plates", + "nodes.column.edgeSoftness": "Edge Softness", + "nodes.column.endWidth": "End Width", + "nodes.column.forkSpread": "Fork Spread", + "nodes.column.hourglass": "Hourglass", + "nodes.column.kBrace": "K Brace", + "nodes.column.neckWidth": "Neck Width", + "nodes.column.noBottom": "No Bottom", + "nodes.column.noTop": "No Top", + "nodes.column.plinthThickness": "Plinth Thickness", + "nodes.column.portalFrame": "Portal Frame", + "nodes.column.rectangular": "Rectangular", + "nodes.column.ringPairs": "Ring Pairs", + "nodes.column.ringSpread": "Ring Spread", + "nodes.column.ringThickness": "Ring Thickness", + "nodes.column.round": "Round", + "nodes.column.roundBandWidth": "Round Band Width", + "nodes.column.roundRings": "Rounded Bottom", + "nodes.column.roundedTop": "Rounded Top", + "nodes.column.segmentTwist": "Segment Twist", + "nodes.column.shaftCornerRadius": "Shaft Corner Radius", + "nodes.column.shaftWidth": "Shaft Width", + "nodes.column.simpleBlockBottom": "Simple Block Bottom", + "nodes.column.simpleTop": "Simple Top", + "nodes.column.singleStrut": "Single Strut", + "nodes.column.square": "Square", + "nodes.column.squarePlinthBottom": "Square Plinth Bottom", + "nodes.column.steppedBottom": "Stepped Bottom", + "nodes.column.steppedTop": "Stepped Top", + "nodes.column.straight": "Straight", + "nodes.column.taper": "Taper", + "nodes.column.tapered": "Tapered", + "nodes.column.topDepth": "Top Depth", + "nodes.column.topHeight": "Top Height", + "nodes.column.topSpread": "Top Spread", + "nodes.column.topStepSpread": "Top Step Spread", + "nodes.column.topTiers": "Top Tiers", + "nodes.column.topWidth": "Top Width", + "nodes.column.trestle": "Trestle", + "nodes.column.tripod": "Tripod", + "nodes.column.twistSegments": "Twist Segments", + "nodes.column.vFrame": "V Support", + "nodes.column.vertical": "Vertical", + "nodes.column.waist": "Waist", + "nodes.column.xBrace": "X Brace", + "nodes.column.yFrame": "Y Support", + "nodes.column.yaw": "Yaw", + "nodes.door.addSegment": "+ Add Segment", + "nodes.door.archHeight": "Arch Height", + "nodes.door.columnLabel": "C{i}", + "nodes.door.columns": "Columns", + "nodes.door.contentPaddingSection": "Content Padding", + "nodes.door.corners.topLeft": "Top Left", + "nodes.door.corners.topRight": "Top Right", + "nodes.door.defaultName": "Door {count}", + "nodes.door.description": "A door cut into a wall. Animated open/close.", + "nodes.door.direction": "Direction", + "nodes.door.divider": "Divider", + "nodes.door.doorCloser": "Door Closer", + "nodes.door.doorTypeOptions.barn": "Barn", + "nodes.door.doorTypeOptions.double": "Double", + "nodes.door.doorTypeOptions.folding": "Folding", + "nodes.door.doorTypeOptions.french": "French", + "nodes.door.doorTypeOptions.hinged": "Hinged", + "nodes.door.doorTypeOptions.pocket": "Pocket", + "nodes.door.doorTypeOptions.rollup": "Roll-up", + "nodes.door.doorTypeOptions.sectional": "Sectional", + "nodes.door.doorTypeOptions.sliding": "Sliding", + "nodes.door.doorTypeOptions.tiltup": "Tilt-up", + "nodes.door.enableHandle": "Enable Handle", + "nodes.door.enableThreshold": "Enable Threshold", + "nodes.door.fallbackTitle": "Door", + "nodes.door.flipSide": "Flip Side", + "nodes.door.fold": "Fold", + "nodes.door.frameSection": "Frame", + "nodes.door.handleSection": "Handle", + "nodes.door.handleSide": "Handle Side", + "nodes.door.hardwareSection": "Hardware", + "nodes.door.hingesSide": "Hinges Side", + "nodes.door.horizontalPadding": "Horizontal", + "nodes.door.inset": "Inset", + "nodes.door.label": "Door", + "nodes.door.open": "Open", + "nodes.door.openingShapeOptions.arch": "Arch", + "nodes.door.openingShapeOptions.rect": "Rect", + "nodes.door.openingShapeOptions.rounded": "Rounded", + "nodes.door.openingShapeSection": "Opening Shape", + "nodes.door.panels": "Panels", + "nodes.door.panicBar": "Panic Bar", + "nodes.door.panicBarHeight": "Bar Height", + "nodes.door.radiusModeOptions.all": "All", + "nodes.door.radiusModeOptions.individual": "Individual", + "nodes.door.remove": "- Remove", + "nodes.door.revealRadius": "Reveal Radius", + "nodes.door.segmentLabel": "Segment {i}", + "nodes.door.segmentTypeOptions.empty": "Empty", + "nodes.door.segmentTypeOptions.glass": "Glass", + "nodes.door.segmentTypeOptions.panel": "Panel", + "nodes.door.segmentsSection": "Segments", + "nodes.door.slide": "Slide", + "nodes.door.slideDirectionOptions.panel": "Panel", + "nodes.door.slideDirectionOptions.pocket": "Pocket", + "nodes.door.slideDirectionOptions.rail": "Rail", + "nodes.door.swingDirectionOptions.inward": "Inward", + "nodes.door.swingDirectionOptions.outward": "Outward", + "nodes.door.swingSection": "Swing", + "nodes.door.thresholdSection": "Threshold", + "nodes.door.toolHints.cancel": "Cancel", + "nodes.door.toolHints.place": "Place door on wall", + "nodes.door.topShapeOptions.arch": "Arch", + "nodes.door.topShapeOptions.rect": "Rect", + "nodes.door.topShapeOptions.rounded": "Rounded", + "nodes.door.topShapeSection": "Top Shape", + "nodes.door.type": "Type", + "nodes.door.typeOptions.door": "Door", + "nodes.door.typeOptions.garage": "Garage", + "nodes.door.typeOptions.opening": "Opening", + "nodes.door.verticalPadding": "Vertical", + "nodes.dormer.all": "All", + "nodes.dormer.arch": "Arch", + "nodes.dormer.archHeight": "Arch Height", + "nodes.dormer.bottomLeft": "Bottom Left", + "nodes.dormer.bottomRight": "Bottom Right", + "nodes.dormer.columns": "Columns", + "nodes.dormer.cornerRadius": "Corner Radius", + "nodes.dormer.depth": "Depth", + "nodes.dormer.divider": "Divider", + "nodes.dormer.dormer": "Dormer", + "nodes.dormer.dutch": "Dutch", + "nodes.dormer.enableSill": "Enable Sill", + "nodes.dormer.flat": "Flat", + "nodes.dormer.frameDepth": "Depth", + "nodes.dormer.frameThickness": "Thickness", + "nodes.dormer.gable": "Gable", + "nodes.dormer.gambrel": "Gambrel", + "nodes.dormer.hip": "Hip", + "nodes.dormer.hungWall": "Hung Wall", + "nodes.dormer.individual": "Individual", + "nodes.dormer.mansard": "Mansard", + "nodes.dormer.rect": "Rect", + "nodes.dormer.roofHeight": "Roof Height", + "nodes.dormer.rounded": "Rounded", + "nodes.dormer.rows": "Rows", + "nodes.dormer.section": "Section", + "nodes.dormer.shed": "Shed", + "nodes.dormer.sillDepth": "Depth", + "nodes.dormer.sillThickness": "Thickness", + "nodes.dormer.topLeft": "Top Left", + "nodes.dormer.topRight": "Top Right", + "nodes.dormer.wallHeight": "Wall Height", + "nodes.dormer.width": "Width", + "nodes.dormer.window": "Window", + "nodes.dormer.windowHeight": "Height", + "nodes.dormer.windowOffsetX": "Offset X", + "nodes.dormer.windowOffsetY": "Offset Y", + "nodes.dormer.windowWidth": "Width", + "nodes.elevator.cabHeight": "Cab Height", + "nodes.elevator.cabWidth": "Width", + "nodes.elevator.centerOpening": "Center opening", + "nodes.elevator.defaultFloor": "Default Floor", + "nodes.elevator.disabled": "Disabled", + "nodes.elevator.doorHeight": "Door Height", + "nodes.elevator.doorTime": "Door Time", + "nodes.elevator.doorType": "Door Type", + "nodes.elevator.doorWidth": "Door Width", + "nodes.elevator.dwell": "Dwell", + "nodes.elevator.from": "From", + "nodes.elevator.glassFrame": "Glass frame", + "nodes.elevator.openingStyle": "Opening Style", + "nodes.elevator.segmentedPanel": "Segmented panel", + "nodes.elevator.service": "Service", + "nodes.elevator.shaftDepth": "Shaft Depth", + "nodes.elevator.shaftStyle": "Shaft Style", + "nodes.elevator.shaftWidth": "Shaft Width", + "nodes.elevator.singleLeft": "Single left", + "nodes.elevator.singleRight": "Single right", + "nodes.elevator.solidPanel": "Solid panel", + "nodes.elevator.speed": "Speed", + "nodes.elevator.to": "To", + "nodes.elevator.wallThickness": "Wall Thickness", + "nodes.level.description": "A single floor of a building, holding walls / slabs / ceilings / items.", + "nodes.level.label": "Level", + "nodes.roof.addChimney": "Add Chimney", + "nodes.roof.addDormer": "Add Dormer", + "nodes.roof.addSegment": "Add Segment", + "nodes.roof.addSkylight": "Add Skylight", + "nodes.roof.addSolarPanel": "Add Solar Panel", + "nodes.roof.addVent": "Add Vent", + "nodes.roof.box": "Box", + "nodes.roof.ridge": "Ridge", + "nodes.slab.addHole": "Add Hole", + "nodes.slab.area": "Area", + "nodes.slab.auto": "Auto", + "nodes.slab.autoHoleLabel.elevator": "Auto elevator cutout", + "nodes.slab.autoHoleLabel.stair": "Auto stair cutout", + "nodes.slab.description": "A polygon-bounded floor surface that hosts items on top.", + "nodes.slab.elevationPresets.ground": "Ground (0m)", + "nodes.slab.elevationPresets.raised": "Raised (+5cm)", + "nodes.slab.elevationPresets.step": "Step (+15cm)", + "nodes.slab.elevationPresets.sunken": "Sunken (-15cm)", + "nodes.slab.fallbackTitle": "Slab", + "nodes.slab.holeLabel": "Hole {index}", + "nodes.slab.label": "Slab", + "nodes.slab.manual": "Manual", + "nodes.slab.noHoles": "No holes", + "nodes.slab.toolHints.cancel": "Cancel", + "nodes.slab.toolHints.finish": "Finish slab", + "nodes.slab.toolHints.trace": "Trace slab outline", + "nodes.solarPanel.columns": "Columns", + "nodes.solarPanel.flipOrientation": "Flip orientation", + "nodes.solarPanel.flush": "Flush", + "nodes.solarPanel.frameDepth": "Frame depth", + "nodes.solarPanel.frameThickness": "Frame thickness", + "nodes.solarPanel.gapX": "Gap X", + "nodes.solarPanel.gapY": "Gap Y", + "nodes.solarPanel.rows": "Rows", + "nodes.solarPanel.standoff": "Standoff", + "nodes.solarPanel.tiltAngle": "Tilt angle", + "nodes.solarPanel.tilted": "Tilted", + "nodes.stair.addFlight": "Add flight", + "nodes.stair.addLanding": "Add landing", + "nodes.stair.autoCutout": "Auto Cutout", + "nodes.stair.both": "Both", + "nodes.stair.centerColumn": "Center Column", + "nodes.stair.curved": "Curved", + "nodes.stair.destination": "Destination", + "nodes.stair.fitToFloor": "Fit To Floor", + "nodes.stair.fromLevel": "From Level", + "nodes.stair.geometry": "Geometry", + "nodes.stair.innerRadius": "Inner Radius", + "nodes.stair.integrated": "Integrated", + "nodes.stair.left": "Left", + "nodes.stair.none": "None", + "nodes.stair.opening": "Opening", + "nodes.stair.openingOffset": "Opening Offset", + "nodes.stair.position": "Position", + "nodes.stair.railing": "Railing", + "nodes.stair.railingHeight": "Height", + "nodes.stair.right": "Right", + "nodes.stair.rotationPresets.negative": "-45°", + "nodes.stair.rotationPresets.positive": "+45°", + "nodes.stair.segment": "Segment", + "nodes.stair.segments": "Segments", + "nodes.stair.spiral": "Spiral", + "nodes.stair.stepSupports": "Step Supports", + "nodes.stair.straight": "Straight", + "nodes.stair.sweep": "Sweep", + "nodes.stair.toLevel": "To Level", + "nodes.stair.topLanding": "Top Landing", + "nodes.stair.type": "Type", + "nodes.wall.curve": "Curve", + "nodes.wall.description": "A straight or curved wall segment. Hosts doors, windows, and wall-mounted items.", + "nodes.wall.draftName": "Draft wall", + "nodes.wall.fallbackTitle": "Wall", + "nodes.wall.label": "Wall", + "nodes.wall.length": "Length", + "nodes.wall.toolHints.allowAngles": "Allow non-45° angles", + "nodes.wall.toolHints.cancel": "Cancel", + "nodes.wall.toolHints.setStartEnd": "Set wall start / end", + "nodes.window.archHeight": "Arch Height", + "nodes.window.casementStyleOptions.french": "French", + "nodes.window.casementStyleOptions.single": "Single", + "nodes.window.colWidths": "Col Widths", + "nodes.window.columns": "Columns", + "nodes.window.cornerRadius": "Corner Radius", + "nodes.window.corners.bottomLeft": "Bottom Left", + "nodes.window.corners.bottomRight": "Bottom Right", + "nodes.window.corners.topLeft": "Top Left", + "nodes.window.corners.topRight": "Top Right", + "nodes.window.defaultName": "Window {count}", + "nodes.window.depth": "Depth", + "nodes.window.description": "A window cut into a wall. Animated open/close for opening windows.", + "nodes.window.divider": "Divider", + "nodes.window.enableSill": "Enable Sill", + "nodes.window.fallbackTitle": "Window", + "nodes.window.flipSide": "Flip Side", + "nodes.window.frame": "Frame", + "nodes.window.grid": "Grid", + "nodes.window.label": "Window", + "nodes.window.openingShape": "Opening Shape", + "nodes.window.openingShapeOptions.arch": "Arch", + "nodes.window.openingShapeOptions.rect": "Rect", + "nodes.window.openingShapeOptions.rounded": "Rounded", + "nodes.window.operationLabels.slide": "Slide", + "nodes.window.radiusModeOptions.all": "All", + "nodes.window.radiusModeOptions.individual": "Individual", + "nodes.window.revealRadius": "Reveal Radius", + "nodes.window.rowHeights": "Row Heights", + "nodes.window.rowLabel": "R{i}", + "nodes.window.rows": "Rows", + "nodes.window.sill": "Sill", + "nodes.window.sillDepth": "Sill Depth", + "nodes.window.sillThickness": "Sill Thickness", + "nodes.window.thickness": "Thickness", + "nodes.window.toolHints.cancel": "Cancel", + "nodes.window.toolHints.place": "Place window on wall", + "nodes.window.topShape": "Top Shape", + "nodes.window.topShapeOptions.arch": "Arch", + "nodes.window.topShapeOptions.rect": "Rect", + "nodes.window.topShapeOptions.rounded": "Rounded", + "nodes.window.type": "Type", + "nodes.window.typeOptions.awning": "Awning", + "nodes.window.typeOptions.bay": "Bay", + "nodes.window.typeOptions.bow": "Bow", + "nodes.window.typeOptions.casement": "Casement", + "nodes.window.typeOptions.doubleHung": "Double Hung", + "nodes.window.typeOptions.fixed": "Fixed", + "nodes.window.typeOptions.louvered": "Louvered", + "nodes.window.typeOptions.opening": "Opening", + "nodes.window.typeOptions.singleHung": "Single Hung", + "nodes.window.typeOptions.sliding": "Sliding", + "nodes.window.typeOptions.window": "Window", + "nodes.window.windowType": "Window Type", + "panel.deleteLevel": "Delete level", + "panel.duplicateLevel": "Duplicate level", + "panel.editScale": "Edit Scale", + "panel.hideScale": "Hide Scale", + "panel.intensity": "Intensity", + "panel.noEdgeLines": "No edge lines", + "panel.scaleAndOpacity": "Scale & Opacity", + "panel.setScale": "Set Scale", + "panel.showScale": "Show Scale", + "scenes.allScenes": "All scenes", + "scenes.noScenes": "No scenes yet. Create one to get started.", + "scenes.noThumbnail": "No thumbnail", + "scenes.nodes": "{count} nodes", + "scenes.sceneCount": "{count} scenes", + "scenes.title": "Your scenes", + "settings.adjustVolume": "Adjust volume levels and mute settings", + "settings.audio": "Audio", + "settings.audioSettings": "Audio Settings", + "settings.clearAndStartNew": "Clear & Start New", + "settings.dangerZone": "Danger Zone", + "settings.exploreSceneGraph": "Explore scene graph", + "settings.export": "Export", + "settings.exportedGLB": "Export GLB", + "settings.exportedOBJ": "Export OBJ", + "settings.exportedSTL": "Export STL", + "settings.exporting": "Generating...", + "settings.generateThumbnail": "Generate Thumbnail", + "settings.keyboard": "Keyboard", + "settings.loadBuild": "Load Build", + "settings.masterVolume": "Master Volume", + "settings.muteAllSounds": "Mute All Sounds", + "settings.public": "Public", + "settings.radioVolume": "Radio Volume", + "settings.saveAndLoad": "Save & Load", + "settings.saveBuild": "Save Build", + "settings.sceneGraph": "Scene Graph", + "settings.shadows": "Shadows", + "settings.show3DScans": "Show 3D Scans", + "settings.showFloorplans": "Show Floorplans", + "settings.showGrid": "Show Grid", + "settings.soundEffects": "Sound Effects", + "settings.thumbnail": "Thumbnail", + "settings.unmuteAllSounds": "Unmute All Sounds", + "settings.visibility": "Visibility", + "settings.visibilityAnyone": "Anyone", + "settings.visibilityCanView": "can view", + "settings.visibilityCastShadows": "Cast shadows from lights", + "settings.visibilityEditorOnly": "Visible only in the editor", + "settings.visibilityOnlyYou": "Only you", + "settings.visibilityPublic": "Public", + "settings.visibilityShow3DScans": "Show 3D Scans", + "settings.visibilityShowFloorplans": "Show Floorplans", + "settings.visibilityShowGrid": "Show Grid", + "settings.visibilityVisiblePublic": "Visible to public viewers", + "shortcuts.addOrRemoveFromSelection": "Add or remove an object from multi-selection", + "shortcuts.addToSelection": "Add or remove an object from multi-selection", + "shortcuts.bypassPlacementValidation": "Temporarily bypass placement validation constraints", + "shortcuts.camera": "Camera", + "shortcuts.cancelTool": "Cancel the active tool and return to Select mode", + "shortcuts.contextAware": "Shortcuts are context-aware and depend on the current phase or tool.", + "shortcuts.delete": "Delete", + "shortcuts.deleteOrBackspace": "Delete selected objects", + "shortcuts.deleteSelected": "Delete selected objects", + "shortcuts.disableAngleSnapping": "Temporarily disable angle snapping while drawing walls, slabs, and ceilings", + "shortcuts.dragMiddleMouseOrHoldSpace": "Drag with the middle mouse button, or hold Space while dragging with the left mouse button.", + "shortcuts.dragRightMouse": "Drag with the right mouse button.", + "shortcuts.drawingTools": "Drawing Tools", + "shortcuts.editorNavigation": "Editor Navigation", + "shortcuts.holdWhileDrawing": "Hold while drawing.", + "shortcuts.holdWhilePlacing": "Hold while placing.", + "shortcuts.itemPlacement": "Item Placement", + "shortcuts.keyboardShortcuts": "Keyboard Shortcuts", + "shortcuts.modesAndHistory": "Modes & History", + "shortcuts.orbitCamera": "Orbit camera", + "shortcuts.panCamera": "Pan camera", + "shortcuts.redo": "Redo", + "shortcuts.rotateItemClockwise": "Rotate item clockwise, or toggle selected door open/closed", + "shortcuts.rotateItemCounterClockwise": "Rotate item counter-clockwise, or close selected door", + "shortcuts.selectNextLevel": "Select next level in the active building", + "shortcuts.selectPreviousLevel": "Select previous level in the active building", + "shortcuts.selection": "Selection", + "shortcuts.switchToBuildMode": "Switch to Build mode", + "shortcuts.switchToFurnishLayer": "Switch to Furnish layer", + "shortcuts.switchToFurnishPhase": "Switch to Furnish phase", + "shortcuts.switchToSelectMode": "Switch to Select mode", + "shortcuts.switchToSitePhase": "Switch to Site phase", + "shortcuts.switchToStructureLayer": "Switch to Structure layer", + "shortcuts.switchToStructurePhase": "Switch to Structure phase", + "shortcuts.switchToZonesLayer": "Switch to Zones layer", + "shortcuts.toggleSidebar": "Toggle sidebar", + "shortcuts.undo": "Undo", + "shortcuts.worksWhileInSelectMode": "Works while in Select mode.", + "sidebar.items": "Items", + "sidebar.scene": "Scene", + "sidebar.site": "Site", + "site.addLevel": "Add level", + "site.area": "Area", + "site.furnish": "Furnish", + "site.noBuildingsYet": "No buildings yet", + "site.noLevelsYet": "No levels yet", + "site.perimeter": "Perimeter", + "site.structure": "Structure", + "site.zones": "Zones", + "snapshot.area": "Area", + "snapshot.capture": "Capture", + "snapshot.capturing": "Capturing", + "snapshot.closeCapture": "Close capture mode", + "snapshot.dragArea": "Drag the area you want to capture", + "snapshot.escToCancel": "Esc to cancel", + "snapshot.saved": "Saved", + "snapshot.standard": "Standard", + "snapshot.viewport": "Viewport", + "toolbar.cutaway": "Cutaway", + "toolbar.fullHeight": "Full Height", + "toolbar.low": "Low", + "toolbar.up": "Up", + "toolbar.walls": "Walls", + "tools.ceiling": "Ceiling", + "tools.column": "Column", + "tools.door": "Door", + "tools.elevator": "Elevator", + "tools.fence": "Fence", + "tools.gableRoof": "Gable Roof", + "tools.shelf": "Shelf", + "tools.slab": "Slab", + "tools.spawnPoint": "Spawn Point", + "tools.stairs": "Stairs", + "tools.wall": "Wall", + "tools.window": "Window", + "tools.zone": "Zone", + "treeActions.cameraSnapshot": "Camera snapshot", + "treeActions.clearSelection": "Clear selection", + "treeActions.clearSnapshot": "Clear snapshot", + "treeActions.hide": "Hide", + "treeActions.show": "Show", + "treeActions.takeSnapshot": "Take snapshot", + "treeActions.updateSnapshot": "Update snapshot", + "treeActions.viewSnapshot": "View snapshot", + "viewer.building": "Building", + "viewer.camera": "Camera", + "viewer.collapseSidebar": "Collapse sidebar", + "viewer.couldNotAddGuideImage": "Could not add that guide image.", + "viewer.crispEdges": "Crisp, opaque edge lines", + "viewer.cutaway": "Cutaway", + "viewer.deleteGuideImage": "Delete guide image", + "viewer.deleteScan": "Delete scan", + "viewer.edges": "Edges", + "viewer.expandSidebar": "Expand sidebar", + "viewer.exploded": "Exploded", + "viewer.faintOutline": "Faint outline of major creases", + "viewer.fileTooLarge": "File is too large. Maximum size is 200 MB.", + "viewer.flatAndFast": "Flat and fast — no ambient occlusion", + "viewer.fullAO": "Full ambient occlusion", + "viewer.fullHeight": "Full Height", + "viewer.grid": "Grid", + "viewer.gridSnap": "Grid snap", + "viewer.guideImageSettings": "Guide image settings", + "viewer.guideImagesOnThisLevel": "{count} guide image on this level", + "viewer.guideImagesOnThisLevel_plural": "{count} guide images on this level", + "viewer.guides": "Guides", + "viewer.hidden": "Hidden", + "viewer.hideReferenceFloor": "Hide reference floor", + "viewer.imperial": "Imperial (ft)", + "viewer.levels": "Levels", + "viewer.low": "Low", + "viewer.manual": "Manual", + "viewer.metric": "Metric (m)", + "viewer.noEdgeLines": "No edge lines", + "viewer.noGuideImagesOnLevel": "No guide images on this level yet.", + "viewer.noScansOnLevel": "No scans on this level yet.", + "viewer.off": "Off", + "viewer.off_edge": "Off", + "viewer.on": "On", + "viewer.opacity": "Opacity", + "viewer.openProjectBeforeUpload": "Open a project before uploading a scan.", + "viewer.orbitLeft": "Orbit Left", + "viewer.orbitRight": "Orbit Right", + "viewer.orthographic": "Orthographic", + "viewer.perspective": "Perspective", + "viewer.referenceFloorSettings": "Reference floor settings", + "viewer.render": "Render", + "viewer.rendered": "Rendered", + "viewer.scanSettings": "Scan settings", + "viewer.scanUploadUnavailable": "Scan upload is unavailable.", + "viewer.scans": "Scans", + "viewer.scansOnThisLevel": "{count} scan on this level", + "viewer.scansOnThisLevel_plural": "{count} scans on this level", + "viewer.sceneTheme": "Scene theme", + "viewer.shadows": "Shadows", + "viewer.showReferenceFloor": "Show reference floor", + "viewer.soft": "Soft", + "viewer.solid": "Solid", + "viewer.solo": "Solo", + "viewer.stack": "Stack", + "viewer.strong": "Strong", + "viewer.topView": "Top View", + "viewer.uploadGlbOrImage": "Upload a .glb/.gltf scan or an image.", + "viewer.uploadScanOrGuide": "Upload scan or guide image", + "viewer.uploading": "Uploading", + "viewer.viewMode2D": "2D", + "viewer.viewMode3D": "3D", + "viewer.viewModeSplit": "Split", + "viewer.visible": "Visible", + "viewer.walkthrough": "Walkthrough", + "zone.addOne": "Add one", + "zone.noZonesOnLevel": "No zones on this level.", + "zone.selectLevelToView": "Select a level to view and create zones", + "nodes.ceiling.toolHints.trace": "Trace ceiling outline", + "nodes.ceiling.toolHints.finish": "Finish ceiling", + "nodes.item.label": "Item", + "nodes.item.description": "An item placed in the scene.", + "nodes.site.label": "Site", + "nodes.site.description": "A site containing buildings.", + "nodes.dormer.toolHints.place": "Place dormer on roof", + "nodes.chimney.toolHints.place": "Place chimney on roof", + "nodes.ridgeVent.toolHints.place": "Place ridge vent on roof", + "nodes.shelf.toolHints.place": "Place shelf", + "nodes.skylight.toolHints.place": "Place skylight on roof", + "nodes.spawn.toolHints.place": "Place spawn point", + "nodes.zone.toolHints.place": "Place zone" +} \ No newline at end of file diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json new file mode 100644 index 0000000000..1a94668fdf --- /dev/null +++ b/packages/editor/src/lib/i18n/zh.json @@ -0,0 +1,861 @@ +{ + "catalog.intensity": "强度", + "commands.addLevel": "添加楼层", + "commands.back": "返回", + "commands.ceilingTool": "天花板工具", + "commands.close": "关闭", + "commands.commandPalette": "命令面板", + "commands.copyShareLink": "复制分享链接", + "commands.cutaway": "剖面", + "commands.deleteLevel": "删除楼层", + "commands.deleteSelection": "删除选中", + "commands.doorTool": "门工具", + "commands.down": "下", + "commands.enterPreview": "进入预览", + "commands.exitPreview": "退出预览", + "commands.exploded": "爆炸", + "commands.exportGLB": "导出3D模型 (GLB)", + "commands.exportJSON": "导出场景 (JSON)", + "commands.filterOptions": "筛选选项…", + "commands.gotoLevel": "跳转楼层", + "commands.group.exportShare": "导出与分享", + "commands.group.history": "历史", + "commands.group.levels": "楼层", + "commands.group.scene": "场景", + "commands.group.view": "视图", + "commands.group.viewerControls": "查看器控制", + "commands.itemTool": "物品工具", + "commands.levelMode": "楼层模式", + "commands.manual": "手动", + "commands.materialPaint": "材质绘制", + "commands.navigate": "导航", + "commands.noCommandsFound": "未找到命令。", + "commands.redo": "重做", + "commands.renameLevel": "重命名楼层", + "commands.searchActions": "搜索操作…", + "commands.select": "选择", + "commands.slabTool": "楼板工具", + "commands.solo": "单独", + "commands.stacked": "堆叠", + "commands.stairTool": "楼梯工具", + "commands.switchTo": "切换到", + "commands.switchToRendered": "切换到渲染", + "commands.switchToSolid": "切换到纯色", + "commands.takeScreenshot": "截图", + "commands.takeSnapshot": "快照", + "commands.toggleFullscreen": "切换全屏", + "commands.typeNewName": "输入新名称…", + "commands.typeNewNameAbove": "在上方输入新名称…", + "commands.undo": "撤销", + "commands.up": "上", + "commands.wallMode": "墙模式", + "commands.wallTool": "墙工具", + "commands.windowTool": "窗工具", + "commands.zoneTool": "区域工具", + "common.actions": "操作", + "common.add": "添加", + "common.addOne": "添加一个", + "common.all": "全部", + "common.apply": "应用", + "common.back": "返回", + "common.cancel": "取消", + "common.close": "关闭", + "common.confirm": "确认", + "common.copy": "复制", + "common.corners.bottomLeft": "左下", + "common.corners.bottomRight": "右下", + "common.corners.topLeft": "左上", + "common.corners.topRight": "右上", + "common.cut": "剪切", + "common.delete": "删除", + "common.depth": "深度", + "common.deselect": "取消选择", + "common.dimensions": "尺寸", + "common.directions.down": "下", + "common.directions.up": "上", + "common.done": "完成", + "common.duplicate": "复制", + "common.edit": "编辑", + "common.error": "错误", + "common.height": "高度", + "common.hide": "隐藏", + "common.home": "首页", + "common.length": "长度", + "common.loading": "加载中...", + "common.move": "移动", + "common.next": "下一步", + "common.noResults": "未找到结果", + "common.objectsSelected": "个对象已选择", + "common.open": "打开", + "common.paste": "粘贴", + "common.position": "位置", + "common.redo": "重做", + "common.remove": "移除", + "common.reset": "重置", + "common.rotate": "旋转", + "common.save": "保存", + "common.search": "搜索", + "common.select": "选择", + "common.settings": "设置", + "common.show": "显示", + "common.success": "成功", + "common.thickness": "厚度", + "common.undo": "撤销", + "common.untitled": "未命名", + "common.width": "宽度", + "controlModes.boxSelect": "框选", + "controlModes.build": "构建", + "controlModes.editSite": "编辑场地", + "controlModes.exitSiteEditing": "退出场地编辑", + "controlModes.furnish": "家具", + "controlModes.materialPaint": "材质绘制", + "controlModes.preview": "预览", + "controlModes.select": "选择", + "controlModes.siteEditGroundOnly": "场地编辑仅在地面层可用", + "controlModes.zone": "区域", + "editor.cameraPan": "平移", + "editor.cameraRotate": "旋转", + "editor.cameraZoom": "缩放", + "editor.createNew": "创建新场景", + "editor.dismiss": "关闭", + "editor.leftClick": "左键点击", + "editor.localWarning": "本地编辑器 — 场景不会保存。", + "editor.middleClick": "中键点击", + "editor.openRecent": "打开最近场景", + "editor.rightClick": "右键点击", + "editor.scrollWheel": "滚轮", + "editor.space": "空格", + "furnishTools.appliance": "家电", + "furnishTools.bathroom": "浴室", + "furnishTools.furniture": "家具", + "furnishTools.kitchen": "厨房", + "furnishTools.outdoor": "户外", + "itemHelper.freePlace": "自由放置", + "itemHelper.placeItem": "放置物品", + "itemHelper.rotateClockwise": "顺时针旋转", + "itemHelper.rotateCounterclockwise": "逆时针旋转", + "items.all": "全部", + "items.community": "社区", + "items.library": "库", + "items.mine": "我的", + "items.noResults": "未找到\"{search}\"的结果", + "items.search": "搜索...", + "level.addLevelAbove": "在上方添加楼层", + "level.addLevelBelow": "在下方添加楼层", + "level.basement": "地下 {n} 层", + "level.cancel": "取消", + "level.cannotDeleteGround": "底层无法删除", + "level.confirmDelete": "确定要删除 {name} 吗?此楼层的所有墙体、地板和物体将被永久删除。", + "level.delete": "删除", + "level.deleteLevel": "删除楼层", + "level.deleteLevelTitle": "删除楼层", + "level.dragToReorder": "拖动重新排序", + "level.duplicateLevel": "复制楼层", + "level.duplicateOptions": "带选项复制...", + "level.floor": "第 {n} 层", + "level.groundFloor": "底层", + "level.insertLevelHere": "在此处插入楼层", + "level.noElementsOnLevel": "此楼层上没有元素", + "level.noZonesOnLevel": "此楼层上没有区域。", + "level.pasteCopied": "粘贴已复制内容", + "level.reorder": "重新排序", + "level.selectLevelToView": "选择楼层以查看内容", + "levelDuplicate.cancel": "取消", + "levelDuplicate.chooseWhatToCopy": "选择要从 {level} 复制的内容。", + "levelDuplicate.duplicate": "复制", + "levelDuplicate.duplicateLevel": "复制楼层", + "levelDuplicate.everything": "全部", + "levelDuplicate.everythingDesc": "结构、材料、家具和参考。", + "levelDuplicate.structure": "仅结构", + "levelDuplicate.structureDesc": "墙壁、楼板、屋顶、楼梯、窗户和门,无饰面。", + "levelDuplicate.structureFurniture": "结构+家具", + "levelDuplicate.structureFurnitureDesc": "结构、饰面和放置的物品,无参考图。", + "levelDuplicate.structureMaterials": "结构+材料", + "levelDuplicate.structureMaterialsDesc": "带当前材料和饰面分配的结构。", + "loadBuild.buildings": "建筑", + "loadBuild.cannotImport": "无法导入此文件", + "loadBuild.ceilings": "天花板", + "loadBuild.doors": "门", + "loadBuild.errors": "{count} 个错误", + "loadBuild.floorArea": "地板面积", + "loadBuild.hide": "隐藏", + "loadBuild.items": "物品", + "loadBuild.levels": "楼层", + "loadBuild.noRecognisedNodes": "该文件不包含可识别的节点。", + "loadBuild.readyToImport": "准备导入", + "loadBuild.replaceScene": "替换当前场景", + "loadBuild.scans": "扫描", + "loadBuild.schemaDetails": "架构详情({count} 个节点)", + "loadBuild.showMore": "显示另外 {count} 个", + "loadBuild.sites": "场地", + "loadBuild.slabs": "楼板", + "loadBuild.structure": "结构", + "loadBuild.walls": "墙体", + "loadBuild.warnings": "{count} 个警告", + "loadBuild.windows": "窗", + "loadBuild.zones": "区域", + "nav.home": "首页", + "nav.privacy": "隐私", + "nav.scenes": "场景", + "nav.terms": "条款", + "nodeActions.curve": "曲线", + "nodeActions.cutOut": "切除", + "nodeActions.delete": "删除", + "nodeActions.duplicate": "复制", + "nodeActions.move": "移动", + "nodeTypes.boxVent": "箱式通风口", + "nodeTypes.building": "建筑", + "nodeTypes.ceiling": "天花板", + "nodeTypes.ceilingWithArea": "天花板 ({area}m²)", + "nodeTypes.chimney": "烟囱", + "nodeTypes.column": "柱子", + "nodeTypes.door": "门", + "nodeTypes.dormer": "老虎窗", + "nodeTypes.elevator": "电梯", + "nodeTypes.fence": "围栏", + "nodeTypes.flat": "平顶", + "nodeTypes.flight": "梯段", + "nodeTypes.gable": "山墙", + "nodeTypes.guide": "参考图", + "nodeTypes.hip": "四坡", + "nodeTypes.item": "物品", + "nodeTypes.landing": "平台", + "nodeTypes.level": "楼层", + "nodeTypes.ridgeVent": "屋脊通风口", + "nodeTypes.roof": "屋顶", + "nodeTypes.roofSegment": "屋顶段", + "nodeTypes.roofSegmentWithDims": "{type} (宽{width}×深{depth}米)", + "nodeTypes.roofWithSegments": "屋顶 ({count} 段)", + "nodeTypes.scan": "扫描", + "nodeTypes.selection": "选择", + "nodeTypes.shed": "棚式", + "nodeTypes.shelf": "架子", + "nodeTypes.site": "场地", + "nodeTypes.skyLight": "天窗", + "nodeTypes.slab": "楼板", + "nodeTypes.slabWithArea": "楼板 ({area}m²)", + "nodeTypes.solarPanel": "太阳能板", + "nodeTypes.spawn": "出生点", + "nodeTypes.stairSegment": "楼梯段", + "nodeTypes.stairSegmentWithDims": "{type} (宽{width}×深{depth}米)", + "nodeTypes.staircaseWithSegments": "楼梯 ({count} 段)", + "nodeTypes.stairs": "楼梯", + "nodeTypes.wall": "墙", + "nodeTypes.window": "窗", + "nodeTypes.zone": "区域", + "nodeTypes.zoneWithArea": "区域 ({area}m²)", + "nodes.boxVent.baseHeight": "底座高度", + "nodes.boxVent.baseInset": "底座内嵌", + "nodes.boxVent.box": "箱", + "nodes.boxVent.cap": "帽", + "nodes.boxVent.capHeight": "帽高", + "nodes.boxVent.cornerBevel": "角部斜角", + "nodes.boxVent.dome": "穹顶", + "nodes.boxVent.domeCurvature": "穹顶曲率", + "nodes.boxVent.gapHeight": "间隙高度", + "nodes.boxVent.hoodOverhang": "罩悬挑", + "nodes.boxVent.topTaper": "顶部锥度", + "nodes.building.description": "包含一个或多个楼层的建筑容器。", + "nodes.building.label": "建筑", + "nodes.ceiling.addHole": "添加切口", + "nodes.ceiling.auto": "自动", + "nodes.ceiling.autoHoleLabel.elevator": "自动电梯切口", + "nodes.ceiling.autoHoleLabel.stair": "自动楼梯切口", + "nodes.ceiling.heightPresets.high": "高 (3.0m)", + "nodes.ceiling.heightPresets.low": "低 (2.4m)", + "nodes.ceiling.heightPresets.standard": "标准 (2.5m)", + "nodes.ceiling.holeLabel": "切口 {index}", + "nodes.ceiling.manual": "手动", + "nodes.ceiling.noHoles": "无切口", + "nodes.chimney.aboveRidge": "高过屋脊", + "nodes.chimney.back": "后", + "nodes.chimney.bandExtent": "范围", + "nodes.chimney.bandOffset": "偏移", + "nodes.chimney.bandThickness": "厚度", + "nodes.chimney.bands": "环形带", + "nodes.chimney.cap": "帽", + "nodes.chimney.capThickness": "厚度", + "nodes.chimney.corbeled": "悬砌", + "nodes.chimney.cornerBevel": "角部斜角", + "nodes.chimney.count": "数量", + "nodes.chimney.cricket": "斜撑", + "nodes.chimney.cricketHeight": "高度", + "nodes.chimney.cricketLength": "长度", + "nodes.chimney.cutoutOffset": "切口偏移", + "nodes.chimney.diameter": "直径", + "nodes.chimney.double": "双个", + "nodes.chimney.flat": "平顶", + "nodes.chimney.flueDiameter": "直径", + "nodes.chimney.flueHeight": "高度", + "nodes.chimney.flueShape": "形状", + "nodes.chimney.flueSpacing": "间距", + "nodes.chimney.flueWallThickness": "壁厚", + "nodes.chimney.flues": "烟道", + "nodes.chimney.front": "前", + "nodes.chimney.hollowDepth": "空心深度", + "nodes.chimney.none": "无", + "nodes.chimney.overhang": "悬挑", + "nodes.chimney.panelDepth": "深度", + "nodes.chimney.panelHeight": "高度", + "nodes.chimney.panelMargin": "侧边距", + "nodes.chimney.panelOffsetTop": "顶部偏移", + "nodes.chimney.panels": "面板", + "nodes.chimney.rectangular": "矩形", + "nodes.chimney.round": "圆形", + "nodes.chimney.shoulder": "肩部", + "nodes.chimney.shoulderExtent": "范围", + "nodes.chimney.shoulderHeight": "高度", + "nodes.chimney.simple": "简单", + "nodes.chimney.single": "单个", + "nodes.chimney.sloped": "斜面", + "nodes.chimney.square": "方形", + "nodes.chimney.stepped": "阶梯式", + "nodes.chimney.tapered": "渐缩", + "nodes.column.aFrame": "A型框架", + "nodes.column.applyPreset": "应用预设...", + "nodes.column.applyProportion": "应用比例...", + "nodes.column.bottomDepth": "底部深度", + "nodes.column.bottomHeight": "底部高度", + "nodes.column.bottomSpread": "底部展开", + "nodes.column.bottomStepSpread": "底部台阶展开", + "nodes.column.bottomTiers": "底层层数", + "nodes.column.bottomWidth": "底部宽度", + "nodes.column.boxFrame": "箱式框架", + "nodes.column.braceDepth": "支撑深度", + "nodes.column.braceWidth": "支撑宽度", + "nodes.column.bulge": "鼓度", + "nodes.column.bulged": "鼓形", + "nodes.column.connectorPlates": "连接板", + "nodes.column.edgeSoftness": "边缘柔和度", + "nodes.column.endWidth": "端部宽度", + "nodes.column.forkSpread": "分叉展开", + "nodes.column.hourglass": "沙漏形", + "nodes.column.kBrace": "K型支撑", + "nodes.column.neckWidth": "颈部宽度", + "nodes.column.noBottom": "无底", + "nodes.column.noTop": "无顶", + "nodes.column.plinthThickness": "底座厚度", + "nodes.column.portalFrame": "门式框架", + "nodes.column.rectangular": "矩形", + "nodes.column.ringPairs": "环配对数", + "nodes.column.ringSpread": "环展开", + "nodes.column.ringThickness": "环厚度", + "nodes.column.round": "圆形", + "nodes.column.roundBandWidth": "圆带宽", + "nodes.column.roundRings": "圆底", + "nodes.column.roundedTop": "圆顶", + "nodes.column.segmentTwist": "分段扭曲", + "nodes.column.shaftCornerRadius": "柱身圆角半径", + "nodes.column.shaftWidth": "柱身宽度", + "nodes.column.simpleBlockBottom": "简单块体底座", + "nodes.column.simpleTop": "简单顶", + "nodes.column.singleStrut": "单支撑", + "nodes.column.square": "方形", + "nodes.column.squarePlinthBottom": "方形底座底", + "nodes.column.steppedBottom": "阶梯式底座", + "nodes.column.steppedTop": "阶梯顶", + "nodes.column.straight": "直柱", + "nodes.column.taper": "锥度", + "nodes.column.tapered": "渐缩", + "nodes.column.topDepth": "顶部深度", + "nodes.column.topHeight": "顶部高度", + "nodes.column.topSpread": "顶部展开", + "nodes.column.topStepSpread": "顶部台阶展开", + "nodes.column.topTiers": "顶层数", + "nodes.column.topWidth": "顶部宽度", + "nodes.column.trestle": "支架", + "nodes.column.tripod": "三脚架", + "nodes.column.twistSegments": "扭曲段数", + "nodes.column.vFrame": "V型支撑", + "nodes.column.vertical": "垂直", + "nodes.column.waist": "腰部", + "nodes.column.xBrace": "X型支撑", + "nodes.column.yFrame": "Y型支撑", + "nodes.column.yaw": "偏航", + "nodes.door.addSegment": "+ 添加分段", + "nodes.door.archHeight": "拱高", + "nodes.door.columnLabel": "C{i}", + "nodes.door.columns": "列数", + "nodes.door.contentPaddingSection": "内容边距", + "nodes.door.corners.topLeft": "左上", + "nodes.door.corners.topRight": "右上", + "nodes.door.defaultName": "门 {count}", + "nodes.door.description": "墙体上的门,可动画开关。", + "nodes.door.direction": "方向", + "nodes.door.divider": "分隔条", + "nodes.door.doorCloser": "闭门器", + "nodes.door.doorTypeOptions.barn": "谷仓", + "nodes.door.doorTypeOptions.double": "双扇", + "nodes.door.doorTypeOptions.folding": "折叠", + "nodes.door.doorTypeOptions.french": "法式", + "nodes.door.doorTypeOptions.hinged": "铰链", + "nodes.door.doorTypeOptions.pocket": "隐藏", + "nodes.door.doorTypeOptions.rollup": "卷起", + "nodes.door.doorTypeOptions.sectional": "卷帘", + "nodes.door.doorTypeOptions.sliding": "推拉", + "nodes.door.doorTypeOptions.tiltup": "上翻", + "nodes.door.enableHandle": "启用把手", + "nodes.door.enableThreshold": "启用门槛", + "nodes.door.fallbackTitle": "门", + "nodes.door.flipSide": "翻转侧面", + "nodes.door.fold": "折叠", + "nodes.door.frameSection": "框架", + "nodes.door.handleSection": "把手", + "nodes.door.handleSide": "把手侧", + "nodes.door.hardwareSection": "五金", + "nodes.door.hingesSide": "铰链侧", + "nodes.door.horizontalPadding": "水平", + "nodes.door.inset": "内嵌", + "nodes.door.label": "门", + "nodes.door.open": "打开", + "nodes.door.openingShapeOptions.arch": "拱形", + "nodes.door.openingShapeOptions.rect": "矩形", + "nodes.door.openingShapeOptions.rounded": "圆角", + "nodes.door.openingShapeSection": "开口形状", + "nodes.door.panels": "门扇", + "nodes.door.panicBar": "逃生杆", + "nodes.door.panicBarHeight": "杆高度", + "nodes.door.radiusModeOptions.all": "全部", + "nodes.door.radiusModeOptions.individual": "单独", + "nodes.door.remove": "- 移除", + "nodes.door.revealRadius": "嵌槽半径", + "nodes.door.segmentLabel": "分段 {i}", + "nodes.door.segmentTypeOptions.empty": "空", + "nodes.door.segmentTypeOptions.glass": "玻璃", + "nodes.door.segmentTypeOptions.panel": "面板", + "nodes.door.segmentsSection": "分段", + "nodes.door.slide": "推拉", + "nodes.door.slideDirectionOptions.panel": "面板式", + "nodes.door.slideDirectionOptions.pocket": "隐藏式", + "nodes.door.slideDirectionOptions.rail": "轨道式", + "nodes.door.swingDirectionOptions.inward": "向内", + "nodes.door.swingDirectionOptions.outward": "向外", + "nodes.door.swingSection": "开启", + "nodes.door.thresholdSection": "门槛", + "nodes.door.toolHints.cancel": "取消", + "nodes.door.toolHints.place": "在墙体上放置门", + "nodes.door.topShapeOptions.arch": "拱形", + "nodes.door.topShapeOptions.rect": "矩形", + "nodes.door.topShapeOptions.rounded": "圆角", + "nodes.door.topShapeSection": "顶部形状", + "nodes.door.type": "类型", + "nodes.door.typeOptions.door": "门", + "nodes.door.typeOptions.garage": "车库门", + "nodes.door.typeOptions.opening": "开口", + "nodes.door.verticalPadding": "垂直", + "nodes.dormer.all": "全部", + "nodes.dormer.arch": "拱形", + "nodes.dormer.archHeight": "拱高", + "nodes.dormer.bottomLeft": "左下", + "nodes.dormer.bottomRight": "右下", + "nodes.dormer.columns": "列数", + "nodes.dormer.cornerRadius": "圆角半径", + "nodes.dormer.depth": "深度", + "nodes.dormer.divider": "分隔条", + "nodes.dormer.dormer": "老虎窗", + "nodes.dormer.dutch": "荷兰式", + "nodes.dormer.enableSill": "启用窗台", + "nodes.dormer.flat": "平顶", + "nodes.dormer.frameDepth": "深度", + "nodes.dormer.frameThickness": "厚度", + "nodes.dormer.gable": "山墙", + "nodes.dormer.gambrel": "双坡", + "nodes.dormer.hip": "四坡", + "nodes.dormer.hungWall": "悬墙", + "nodes.dormer.individual": "单独", + "nodes.dormer.mansard": "曼萨尔式", + "nodes.dormer.rect": "矩形", + "nodes.dormer.roofHeight": "屋顶高度", + "nodes.dormer.rounded": "圆角", + "nodes.dormer.rows": "行数", + "nodes.dormer.section": "截面", + "nodes.dormer.shed": "棚式", + "nodes.dormer.sillDepth": "窗台深度", + "nodes.dormer.sillThickness": "窗台厚度", + "nodes.dormer.topLeft": "左上", + "nodes.dormer.topRight": "右上", + "nodes.dormer.wallHeight": "壁高", + "nodes.dormer.width": "宽度", + "nodes.dormer.window": "窗户", + "nodes.dormer.windowHeight": "高度", + "nodes.dormer.windowOffsetX": "X偏移", + "nodes.dormer.windowOffsetY": "Y偏移", + "nodes.dormer.windowWidth": "宽度", + "nodes.elevator.cabHeight": "轿厢高度", + "nodes.elevator.cabWidth": "宽度", + "nodes.elevator.centerOpening": "中部开口", + "nodes.elevator.defaultFloor": "默认楼层", + "nodes.elevator.disabled": "禁用", + "nodes.elevator.doorHeight": "门高", + "nodes.elevator.doorTime": "门时间", + "nodes.elevator.doorType": "门类型", + "nodes.elevator.doorWidth": "门宽", + "nodes.elevator.dwell": "停留", + "nodes.elevator.from": "从", + "nodes.elevator.glassFrame": "玻璃框", + "nodes.elevator.openingStyle": "开口样式", + "nodes.elevator.segmentedPanel": "分段面板", + "nodes.elevator.service": "服务", + "nodes.elevator.shaftDepth": "井道深度", + "nodes.elevator.shaftStyle": "井道样式", + "nodes.elevator.shaftWidth": "井道宽度", + "nodes.elevator.singleLeft": "单左", + "nodes.elevator.singleRight": "单右", + "nodes.elevator.solidPanel": "实心面板", + "nodes.elevator.speed": "速度", + "nodes.elevator.to": "到", + "nodes.elevator.wallThickness": "壁厚", + "nodes.level.description": "建筑的单个楼层,包含墙体/楼板/天花板/物品。", + "nodes.level.label": "楼层", + "nodes.roof.addChimney": "添加烟囱", + "nodes.roof.addDormer": "添加老虎窗", + "nodes.roof.addSegment": "添加分段", + "nodes.roof.addSkylight": "添加天窗", + "nodes.roof.addSolarPanel": "添加太阳能板", + "nodes.roof.addVent": "添加通风口", + "nodes.roof.box": "箱形", + "nodes.roof.ridge": "屋脊", + "nodes.slab.addHole": "添加切口", + "nodes.slab.area": "面积", + "nodes.slab.auto": "自动", + "nodes.slab.autoHoleLabel.elevator": "自动电梯切口", + "nodes.slab.autoHoleLabel.stair": "自动楼梯切口", + "nodes.slab.description": "承载物品的多边形地板表面。", + "nodes.slab.elevationPresets.ground": "地面 (0m)", + "nodes.slab.elevationPresets.raised": "抬起 (+5cm)", + "nodes.slab.elevationPresets.step": "台阶 (+15cm)", + "nodes.slab.elevationPresets.sunken": "下沉 (-15cm)", + "nodes.slab.fallbackTitle": "楼板", + "nodes.slab.holeLabel": "切口 {index}", + "nodes.slab.label": "楼板", + "nodes.slab.manual": "手动", + "nodes.slab.noHoles": "无切口", + "nodes.slab.toolHints.cancel": "取消", + "nodes.slab.toolHints.finish": "完成楼板", + "nodes.slab.toolHints.trace": "绘制楼板轮廓", + "nodes.solarPanel.columns": "列数", + "nodes.solarPanel.flipOrientation": "翻转方向", + "nodes.solarPanel.flush": "平齐", + "nodes.solarPanel.frameDepth": "框架深度", + "nodes.solarPanel.frameThickness": "框架厚度", + "nodes.solarPanel.gapX": "水平间距", + "nodes.solarPanel.gapY": "垂直间距", + "nodes.solarPanel.rows": "行数", + "nodes.solarPanel.standoff": "支架", + "nodes.solarPanel.tiltAngle": "倾斜角度", + "nodes.solarPanel.tilted": "倾斜", + "nodes.stair.addFlight": "添加梯段", + "nodes.stair.addLanding": "添加平台", + "nodes.stair.autoCutout": "自动切口", + "nodes.stair.both": "双侧", + "nodes.stair.centerColumn": "中心柱", + "nodes.stair.curved": "弧形梯", + "nodes.stair.destination": "目标", + "nodes.stair.fitToFloor": "适应楼层", + "nodes.stair.fromLevel": "起始楼层", + "nodes.stair.geometry": "几何", + "nodes.stair.innerRadius": "内半径", + "nodes.stair.integrated": "整体", + "nodes.stair.left": "左侧", + "nodes.stair.none": "无", + "nodes.stair.opening": "开口", + "nodes.stair.openingOffset": "开口偏移", + "nodes.stair.position": "位置", + "nodes.stair.railing": "栏杆", + "nodes.stair.railingHeight": "高度", + "nodes.stair.right": "右侧", + "nodes.stair.rotationPresets.negative": "-45°", + "nodes.stair.rotationPresets.positive": "+45°", + "nodes.stair.segment": "分段", + "nodes.stair.segments": "分段", + "nodes.stair.spiral": "螺旋梯", + "nodes.stair.stepSupports": "踏步支撑", + "nodes.stair.straight": "直梯", + "nodes.stair.sweep": "旋转角度", + "nodes.stair.toLevel": "目标楼层", + "nodes.stair.topLanding": "顶部平台", + "nodes.stair.type": "类型", + "nodes.wall.curve": "弧度", + "nodes.wall.description": "直墙或弧墙。承载门、窗和壁挂物品。", + "nodes.wall.draftName": "草稿墙体", + "nodes.wall.fallbackTitle": "墙体", + "nodes.wall.label": "墙体", + "nodes.wall.length": "长度", + "nodes.wall.toolHints.allowAngles": "允许非45°角", + "nodes.wall.toolHints.cancel": "取消", + "nodes.wall.toolHints.setStartEnd": "设置墙体起点/终点", + "nodes.window.archHeight": "拱高", + "nodes.window.casementStyleOptions.french": "法式", + "nodes.window.casementStyleOptions.single": "单扇", + "nodes.window.colWidths": "列宽", + "nodes.window.columns": "列数", + "nodes.window.cornerRadius": "圆角半径", + "nodes.window.corners.bottomLeft": "左下", + "nodes.window.corners.bottomRight": "右下", + "nodes.window.corners.topLeft": "左上", + "nodes.window.corners.topRight": "右上", + "nodes.window.defaultName": "窗户 {count}", + "nodes.window.depth": "深度", + "nodes.window.description": "墙体上的窗户,可动画开关。", + "nodes.window.divider": "分隔条", + "nodes.window.enableSill": "启用窗台", + "nodes.window.fallbackTitle": "窗户", + "nodes.window.flipSide": "翻转侧面", + "nodes.window.frame": "框架", + "nodes.window.grid": "网格", + "nodes.window.label": "窗户", + "nodes.window.openingShape": "开口形状", + "nodes.window.openingShapeOptions.arch": "拱形", + "nodes.window.openingShapeOptions.rect": "矩形", + "nodes.window.openingShapeOptions.rounded": "圆角", + "nodes.window.operationLabels.slide": "推拉", + "nodes.window.radiusModeOptions.all": "全部", + "nodes.window.radiusModeOptions.individual": "单独", + "nodes.window.revealRadius": "嵌槽半径", + "nodes.window.rowHeights": "行高", + "nodes.window.rowLabel": "R{i}", + "nodes.window.rows": "行数", + "nodes.window.sill": "窗台", + "nodes.window.sillDepth": "窗台深度", + "nodes.window.sillThickness": "窗台厚度", + "nodes.window.thickness": "厚度", + "nodes.window.toolHints.cancel": "取消", + "nodes.window.toolHints.place": "在墙体上放置窗户", + "nodes.window.topShape": "顶部形状", + "nodes.window.topShapeOptions.arch": "拱形", + "nodes.window.topShapeOptions.rect": "矩形", + "nodes.window.topShapeOptions.rounded": "圆角", + "nodes.window.type": "类型", + "nodes.window.typeOptions.awning": "上悬", + "nodes.window.typeOptions.bay": "飘窗", + "nodes.window.typeOptions.bow": "圆弧窗", + "nodes.window.typeOptions.casement": "平开", + "nodes.window.typeOptions.doubleHung": "双悬", + "nodes.window.typeOptions.fixed": "固定", + "nodes.window.typeOptions.louvered": "百叶", + "nodes.window.typeOptions.opening": "开口", + "nodes.window.typeOptions.singleHung": "单悬", + "nodes.window.typeOptions.sliding": "推拉", + "nodes.window.typeOptions.window": "窗户", + "nodes.window.windowType": "窗户类型", + "panel.deleteLevel": "删除楼层", + "panel.duplicateLevel": "复制楼层", + "panel.editScale": "编辑比例", + "panel.hideScale": "隐藏比例", + "panel.intensity": "强度", + "panel.noEdgeLines": "无边缘线", + "panel.scaleAndOpacity": "比例与不透明度", + "panel.setScale": "设置比例", + "panel.showScale": "显示比例", + "scenes.allScenes": "所有场景", + "scenes.noScenes": "还没有场景。创建一个开始吧。", + "scenes.noThumbnail": "无缩略图", + "scenes.nodes": "{count} 个节点", + "scenes.sceneCount": "{count} 个场景", + "scenes.title": "您的场景", + "settings.adjustVolume": "调整音量级别和静音设置", + "settings.audio": "音频", + "settings.audioSettings": "音频设置", + "settings.clearAndStartNew": "清除并重新开始", + "settings.dangerZone": "危险区域", + "settings.exploreSceneGraph": "探索场景图", + "settings.export": "导出", + "settings.exportedGLB": "导出GLB", + "settings.exportedOBJ": "导出OBJ", + "settings.exportedSTL": "导出STL", + "settings.exporting": "生成中...", + "settings.generateThumbnail": "生成缩略图", + "settings.keyboard": "键盘", + "settings.loadBuild": "加载构建", + "settings.masterVolume": "主音量", + "settings.muteAllSounds": "静音所有声音", + "settings.public": "公开", + "settings.radioVolume": "收音机音量", + "settings.saveAndLoad": "保存与加载", + "settings.saveBuild": "保存构建", + "settings.sceneGraph": "场景图", + "settings.shadows": "阴影", + "settings.show3DScans": "显示3D扫描", + "settings.showFloorplans": "显示平面图", + "settings.showGrid": "显示网格", + "settings.soundEffects": "音效", + "settings.thumbnail": "缩略图", + "settings.unmuteAllSounds": "取消静音所有声音", + "settings.visibility": "可见性", + "settings.visibilityAnyone": "任何人", + "settings.visibilityCanView": "可以查看", + "settings.visibilityCastShadows": "从灯光投射阴影", + "settings.visibilityEditorOnly": "仅在编辑器中可见", + "settings.visibilityOnlyYou": "仅你", + "settings.visibilityPublic": "公开", + "settings.visibilityShow3DScans": "显示3D扫描", + "settings.visibilityShowFloorplans": "显示平面图", + "settings.visibilityShowGrid": "显示网格", + "settings.visibilityVisiblePublic": "对公开查看者可见", + "shortcuts.addOrRemoveFromSelection": "从多选中添加或移除对象", + "shortcuts.addToSelection": "添加或移除多选中的对象", + "shortcuts.bypassPlacementValidation": "暂时绕过放置验证约束", + "shortcuts.camera": "相机", + "shortcuts.cancelTool": "取消当前工具并返回选择模式", + "shortcuts.contextAware": "快捷键是上下文感知的,取决于当前阶段或工具。", + "shortcuts.delete": "删除", + "shortcuts.deleteOrBackspace": "删除选中对象", + "shortcuts.deleteSelected": "删除选中对象", + "shortcuts.disableAngleSnapping": "绘制墙、楼板和天花板时暂时禁用角度吸附", + "shortcuts.dragMiddleMouseOrHoldSpace": "按住中键拖动,或按住空格键同时左键拖动。", + "shortcuts.dragRightMouse": "右键拖动。", + "shortcuts.drawingTools": "绘制工具", + "shortcuts.editorNavigation": "编辑器导航", + "shortcuts.holdWhileDrawing": "绘制时按住。", + "shortcuts.holdWhilePlacing": "放置时按住。", + "shortcuts.itemPlacement": "物品放置", + "shortcuts.keyboardShortcuts": "键盘快捷键", + "shortcuts.modesAndHistory": "模式与历史", + "shortcuts.orbitCamera": "旋转相机", + "shortcuts.panCamera": "平移相机", + "shortcuts.redo": "重做", + "shortcuts.rotateItemClockwise": "顺时针旋转物品,或切换选中门的开关", + "shortcuts.rotateItemCounterClockwise": "逆时针旋转物品,或关闭选中的门", + "shortcuts.selectNextLevel": "选择当前建筑的下一层", + "shortcuts.selectPreviousLevel": "选择当前建筑的上一层", + "shortcuts.selection": "选择", + "shortcuts.switchToBuildMode": "切换到构建模式", + "shortcuts.switchToFurnishLayer": "切换到家具层", + "shortcuts.switchToFurnishPhase": "切换到家具阶段", + "shortcuts.switchToSelectMode": "切换到选择模式", + "shortcuts.switchToSitePhase": "切换到场地阶段", + "shortcuts.switchToStructureLayer": "切换到结构层", + "shortcuts.switchToStructurePhase": "切换到结构阶段", + "shortcuts.switchToZonesLayer": "切换到区域层", + "shortcuts.toggleSidebar": "切换侧边栏", + "shortcuts.undo": "撤销", + "shortcuts.worksWhileInSelectMode": "在选择模式下有效。", + "sidebar.items": "物品", + "sidebar.scene": "场景", + "sidebar.site": "场地", + "site.addLevel": "添加楼层", + "site.area": "面积", + "site.furnish": "家具", + "site.noBuildingsYet": "暂无建筑", + "site.noLevelsYet": "暂无楼层", + "site.perimeter": "周长", + "site.structure": "结构", + "site.zones": "区域", + "snapshot.area": "区域", + "snapshot.capture": "捕获", + "snapshot.capturing": "捕获中", + "snapshot.closeCapture": "关闭捕获模式", + "snapshot.dragArea": "拖动选择要捕获的区域", + "snapshot.escToCancel": "Esc取消", + "snapshot.saved": "已保存", + "snapshot.standard": "标准", + "snapshot.viewport": "视口", + "toolbar.cutaway": "剖面", + "toolbar.fullHeight": "全高", + "toolbar.low": "低", + "toolbar.up": "高", + "toolbar.walls": "墙体", + "tools.ceiling": "天花板", + "tools.column": "柱子", + "tools.door": "门", + "tools.elevator": "电梯", + "tools.fence": "围栏", + "tools.gableRoof": "山墙屋顶", + "tools.shelf": "架子", + "tools.slab": "楼板", + "tools.spawnPoint": "出生点", + "tools.stairs": "楼梯", + "tools.wall": "墙", + "tools.window": "窗", + "tools.zone": "区域", + "treeActions.cameraSnapshot": "相机快照", + "treeActions.clearSelection": "清除选择", + "treeActions.clearSnapshot": "清除快照", + "treeActions.hide": "隐藏", + "treeActions.show": "显示", + "treeActions.takeSnapshot": "拍摄快照", + "treeActions.updateSnapshot": "更新快照", + "treeActions.viewSnapshot": "查看快照", + "viewer.building": "建筑", + "viewer.camera": "相机", + "viewer.collapseSidebar": "收起侧边栏", + "viewer.couldNotAddGuideImage": "无法添加该参考图。", + "viewer.crispEdges": "清晰不透明的边缘线", + "viewer.cutaway": "剖面", + "viewer.deleteGuideImage": "删除参考图", + "viewer.deleteScan": "删除扫描", + "viewer.edges": "边缘线", + "viewer.expandSidebar": "展开侧边栏", + "viewer.exploded": "爆炸", + "viewer.faintOutline": "仅主要转折处的淡轮廓", + "viewer.fileTooLarge": "文件过大。最大支持 200 MB。", + "viewer.flatAndFast": "快速平坦 — 无环境光遮蔽", + "viewer.fullAO": "完整环境光遮蔽", + "viewer.fullHeight": "全高", + "viewer.grid": "网格", + "viewer.gridSnap": "网格吸附", + "viewer.guideImageSettings": "参考图设置", + "viewer.guideImagesOnThisLevel": "此楼层有 {count} 张参考图", + "viewer.guideImagesOnThisLevel_plural": "此楼层有 {count} 张参考图", + "viewer.guides": "参考图", + "viewer.hidden": "隐藏", + "viewer.hideReferenceFloor": "隐藏参考楼层", + "viewer.imperial": "英制 (ft)", + "viewer.levels": "楼层", + "viewer.low": "低", + "viewer.manual": "手动", + "viewer.metric": "公制 (m)", + "viewer.noEdgeLines": "无边缘线", + "viewer.noGuideImagesOnLevel": "此楼层暂无参考图。", + "viewer.noScansOnLevel": "此楼层暂无扫描。", + "viewer.off": "关", + "viewer.off_edge": "关闭", + "viewer.on": "开", + "viewer.opacity": "不透明度", + "viewer.openProjectBeforeUpload": "请先打开项目再上传扫描。", + "viewer.orbitLeft": "左旋转", + "viewer.orbitRight": "右旋转", + "viewer.orthographic": "正交", + "viewer.perspective": "透视", + "viewer.referenceFloorSettings": "参考楼层设置", + "viewer.render": "渲染", + "viewer.rendered": "渲染", + "viewer.scanSettings": "扫描设置", + "viewer.scanUploadUnavailable": "扫描上传不可用。", + "viewer.scans": "扫描", + "viewer.scansOnThisLevel": "此楼层有 {count} 个扫描", + "viewer.scansOnThisLevel_plural": "此楼层有 {count} 个扫描", + "viewer.sceneTheme": "场景主题", + "viewer.shadows": "阴影", + "viewer.showReferenceFloor": "显示参考楼层", + "viewer.soft": "柔和", + "viewer.solid": "纯色", + "viewer.solo": "单独", + "viewer.stack": "堆叠", + "viewer.strong": "锐利", + "viewer.topView": "顶视图", + "viewer.uploadGlbOrImage": "请上传 .glb/.gltf 扫描文件或图片。", + "viewer.uploadScanOrGuide": "上传扫描或参考图", + "viewer.uploading": "上传中", + "viewer.viewMode2D": "2D", + "viewer.viewMode3D": "3D", + "viewer.viewModeSplit": "分屏", + "viewer.visible": "可见", + "viewer.walkthrough": "导览", + "zone.addOne": "添加", + "zone.noZonesOnLevel": "此楼层暂无区域。", + "zone.selectLevelToView": "选择楼层以查看和创建区域", + "nodes.ceiling.toolHints.trace": "绘制天花板轮廓", + "nodes.ceiling.toolHints.finish": "完成天花板", + "nodes.item.label": "物品", + "nodes.item.description": "放置在场景中的物品。", + "nodes.site.label": "场地", + "nodes.site.description": "包含建筑的场地。", + "nodes.dormer.toolHints.place": "在屋顶上放置老虎窗", + "nodes.chimney.toolHints.place": "在屋顶上放置烟囱", + "nodes.ridgeVent.toolHints.place": "在屋顶上放置屋脊通风口", + "nodes.shelf.toolHints.place": "放置架子", + "nodes.skylight.toolHints.place": "在屋顶上放置天窗", + "nodes.spawn.toolHints.place": "放置出生点", + "nodes.zone.toolHints.place": "放置区域" +} \ No newline at end of file diff --git a/packages/editor/src/lib/level-name.ts b/packages/editor/src/lib/level-name.ts index 9476aa5be9..d91cf1b9f7 100644 --- a/packages/editor/src/lib/level-name.ts +++ b/packages/editor/src/lib/level-name.ts @@ -1,11 +1,13 @@ import type { LevelNode } from '@pascal-app/core' +import { messages } from './i18n' -export function getDefaultLevelName(level: number): string { - if (level === 0) return 'Ground Floor' - if (level > 0) return `Floor ${level}` - return `Basement ${-level}` +export function getDefaultLevelName(level: number, locale: 'en' | 'zh' = 'zh'): string { + const msgs = messages[locale] as Record + if (level === 0) return msgs['level.groundFloor'] || 'Ground Floor' + if (level > 0) return msgs['level.floor']?.replace('{n}', String(level)) || `Floor ${level}` + return msgs['level.basement']?.replace('{n}', String(-level)) || `Basement ${-level}` } -export function getLevelDisplayName(level: Pick): string { - return level.name || getDefaultLevelName(level.level) +export function getLevelDisplayName(level: Pick, locale: 'en' | 'zh' = 'zh'): string { + return level.name || getDefaultLevelName(level.level, locale) } diff --git a/packages/editor/src/store/use-command-registry.ts b/packages/editor/src/store/use-command-registry.ts index d3393b2049..3d6ef1d980 100644 --- a/packages/editor/src/store/use-command-registry.ts +++ b/packages/editor/src/store/use-command-registry.ts @@ -1,3 +1,5 @@ +'use client' + import type { ReactNode } from 'react' import { create } from 'zustand' diff --git a/packages/editor/src/store/use-palette-view-registry.ts b/packages/editor/src/store/use-palette-view-registry.ts index 339522304f..075aed5782 100644 --- a/packages/editor/src/store/use-palette-view-registry.ts +++ b/packages/editor/src/store/use-palette-view-registry.ts @@ -1,3 +1,5 @@ +'use client' + import type { ComponentType } from 'react' import { create } from 'zustand' diff --git a/packages/nodes/src/box-vent/definition.ts b/packages/nodes/src/box-vent/definition.ts index e0d61abd5d..1dd14bece8 100644 --- a/packages/nodes/src/box-vent/definition.ts +++ b/packages/nodes/src/box-vent/definition.ts @@ -64,12 +64,12 @@ export const boxVentDefinition: NodeDefinition = { move: () => import('./move-tool'), }, toolHints: [ - { key: 'Left click', label: 'Place box vent on roof' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.boxVent.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Box Vent', + label: 'nodes.boxVent.label', description: 'Small louvered exhaust vent that sits on a roof slope.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/box-vent/geometry.ts b/packages/nodes/src/box-vent/geometry.ts index 710f965212..a6c80c24e8 100644 --- a/packages/nodes/src/box-vent/geometry.ts +++ b/packages/nodes/src/box-vent/geometry.ts @@ -1,3 +1,5 @@ +'use client' + import { type BoxVentNode, getActiveRoofHeight, type RoofType } from '@pascal-app/core' import * as THREE from 'three' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' diff --git a/packages/nodes/src/box-vent/panel.tsx b/packages/nodes/src/box-vent/panel.tsx index 6e1c8f7823..ebd16bd2d8 100644 --- a/packages/nodes/src/box-vent/panel.tsx +++ b/packages/nodes/src/box-vent/panel.tsx @@ -18,6 +18,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Trash2 } from 'lucide-react' @@ -37,6 +38,7 @@ import type { BoxVentNode } from './schema' * ghost commits; on Esc it cancels and the original mesh is restored. */ export default function BoxVentPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) @@ -167,7 +169,7 @@ export default function BoxVentPanel() { icon="/icons/roof.png" onBack={node.roofSegmentId ? handleBack : undefined} onClose={handleClose} - title={node.name || 'Box Vent'} + title={node.name || t('nodes.boxVent.fallbackTitle')} width={300} > diff --git a/packages/nodes/src/box-vent/tool.tsx b/packages/nodes/src/box-vent/tool.tsx index cb004c14de..520826cfe5 100644 --- a/packages/nodes/src/box-vent/tool.tsx +++ b/packages/nodes/src/box-vent/tool.tsx @@ -9,7 +9,7 @@ import { sceneRegistry, useScene, } from '@pascal-app/core' -import { triggerSFX } from '@pascal-app/editor' +import { triggerSFX, useTranslations } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import * as THREE from 'three' @@ -31,6 +31,7 @@ const worldPoint = new THREE.Vector3() * roof + segment rotation stack. */ const BoxVentTool = () => { + const t = useTranslations() const activeBuildingId = useViewer((s) => s.selection.buildingId) const setSelection = useViewer((s) => s.setSelection) @@ -94,10 +95,11 @@ const BoxVentTool = () => { ) if (!hit) return const state = useScene.getState() + const ventCount = Object.values(state.nodes).filter((n) => (n as any).type === 'boxVent').length const vent = BoxVentNode.parse({ ...boxVentDefinition.defaults(), - name: 'Box Vent', + name: t('nodes.boxVent.defaultName', { count: ventCount + 1 }), roofSegmentId: hit.segment.id, position: [hit.localX, hit.localY, hit.localZ], rotation: 0, diff --git a/packages/nodes/src/building/definition.ts b/packages/nodes/src/building/definition.ts index 207733a14f..ea75f94189 100644 --- a/packages/nodes/src/building/definition.ts +++ b/packages/nodes/src/building/definition.ts @@ -39,8 +39,8 @@ export const buildingDefinition: NodeDefinition = { }, presentation: { - label: 'Building', - description: 'A building container holding one or more levels.', + label: 'nodes.building.label', + description: 'nodes.building.description', icon: { kind: 'url', src: '/icons/building.png' }, paletteSection: 'site', paletteOrder: 6, diff --git a/packages/nodes/src/ceiling/definition.ts b/packages/nodes/src/ceiling/definition.ts index 93bd59031b..068d550766 100644 --- a/packages/nodes/src/ceiling/definition.ts +++ b/packages/nodes/src/ceiling/definition.ts @@ -148,13 +148,13 @@ export const ceilingDefinition: NodeDefinition = { }, toolHints: [ - { key: 'Left click', label: 'Trace ceiling outline' }, - { key: 'Enter', label: 'Finish ceiling' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.ceiling.toolHints.trace' }, + { key: 'Enter', label: 'nodes.ceiling.toolHints.finish' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Ceiling', + label: 'nodes.ceiling.label', description: 'A polygon-bounded ceiling surface that hosts ceiling-mounted items.', icon: { kind: 'url', src: '/icons/ceiling.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/ceiling/panel.tsx b/packages/nodes/src/ceiling/panel.tsx index 67eceeffe3..01870ec41e 100644 --- a/packages/nodes/src/ceiling/panel.tsx +++ b/packages/nodes/src/ceiling/panel.tsx @@ -9,6 +9,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Edit, Move, Plus, Trash2 } from 'lucide-react' @@ -23,6 +24,7 @@ import { useCallback, useEffect, useRef } from 'react' * panel can collapse into auto-derived groups. */ export function CeilingPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const editingHole = useEditor((s) => s.editingHole) @@ -149,12 +151,12 @@ export function CeilingPanel() { - + handleUpdate({ height: v })} @@ -165,15 +167,15 @@ export function CeilingPanel() { />
- handleUpdate({ height: 2.4 })} /> - handleUpdate({ height: 2.5 })} /> - handleUpdate({ height: 3.0 })} /> + handleUpdate({ height: 2.4 })} /> + handleUpdate({ height: 2.5 })} /> + handleUpdate({ height: 3.0 })} />
- +
- Area + {t('nodes.ceiling.area')} {area.toFixed(2)} m²
@@ -187,7 +189,7 @@ export function CeilingPanel() { editingHole?.nodeId === selectedId && editingHole?.holeIndex === index const source = node.holeMetadata?.[index]?.source ?? 'manual' const isAutoHole = source !== 'manual' - const autoLabel = source === 'elevator' ? 'Auto elevator cutout' : 'Auto stair cutout' + const autoLabel = source === 'elevator' ? t('nodes.ceiling.autoHoleLabel.elevator') : t('nodes.ceiling.autoHoleLabel.stair') return (
- Hole {index + 1} {isEditing && '(Editing)'} + {t('nodes.ceiling.holeLabel', { index: index + 1 })} {isEditing && `(${t('common.done')})`}

{holeArea.toFixed(2)} m² · {hole.length} pts ·{' '} - {isAutoHole ? autoLabel : 'Manual'} + {isAutoHole ? autoLabel : t('nodes.ceiling.manual')}

{isEditing ? ( setEditingHole(null)} /> ) : isAutoHole ? (
- Auto + {t('nodes.ceiling.auto')}
) : ( <> @@ -243,7 +245,7 @@ export function CeilingPanel() { })}
) : ( -
No holes
+
{t('nodes.ceiling.noHoles')}
)}
@@ -251,14 +253,14 @@ export function CeilingPanel() { className="w-full" disabled={editingHole?.nodeId === selectedId} icon={} - label="Add Hole" + label={t('nodes.ceiling.addHole')} onClick={handleAddHole} />
- } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} />
) diff --git a/packages/nodes/src/ceiling/tool.tsx b/packages/nodes/src/ceiling/tool.tsx index 84226f1fd9..f73ad25d91 100644 --- a/packages/nodes/src/ceiling/tool.tsx +++ b/packages/nodes/src/ceiling/tool.tsx @@ -1,7 +1,7 @@ 'use client' import { emitter, type GridEvent, type LevelNode, useScene } from '@pascal-app/core' -import { CursorSphere, EDITOR_LAYER, markToolCancelConsumed, triggerSFX } from '@pascal-app/editor' +import { CursorSphere, EDITOR_LAYER, markToolCancelConsumed, triggerSFX, useTranslations } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three' @@ -42,10 +42,8 @@ function calculateSnapPoint( return [x1, y] } -function commitCeilingDrawing(levelId: LevelNode['id'], points: Array<[number, number]>): string { - const { createNode, nodes } = useScene.getState() - const ceilingCount = Object.values(nodes).filter((n) => n.type === 'ceiling').length - const name = `Ceiling ${ceilingCount + 1}` +function commitCeilingDrawing(levelId: LevelNode['id'], points: Array<[number, number]>, name: string): string { + const { createNode } = useScene.getState() const ceiling = CeilingNode.parse({ name, polygon: points }) createNode(ceiling, levelId) triggerSFX('sfx:structure-build') @@ -53,6 +51,7 @@ function commitCeilingDrawing(levelId: LevelNode['id'], points: Array<[number, n } export const CeilingTool: React.FC = () => { + const t = useTranslations() const cursorRef = useRef(null) const gridCursorRef = useRef(null) const mainLineRef = useRef(null!) @@ -128,7 +127,10 @@ export const CeilingTool: React.FC = () => { Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 && Math.abs(clickPoint[1] - firstPoint[1]) < 0.25 ) { - const ceilingId = commitCeilingDrawing(currentLevelId, points) + const { nodes } = useScene.getState() + const ceilingCount = Object.values(nodes).filter((n) => n.type === 'ceiling').length + const ceilingName = t('nodes.ceiling.defaultName', { count: ceilingCount + 1 }) + const ceilingId = commitCeilingDrawing(currentLevelId, points, ceilingName) setSelection({ selectedIds: [ceilingId] }) setPoints([]) } else { @@ -139,7 +141,10 @@ export const CeilingTool: React.FC = () => { const onGridDoubleClick = (_event: GridEvent) => { if (!currentLevelId) return if (points.length >= 3) { - const ceilingId = commitCeilingDrawing(currentLevelId, points) + const { nodes } = useScene.getState() + const ceilingCount = Object.values(nodes).filter((n) => n.type === 'ceiling').length + const ceilingName = t('nodes.ceiling.defaultName', { count: ceilingCount + 1 }) + const ceilingId = commitCeilingDrawing(currentLevelId, points, ceilingName) setSelection({ selectedIds: [ceilingId] }) setPoints([]) } diff --git a/packages/nodes/src/chimney/definition.ts b/packages/nodes/src/chimney/definition.ts index 4cb0d276cb..032acfe1d5 100644 --- a/packages/nodes/src/chimney/definition.ts +++ b/packages/nodes/src/chimney/definition.ts @@ -81,12 +81,12 @@ export const chimneyDefinition: NodeDefinition = { tool: () => import('./tool'), toolHints: [ - { key: 'Left click', label: 'Place chimney on roof' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.chimney.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Chimney', + label: 'nodes.chimney.label', description: 'Vertical masonry stack on a roof segment.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/chimney/geometry.ts b/packages/nodes/src/chimney/geometry.ts index 1cf71eb33e..a17f331a35 100644 --- a/packages/nodes/src/chimney/geometry.ts +++ b/packages/nodes/src/chimney/geometry.ts @@ -1,3 +1,5 @@ +'use client' + import { type ChimneyNode, getActiveRoofHeight, type RoofSegmentNode } from '@pascal-app/core' import * as THREE from 'three' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' diff --git a/packages/nodes/src/chimney/holes.ts b/packages/nodes/src/chimney/holes.ts index e8fef23f8b..c12ef01476 100644 --- a/packages/nodes/src/chimney/holes.ts +++ b/packages/nodes/src/chimney/holes.ts @@ -1,3 +1,5 @@ +'use client' + import { type ChimneyNode, getActiveRoofHeight, type RoofSegmentNode } from '@pascal-app/core' import { Brush, diff --git a/packages/nodes/src/chimney/panel.tsx b/packages/nodes/src/chimney/panel.tsx index ab2ddc9fd4..32a2a313e6 100644 --- a/packages/nodes/src/chimney/panel.tsx +++ b/packages/nodes/src/chimney/panel.tsx @@ -19,6 +19,7 @@ import { SegmentedControl, SliderControl, triggerSFX, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Trash2 } from 'lucide-react' @@ -49,6 +50,7 @@ const CHIMNEY_TYPE_OPTIONS: Array<{ label: string; value: ChimneyType }> = [ ] export default function ChimneyPanel() { + const t = useTranslations() const [chimneyType, setChimneyType] = useState('cap') const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) @@ -341,7 +343,7 @@ export default function ChimneyPanel() { icon="/icons/roof.png" onBack={node.roofSegmentId ? handleBack : undefined} onClose={handleClose} - title={node.name || 'Chimney'} + title={node.name || t('nodes.chimney.fallbackTitle')} width={300} > diff --git a/packages/nodes/src/chimney/roof-trim.ts b/packages/nodes/src/chimney/roof-trim.ts index 6fc76e23d2..e878f8a026 100644 --- a/packages/nodes/src/chimney/roof-trim.ts +++ b/packages/nodes/src/chimney/roof-trim.ts @@ -1,3 +1,5 @@ +'use client' + import { type ChimneyNode, getActiveRoofHeight, type RoofSegmentNode } from '@pascal-app/core' import { Brush, diff --git a/packages/nodes/src/chimney/tool.tsx b/packages/nodes/src/chimney/tool.tsx index d9b582aae3..cdf435fdd4 100644 --- a/packages/nodes/src/chimney/tool.tsx +++ b/packages/nodes/src/chimney/tool.tsx @@ -10,7 +10,7 @@ import { sceneRegistry, useScene, } from '@pascal-app/core' -import { triggerSFX } from '@pascal-app/editor' +import { triggerSFX, useTranslations } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import * as THREE from 'three' @@ -37,6 +37,7 @@ type SegmentTransform = { } const ChimneyTool = () => { + const t = useTranslations() const activeBuildingId = useViewer((s) => s.selection.buildingId) const setSelection = useViewer((s) => s.setSelection) @@ -113,10 +114,11 @@ const ChimneyTool = () => { ) if (!hit) return const state = useScene.getState() + const chimneyCount = Object.values(state.nodes).filter((n) => n.type === 'chimney').length const chimney = ChimneyNode.parse({ ...chimneyDefinition.defaults(), - name: 'Chimney', + name: t('nodes.chimney.defaultName', { count: chimneyCount + 1 }), roofSegmentId: hit.segment.id, position: [hit.localX, hit.localY, hit.localZ], rotation: 0, diff --git a/packages/nodes/src/column/definition.ts b/packages/nodes/src/column/definition.ts index 485f7c1bb6..83c3375056 100644 --- a/packages/nodes/src/column/definition.ts +++ b/packages/nodes/src/column/definition.ts @@ -354,7 +354,7 @@ export const columnDefinition: NodeDefinition = { }, presentation: { - label: 'Column', + label: 'nodes.column.label', description: 'A parametric column with configurable cross-section, base, and capital.', icon: { kind: 'url', src: '/icons/column.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/column/panel.tsx b/packages/nodes/src/column/panel.tsx index fad37c85b9..4e29ff1a14 100644 --- a/packages/nodes/src/column/panel.tsx +++ b/packages/nodes/src/column/panel.tsx @@ -17,6 +17,7 @@ import { ToggleControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Move, Trash2 } from 'lucide-react' @@ -83,17 +84,17 @@ const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map( ) const SUPPORT_STYLE_OPTIONS: Array<{ label: string; value: ColumnNode['supportStyle'] }> = [ - { label: 'Vertical', value: 'vertical' }, - { label: 'A-Frame', value: 'a-frame' }, - { label: 'Y Support', value: 'y-frame' }, - { label: 'V Support', value: 'v-frame' }, - { label: 'X Brace', value: 'x-brace' }, - { label: 'K Brace', value: 'k-brace' }, - { label: 'Single Strut', value: 'single-strut' }, - { label: 'Tripod', value: 'tripod' }, - { label: 'Trestle', value: 'trestle' }, - { label: 'Portal Frame', value: 'portal-frame' }, - { label: 'Box Frame', value: 'box-frame' }, + { label: 'nodes.column.vertical', value: 'vertical' }, + { label: 'nodes.column.aFrame', value: 'a-frame' }, + { label: 'nodes.column.yFrame', value: 'y-frame' }, + { label: 'nodes.column.vFrame', value: 'v-frame' }, + { label: 'nodes.column.xBrace', value: 'x-brace' }, + { label: 'nodes.column.kBrace', value: 'k-brace' }, + { label: 'nodes.column.singleStrut', value: 'single-strut' }, + { label: 'nodes.column.tripod', value: 'tripod' }, + { label: 'nodes.column.trestle', value: 'trestle' }, + { label: 'nodes.column.portalFrame', value: 'portal-frame' }, + { label: 'nodes.column.boxFrame', value: 'box-frame' }, ] type NonVerticalSupportStyle = Exclude @@ -259,6 +260,9 @@ function shaftProfileUpdates(shaftProfile: ColumnNode['shaftProfile']): Partial< } export default function ColumnPanel() { + const t = useTranslations() + const resolveOpts = (opts: { label: string; value: any }[]) => + opts.map((o) => ({ ...o, label: t(o.label) })) const selectedId = useViewer((s) => s.selection.selectedIds[0]) const selectedCount = useViewer((s) => s.selection.selectedIds.length) const setSelection = useViewer((s) => s.setSelection) @@ -315,10 +319,10 @@ export default function ColumnPanel() { - + - +
- {SUPPORT_STYLE_OPTIONS.map((option) => { + {resolveOpts(SUPPORT_STYLE_OPTIONS).map((option) => { const isSelected = supportStyle === option.value return (
@@ -610,11 +621,11 @@ export default function DoorPanel() {
{showFoldSection && ( - +
- Panels + {t('nodes.door.panels')} handleUpdate({ leafCount: v === '2' ? 2 : 4 })} @@ -627,7 +638,7 @@ export default function DoorPanel() {
handleUpdate({ operationState: v / 100 })} @@ -641,24 +652,24 @@ export default function DoorPanel() { )} {showSlideSection && ( - +
- {doorType === 'pocket' ? 'Pocket' : doorType === 'barn' ? 'Rail' : 'Panel'} + {doorType === 'pocket' ? t('nodes.door.slideDirectionOptions.pocket') : doorType === 'barn' ? t('nodes.door.slideDirectionOptions.rail') : t('nodes.door.slideDirectionOptions.panel')} handleUpdate({ slideDirection: v })} options={[ - { label: 'Left', value: 'left' }, - { label: 'Right', value: 'right' }, + { label: t('common.directions.left'), value: 'left' }, + { label: t('common.directions.right'), value: 'right' }, ]} value={node.slideDirection ?? 'left'} />
handleUpdate({ operationState: v / 100 })} @@ -672,9 +683,9 @@ export default function DoorPanel() { )} {showGarageSection && ( - + handleUpdate({ operationState: v / 100 })} @@ -686,7 +697,7 @@ export default function DoorPanel() { /> {isSectionalGarageDoor && ( handleUpdate({ garagePanelCount: Math.round(v) })} @@ -699,9 +710,9 @@ export default function DoorPanel() { )} - + handleUpdate({ width: v })} @@ -712,7 +723,7 @@ export default function DoorPanel() { value={Math.round(node.width * 100) / 100} /> @@ -727,7 +738,7 @@ export default function DoorPanel() { {showDoorShapeSection && ( - +
@@ -745,9 +756,9 @@ export default function DoorPanel() { }) } options={[ - { label: 'Rect', value: 'rectangle' }, - { label: 'Rounded', value: 'rounded' }, - { label: 'Arch', value: 'arch' }, + { label: t('nodes.door.topShapeOptions.rect'), value: 'rectangle' }, + { label: t('nodes.door.topShapeOptions.rounded'), value: 'rounded' }, + { label: t('nodes.door.topShapeOptions.arch'), value: 'arch' }, ]} value={doorShape} /> @@ -760,15 +771,15 @@ export default function DoorPanel() { handleUpdate({ openingRadiusMode: v as DoorNode['openingRadiusMode'] }) } options={[ - { label: 'All', value: 'all' }, - { label: 'Individual', value: 'individual' }, + { label: t('nodes.door.radiusModeOptions.all'), value: 'all' }, + { label: t('nodes.door.radiusModeOptions.individual'), value: 'individual' }, ]} value={openingRadiusMode} />
{openingRadiusMode === 'all' ? ( previewDoorUpdate('cornerRadius', v)} @@ -781,8 +792,8 @@ export default function DoorPanel() { ) : ( <> {[ - ['Top Left', 0], - ['Top Right', 1], + [t('nodes.door.corners.topLeft'), 0], + [t('nodes.door.corners.topRight'), 1], ].map(([label, index]) => ( )} previewDoorUpdate('openingRevealRadius', v)} @@ -814,7 +825,7 @@ export default function DoorPanel() { )} {doorShape === 'arch' && ( handleUpdate({ archHeight: v })} @@ -829,7 +840,7 @@ export default function DoorPanel() { )} {showOpeningShapeSection && ( - +
@@ -842,9 +853,9 @@ export default function DoorPanel() { }) } options={[ - { label: 'Rect', value: 'rectangle' }, - { label: 'Rounded', value: 'rounded' }, - { label: 'Arch', value: 'arch' }, + { label: t('nodes.door.openingShapeOptions.rect'), value: 'rectangle' }, + { label: t('nodes.door.openingShapeOptions.rounded'), value: 'rounded' }, + { label: t('nodes.door.openingShapeOptions.arch'), value: 'arch' }, ]} value={openingShape} /> @@ -857,15 +868,15 @@ export default function DoorPanel() { handleUpdate({ openingRadiusMode: v as DoorNode['openingRadiusMode'] }) } options={[ - { label: 'All', value: 'all' }, - { label: 'Individual', value: 'individual' }, + { label: t('nodes.door.radiusModeOptions.all'), value: 'all' }, + { label: t('nodes.door.radiusModeOptions.individual'), value: 'individual' }, ]} value={openingRadiusMode} />
{openingRadiusMode === 'all' ? ( previewDoorUpdate('cornerRadius', v)} @@ -878,8 +889,8 @@ export default function DoorPanel() { ) : ( <> {[ - ['Top Left', 0], - ['Top Right', 1], + [t('nodes.door.corners.topLeft'), 0], + [t('nodes.door.corners.topRight'), 1], ].map(([label, index]) => ( )} previewDoorUpdate('openingRevealRadius', v)} @@ -911,7 +922,7 @@ export default function DoorPanel() { )} {openingShape === 'arch' && ( handleUpdate({ archHeight: v })} @@ -928,9 +939,9 @@ export default function DoorPanel() { {!isCutoutOnly && ( <> {showFrameSection && ( - + handleUpdate({ frameThickness: v })} @@ -940,7 +951,7 @@ export default function DoorPanel() { value={Math.round(node.frameThickness * 1000) / 1000} /> handleUpdate({ frameDepth: v })} @@ -953,9 +964,9 @@ export default function DoorPanel() { )} {showContentPaddingSection && ( - + handleUpdate({ contentPadding: [v, node.contentPadding[1]] })} @@ -965,7 +976,7 @@ export default function DoorPanel() { value={Math.round(node.contentPadding[0] * 1000) / 1000} /> handleUpdate({ contentPadding: [node.contentPadding[0], v] })} @@ -978,18 +989,18 @@ export default function DoorPanel() { )} {showSwingSection && ( - +
{supportsHingeSide && (
- Hinges Side + {t('nodes.door.hingesSide')} handleUpdate({ hingesSide: v })} options={[ - { label: 'Left', value: 'left' }, - { label: 'Right', value: 'right' }, + { label: t('common.directions.left'), value: 'left' }, + { label: t('common.directions.right'), value: 'right' }, ]} value={node.hingesSide} /> @@ -997,13 +1008,13 @@ export default function DoorPanel() { )}
- Direction + {t('nodes.door.direction')} handleUpdate({ swingDirection: v })} options={[ - { label: 'Inward', value: 'inward' }, - { label: 'Outward', value: 'outward' }, + { label: t('nodes.door.swingDirectionOptions.inward'), value: 'inward' }, + { label: t('nodes.door.swingDirectionOptions.outward'), value: 'outward' }, ]} value={node.swingDirection} /> @@ -1013,16 +1024,16 @@ export default function DoorPanel() { )} {showThresholdSection && ( - + handleUpdate({ threshold: checked })} /> {node.threshold && (
handleUpdate({ thresholdHeight: v })} @@ -1037,18 +1048,18 @@ export default function DoorPanel() { )} {showHandleSection && ( - + {isSwingDoor && ( handleUpdate({ handle: checked })} /> )} {(node.handle || !isSwingDoor) && (
handleUpdate({ handleHeight: v })} @@ -1060,13 +1071,13 @@ export default function DoorPanel() { {supportsHandleSide && (
- Handle Side + {t('nodes.door.handleSide')} handleUpdate({ handleSide: v })} options={[ - { label: 'Left', value: 'left' }, - { label: 'Right', value: 'right' }, + { label: t('common.directions.left'), value: 'left' }, + { label: t('common.directions.right'), value: 'right' }, ]} value={node.handleSide} /> @@ -1078,21 +1089,21 @@ export default function DoorPanel() { )} {showHardwareSection && ( - + handleUpdate({ doorCloser: checked })} /> handleUpdate({ panicBar: checked })} /> {node.panicBar && (
handleUpdate({ panicBarHeight: v })} @@ -1107,7 +1118,7 @@ export default function DoorPanel() { )} {showSegmentsSection && ( - + {node.segments.map((seg, i) => { const numCols = seg.columnRatios.length const colSum = seg.columnRatios.reduce((a, b) => a + b, 0) @@ -1115,7 +1126,7 @@ export default function DoorPanel() { return (
- Segment {i + 1} + {t('nodes.door.segmentLabel', { i: i + 1 })}
setSegmentHeightRatio(i, v / 100)} @@ -1145,7 +1156,7 @@ export default function DoorPanel() { /> { @@ -1165,7 +1176,7 @@ export default function DoorPanel() { {normCols.map((ratio, ci) => ( setSegmentColumnRatio(i, ci, v / 100)} @@ -1176,7 +1187,7 @@ export default function DoorPanel() { /> ))} { @@ -1196,7 +1207,7 @@ export default function DoorPanel() { {seg.type === 'panel' && (
{ @@ -1211,7 +1222,7 @@ export default function DoorPanel() { value={Math.round(seg.panelInset * 1000) / 1000} /> { @@ -1233,7 +1244,7 @@ export default function DoorPanel() {
{ const updated = [ ...node.segments, @@ -1252,7 +1263,7 @@ export default function DoorPanel() { {node.segments.length > 1 && ( handleUpdate({ segments: node.segments.slice(0, -1) })} /> )} @@ -1262,18 +1273,18 @@ export default function DoorPanel() { )} - + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={handleDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/door/tool.tsx b/packages/nodes/src/door/tool.tsx index 85cb6431fe..dc93270692 100644 --- a/packages/nodes/src/door/tool.tsx +++ b/packages/nodes/src/door/tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, DoorNode, @@ -16,6 +18,7 @@ import { isValidWallSideFace, snapToHalf, triggerSFX, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef } from 'react' @@ -35,6 +38,7 @@ const edgeMaterial = new LineBasicNodeMaterial({ * Doors always sit at floor level (clampedY = height/2). */ const DoorTool: React.FC = () => { + const t = useTranslations() const draftRef = useRef(null) const cursorGroupRef = useRef(null!) const edgesRef = useRef(null!) @@ -242,7 +246,7 @@ const DoorTool: React.FC = () => { const wall = n.parentId ? state.nodes[n.parentId as AnyNodeId] : undefined return wall?.parentId === levelId }).length - const name = `Door ${doorCount + 1}` + const name = t('nodes.door.defaultName', { count: doorCount + 1 }) const node = DoorNode.parse({ name, diff --git a/packages/nodes/src/dormer/csg-geometry.ts b/packages/nodes/src/dormer/csg-geometry.ts index 4141e5af32..03b6435c2b 100644 --- a/packages/nodes/src/dormer/csg-geometry.ts +++ b/packages/nodes/src/dormer/csg-geometry.ts @@ -1,3 +1,5 @@ +'use client' + import { type DormerNode, getActiveRoofHeight, diff --git a/packages/nodes/src/dormer/definition.ts b/packages/nodes/src/dormer/definition.ts index 30ba96cd81..1f4a8eea00 100644 --- a/packages/nodes/src/dormer/definition.ts +++ b/packages/nodes/src/dormer/definition.ts @@ -77,13 +77,13 @@ export const dormerDefinition: NodeDefinition = { tool: () => import('./tool'), toolHints: [ - { key: 'Left click', label: 'Place dormer on roof' }, - { key: 'R / Shift+R', label: 'Rotate ghost ±15°' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.dormer.toolHints.place' }, + { key: 'R / Shift+R', label: 'nodes.dormer.toolHints.rotateGhost' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Dormer', + label: 'nodes.dormer.label', description: 'House-shaped protrusion on a roof segment.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/dormer/panel-actions-section.tsx b/packages/nodes/src/dormer/panel-actions-section.tsx index d8c8cdf013..3a8f4f8a94 100644 --- a/packages/nodes/src/dormer/panel-actions-section.tsx +++ b/packages/nodes/src/dormer/panel-actions-section.tsx @@ -1,6 +1,6 @@ 'use client' -import { ActionButton, ActionGroup, PanelSection } from '@pascal-app/editor' +import { ActionButton, ActionGroup, PanelSection, useTranslations } from '@pascal-app/editor' import { Copy, Move, Trash2 } from 'lucide-react' /** @@ -16,19 +16,20 @@ export function DormerActionsSection({ onDuplicate: () => void onDelete: () => void }) { + const t = useTranslations() return ( - + - } label="Move" onClick={onMove} /> + } label={t('common.move')} onClick={onMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={onDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={onDelete} /> diff --git a/packages/nodes/src/dormer/panel-position-section.tsx b/packages/nodes/src/dormer/panel-position-section.tsx index c36e062910..fca882d15b 100644 --- a/packages/nodes/src/dormer/panel-position-section.tsx +++ b/packages/nodes/src/dormer/panel-position-section.tsx @@ -8,7 +8,7 @@ import { sceneRegistry, useScene, } from '@pascal-app/core' -import { PanelSection, SliderControl } from '@pascal-app/editor' +import { PanelSection, SliderControl, useTranslations } from '@pascal-app/editor' import { useMemo } from 'react' import { Vector3 } from 'three' @@ -34,6 +34,7 @@ export function DormerPositionSection({ previewProp: (updates: Partial) => void commitProp: (updates: Partial) => void }) { + const t = useTranslations() const px = node.position[0] const py = node.position[1] const pz = node.position[2] @@ -165,7 +166,7 @@ export function DormerPositionSection({ } return ( - + ) => void handleUpdate: (updates: Partial) => void }) { + const t = useTranslations() const [radiusViewMode, setRadiusViewMode] = useState('all') const windowShape: WindowShape = node.windowShape @@ -56,9 +57,9 @@ export function DormerWindowSection({ return ( <> - + previewProp({ wallSkirtHeight: v })} @@ -71,9 +72,9 @@ export function DormerWindowSection({ /> - + previewProp({ windowWidth: v })} @@ -85,7 +86,7 @@ export function DormerWindowSection({ value={Math.round(node.windowWidth * 100) / 100} /> previewProp({ windowHeight: v })} @@ -122,7 +123,7 @@ export function DormerWindowSection({ /> - + handleUpdate({ @@ -140,9 +141,9 @@ export function DormerWindowSection({ }) } options={[ - { value: 'rectangle', label: 'Rect' }, - { value: 'rounded', label: 'Rounded' }, - { value: 'arch', label: 'Arch' }, + { value: 'rectangle', label: t('nodes.dormer.rect') }, + { value: 'rounded', label: t('nodes.dormer.rounded') }, + { value: 'arch', label: t('nodes.dormer.arch') }, ]} value={windowShape} /> @@ -151,14 +152,14 @@ export function DormerWindowSection({ setRadiusViewMode(v as WindowRadiusMode)} options={[ - { value: 'all', label: 'All' }, - { value: 'individual', label: 'Individual' }, + { value: 'all', label: t('nodes.dormer.all') }, + { value: 'individual', label: t('nodes.dormer.individual') }, ]} value={tupleIsUniform ? radiusViewMode : 'individual'} /> {tupleIsUniform && radiusViewMode === 'all' ? ( setAllCornerRadii(v, false)} @@ -172,10 +173,10 @@ export function DormerWindowSection({ ) : ( ( [ - ['Top Left', 0], - ['Top Right', 1], - ['Bottom Right', 2], - ['Bottom Left', 3], + [t('nodes.dormer.topLeft'), 0], + [t('nodes.dormer.topRight'), 1], + [t('nodes.dormer.bottomRight'), 2], + [t('nodes.dormer.bottomLeft'), 3], ] as const ).map(([label, index]) => ( previewProp({ windowArchHeight: v })} @@ -211,9 +212,9 @@ export function DormerWindowSection({ )} - + previewProp({ windowFrameThickness: v })} @@ -225,7 +226,7 @@ export function DormerWindowSection({ value={Math.round(node.windowFrameThickness * 1000) / 1000} /> previewProp({ windowFrameDepth: v })} @@ -237,7 +238,7 @@ export function DormerWindowSection({ value={Math.round(node.windowFrameDepth * 1000) / 1000} /> previewProp({ windowDividerThickness: v })} @@ -250,9 +251,9 @@ export function DormerWindowSection({ /> - + previewProp({ windowColumns: Math.max(1, Math.min(8, Math.round(v))) })} @@ -263,7 +264,7 @@ export function DormerWindowSection({ value={node.windowColumns} /> previewProp({ windowRows: Math.max(1, Math.min(8, Math.round(v))) })} @@ -275,16 +276,16 @@ export function DormerWindowSection({ /> - + handleUpdate({ windowSill: checked })} /> {node.windowSill && (
previewProp({ windowSillDepth: v })} @@ -296,7 +297,7 @@ export function DormerWindowSection({ value={Math.round(node.windowSillDepth * 1000) / 1000} /> previewProp({ windowSillThickness: v })} diff --git a/packages/nodes/src/dormer/panel.tsx b/packages/nodes/src/dormer/panel.tsx index be6d1676f4..49a54c98fe 100644 --- a/packages/nodes/src/dormer/panel.tsx +++ b/packages/nodes/src/dormer/panel.tsx @@ -16,6 +16,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useCallback, useState } from 'react' @@ -42,6 +43,7 @@ const SECTION_OPTIONS: Array<{ label: string; value: DormerSection }> = [ ] export default function DormerPanel() { + const t = useTranslations() const [section, setSection] = useState('dormer') const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) @@ -162,7 +164,7 @@ export default function DormerPanel() { icon="/icons/roof.png" onBack={node.roofSegmentId ? handleBack : undefined} onClose={handleClose} - title={node.name || 'Dormer'} + title={node.name || t('nodes.dormer.fallbackTitle')} width={300} > - +
{SECTION_OPTIONS.map((option) => { const isSelected = section === option.value @@ -199,9 +201,9 @@ export default function DormerPanel() { {section === 'dormer' && ( <> - + previewProp({ width: v })} @@ -213,7 +215,7 @@ export default function DormerPanel() { value={Math.round(node.width * 100) / 100} /> previewProp({ depth: v })} @@ -225,7 +227,7 @@ export default function DormerPanel() { value={Math.round(node.depth * 100) / 100} /> previewProp({ height: v })} @@ -237,7 +239,7 @@ export default function DormerPanel() { value={Math.round(node.height * 100) / 100} /> previewProp({ roofHeight: v })} @@ -250,7 +252,7 @@ export default function DormerPanel() { /> - +
{ROOF_TYPE_OPTIONS.map((option) => { const isSelected = node.roofType === option.value diff --git a/packages/nodes/src/dormer/tool.tsx b/packages/nodes/src/dormer/tool.tsx index 68a873a4a0..3b81671f25 100644 --- a/packages/nodes/src/dormer/tool.tsx +++ b/packages/nodes/src/dormer/tool.tsx @@ -1,6 +1,7 @@ 'use client' import { type AnyNodeId, DormerNode, useScene } from '@pascal-app/core' +import { useTranslations } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useMemo } from 'react' import { dormerDefinition } from './definition' @@ -34,6 +35,7 @@ function nextDormerNumber(nodes: Record): number { * registry) so the user sees exactly where the dormer will land. */ const DormerTool = () => { + const t = useTranslations() const setSelection = useViewer((s) => s.setSelection) const previewNode = useMemo( @@ -50,9 +52,10 @@ const DormerTool = () => { const { activeBuildingId, segmentXform, hitLocal, ghostRotation } = useDormerPlacement({ onCommit: (hit, rotation) => { const state = useScene.getState() + const dormerNumber = nextDormerNumber(state.nodes) const dormer = DormerNode.parse({ ...dormerDefinition.defaults(), - name: `Dormer ${nextDormerNumber(state.nodes)}`, + name: t('nodes.dormer.defaultName', { count: dormerNumber }), roofSegmentId: hit.segment.id, parentId: hit.segment.id, // Anchor at the slope height so the renderer matches the ghost. diff --git a/packages/nodes/src/dormer/use-dormer-placement.ts b/packages/nodes/src/dormer/use-dormer-placement.ts index e524342dd1..312d35b36d 100644 --- a/packages/nodes/src/dormer/use-dormer-placement.ts +++ b/packages/nodes/src/dormer/use-dormer-placement.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, diff --git a/packages/nodes/src/elevator/definition.ts b/packages/nodes/src/elevator/definition.ts index cf101303f0..4edb2210d3 100644 --- a/packages/nodes/src/elevator/definition.ts +++ b/packages/nodes/src/elevator/definition.ts @@ -202,7 +202,7 @@ export const elevatorDefinition: NodeDefinition = { }, presentation: { - label: 'Elevator', + label: 'nodes.elevator.label', description: 'A multi-level elevator shaft with configurable openings per level.', icon: { kind: 'url', src: '/icons/wallcut.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/elevator/panel.tsx b/packages/nodes/src/elevator/panel.tsx index 9eb51e3bf1..918a88fad4 100644 --- a/packages/nodes/src/elevator/panel.tsx +++ b/packages/nodes/src/elevator/panel.tsx @@ -23,6 +23,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Send, Trash2 } from 'lucide-react' @@ -158,6 +159,7 @@ function degreesToRadians(degrees: number) { } export default function ElevatorPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const selectedCount = useViewer((s) => s.selection.selectedIds.length) const setSelection = useViewer((s) => s.setSelection) @@ -473,27 +475,27 @@ export default function ElevatorPanel() { - + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={handleDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> - + = { }, toolHints: [ - { key: 'Left click', label: 'Set fence start / end' }, - { key: 'Shift', label: 'Allow non-45° angles' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.fence.toolHints.setStartEnd' }, + { key: 'Shift', label: 'nodes.fence.toolHints.allowAngles' }, + { key: 'Esc', label: 'nodes.fence.toolHints.cancel' }, ], presentation: { - label: 'Fence', + label: 'nodes.fence.label', description: 'A straight or curved fence segment with configurable posts and infill.', icon: { kind: 'url', src: '/icons/fence.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/fence/tool.tsx b/packages/nodes/src/fence/tool.tsx index 956a57b539..df3168f1bc 100644 --- a/packages/nodes/src/fence/tool.tsx +++ b/packages/nodes/src/fence/tool.tsx @@ -26,6 +26,7 @@ import { snapFenceDraftPoint, triggerSFX, useEditor, + useTranslations, WALL_FINE_GRID_STEP, } from '@pascal-app/editor' import { getSceneTheme, useViewer } from '@pascal-app/viewer' @@ -119,12 +120,12 @@ function toFencePlanPoint(point: Point2D): FencePlanPoint { return [point.x, point.y] } -function toMiterWall(segment: SegmentLike): WallNode { +function toMiterWall(segment: SegmentLike, name: string): WallNode { return { object: 'node', id: segment.id as WallNode['id'], type: 'wall', - name: 'Fence reference', + name, parentId: null, visible: true, metadata: {}, @@ -165,12 +166,13 @@ function getFenceFaceAngleCandidates( point: FencePlanPoint, segment: SegmentLike, miterData: WallMiterData, + fenceReferenceName: string, ): FaceAngleCandidate[] { const endpoint = getSegmentEndpointKind(point, segment) const reference = getSegmentAngleReferenceAtPoint(point, segment) if (!(endpoint && reference)) return [] - const boundaryPoints = getWallMiterBoundaryPoints(toMiterWall(segment), miterData) + const boundaryPoints = getWallMiterBoundaryPoints(toMiterWall(segment, fenceReferenceName), miterData) if (!boundaryPoints) return [] const points = @@ -278,11 +280,12 @@ function getDraftAngleLabels( baseY: number, previewHeight: number, previewThickness: number, + fenceReferenceName: string, ): DraftAngleLabel[] { const draftFromStart: FencePlanPoint = [end[0] - start[0], end[1] - start[1]] const draftFromEnd: FencePlanPoint = [start[0] - end[0], start[1] - end[1]] const draftSegment = buildDraftFenceSegment(start, end, previewThickness) - const miterData = calculateLevelMiters([...segments, draftSegment].map(toMiterWall)) + const miterData = calculateLevelMiters([...segments, draftSegment].map((s) => toMiterWall(s, fenceReferenceName))) const endpoints = [ { id: 'start', point: start, draftVector: draftFromStart }, { id: 'end', point: end, draftVector: draftFromEnd }, @@ -295,11 +298,12 @@ function getDraftAngleLabels( if (!connectedSegment) continue const connectedReference = getSegmentAngleReferenceAtPoint(endpoint.point, connectedSegment) if (!connectedReference) continue - const draftFaceCandidates = getFenceFaceAngleCandidates(endpoint.point, draftSegment, miterData) + const draftFaceCandidates = getFenceFaceAngleCandidates(endpoint.point, draftSegment, miterData, fenceReferenceName) const connectedFaceCandidates = getFenceFaceAngleCandidates( endpoint.point, connectedSegment, miterData, + fenceReferenceName, ) const facePairs = getMatchingFaceAnglePairs(draftFaceCandidates, connectedFaceCandidates) const { arcCenter, connectedVector, draftVector } = getAngleSource( @@ -354,6 +358,7 @@ function getDraftMeasurementState( baseY: number, previewHeight: number, previewThickness: number, + fenceReferenceName: string, ): DraftMeasurementState { const dx = end[0] - start[0] const dz = end[1] - start[1] @@ -366,7 +371,7 @@ function getDraftMeasurementState( baseY + previewHeight + DRAFT_LABEL_Y_OFFSET, (start[1] + end[1]) / 2, ], - angleLabels: getDraftAngleLabels(start, end, segments, baseY, previewHeight, previewThickness), + angleLabels: getDraftAngleLabels(start, end, segments, baseY, previewHeight, previewThickness, fenceReferenceName), } } @@ -430,6 +435,7 @@ function getCurrentLevelElements(): { walls: WallNode[]; fences: FenceNode[] } { } export const FenceTool: React.FC = () => { + const t = useTranslations() const unit = useViewer((state) => state.unit) const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark') // A placed preset seeds `toolDefaults.fence` before the tool mounts, so @@ -510,6 +516,7 @@ export const FenceTool: React.FC = () => { startingPoint.current.y, previewHeightRef.current, previewThicknessRef.current, + t('nodes.fence.reference'), ), ) } else { diff --git a/packages/nodes/src/guide/definition.ts b/packages/nodes/src/guide/definition.ts index 49985f8372..b1a6a77adb 100644 --- a/packages/nodes/src/guide/definition.ts +++ b/packages/nodes/src/guide/definition.ts @@ -41,7 +41,7 @@ export const guideDefinition: NodeDefinition = { }, presentation: { - label: 'Guide', + label: 'nodes.guide.label', description: 'A measurement / reference annotation (linear, area, or arc).', icon: { kind: 'url', src: '/icons/blueprint.png' }, paletteSection: 'site', diff --git a/packages/nodes/src/item/definition.ts b/packages/nodes/src/item/definition.ts index 7ca83f1cfc..669c9f9aa4 100644 --- a/packages/nodes/src/item/definition.ts +++ b/packages/nodes/src/item/definition.ts @@ -135,16 +135,16 @@ export const itemDefinition: NodeDefinition = { floorplanMoveTarget: itemFloorplanMoveTarget, toolHints: [ - { key: 'Left click', label: 'Place item' }, - { key: 'R', label: 'Rotate counterclockwise' }, - { key: 'T', label: 'Rotate clockwise' }, - { key: 'Shift', label: 'Free place' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'itemHelper.placeItem' }, + { key: 'R', label: 'itemHelper.rotateCounterclockwise' }, + { key: 'T', label: 'itemHelper.rotateClockwise' }, + { key: 'Shift', label: 'itemHelper.freePlace' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Item', - description: 'A catalog-backed item (furniture, fixtures, decorations).', + label: 'nodes.item.label', + description: 'nodes.item.description', icon: { kind: 'url', src: '/icons/item.png' }, paletteSection: 'furnish', paletteOrder: 10, diff --git a/packages/nodes/src/level/definition.ts b/packages/nodes/src/level/definition.ts index e7623f6841..848034e44b 100644 --- a/packages/nodes/src/level/definition.ts +++ b/packages/nodes/src/level/definition.ts @@ -47,8 +47,8 @@ export const levelDefinition: NodeDefinition = { }, presentation: { - label: 'Level', - description: 'A single floor of a building, holding walls / slabs / ceilings / items.', + label: 'nodes.level.label', + description: 'nodes.level.description', icon: { kind: 'url', src: '/icons/level.png' }, paletteSection: 'site', paletteOrder: 7, diff --git a/packages/nodes/src/ridge-vent/definition.ts b/packages/nodes/src/ridge-vent/definition.ts index a0ff7c559f..2297f13008 100644 --- a/packages/nodes/src/ridge-vent/definition.ts +++ b/packages/nodes/src/ridge-vent/definition.ts @@ -53,12 +53,12 @@ export const ridgeVentDefinition: NodeDefinition = { move: () => import('./move-tool'), }, toolHints: [ - { key: 'Left click', label: 'Place ridge vent on roof' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.ridgeVent.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Ridge Vent', + label: 'nodes.ridgeVent.label', description: 'Ventilation strip running along the ridge of a roof segment.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/ridge-vent/geometry.ts b/packages/nodes/src/ridge-vent/geometry.ts index 61ef72b408..f850ec8f89 100644 --- a/packages/nodes/src/ridge-vent/geometry.ts +++ b/packages/nodes/src/ridge-vent/geometry.ts @@ -1,3 +1,5 @@ +'use client' + import type { RidgeVentNode } from '@pascal-app/core' import * as THREE from 'three' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' diff --git a/packages/nodes/src/roof-segment/definition.ts b/packages/nodes/src/roof-segment/definition.ts index 397ccd8225..8708ab4f89 100644 --- a/packages/nodes/src/roof-segment/definition.ts +++ b/packages/nodes/src/roof-segment/definition.ts @@ -225,7 +225,7 @@ export const roofSegmentDefinition: NodeDefinition = { }, presentation: { - label: 'Roof Segment', + label: 'nodes.roofSegment.label', description: 'A single pitched plane of a parent roof.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/roof/definition.ts b/packages/nodes/src/roof/definition.ts index d18eb13a15..fbf96ec674 100644 --- a/packages/nodes/src/roof/definition.ts +++ b/packages/nodes/src/roof/definition.ts @@ -42,8 +42,8 @@ export const roofDefinition: NodeDefinition = { }, presentation: { - label: 'Roof', - description: 'A pitched / hip / gable roof composed of one or more segments.', + label: 'nodes.roof.label', + description: 'nodes.roof.description', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', paletteOrder: 100, diff --git a/packages/nodes/src/roof/panel.tsx b/packages/nodes/src/roof/panel.tsx index 9dadc262b2..599de62aef 100644 --- a/packages/nodes/src/roof/panel.tsx +++ b/packages/nodes/src/roof/panel.tsx @@ -24,6 +24,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Plus, Trash2 } from 'lucide-react' @@ -31,6 +32,7 @@ import { useCallback, useState } from 'react' import { useShallow } from 'zustand/react/shallow' export default function RoofPanel() { + const t = useTranslations() const [ventType, setVentType] = useState<'box-vent' | 'ridge-vent'>('box-vent') const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) @@ -223,10 +225,10 @@ export default function RoofPanel() { - +
{segments.map((seg, i) => ( ))} } - label="Add Dormer" + label={t('nodes.roof.addDormer')} onClick={() => activateTool('dormer')} /> @@ -375,13 +377,13 @@ export default function RoofPanel() { type="button" > {skylight.name || `Skylight ${i + 1}`} - skylight + {t('nodes.roof.skylightType')} ))} } - label="Add Skylight" + label={t('nodes.roof.addSkylight')} onClick={() => activateTool('skylight')} /> @@ -396,13 +398,13 @@ export default function RoofPanel() { type="button" > {panel.name || `Solar Panel ${i + 1}`} - solar panel + {t('nodes.roof.solarPanelType')} ))} } - label="Add Solar Panel" + label={t('nodes.roof.addSolarPanel')} onClick={() => activateTool('solar-panel')} /> @@ -421,22 +423,22 @@ export default function RoofPanel() { (vent.type === 'box-vent' ? `Box Vent ${i + 1}` : `Ridge Vent ${i + 1}`)} - {vent.type === 'box-vent' ? 'box vent' : 'ridge vent'} + {vent.type === 'box-vent' ? t('nodes.roof.ventType') : t('nodes.roof.ventType')} ))} onChange={setVentType} options={[ - { label: 'Box', value: 'box-vent' }, - { label: 'Ridge', value: 'ridge-vent' }, + { label: t('nodes.roof.box'), value: 'box-vent' }, + { label: t('nodes.roof.ridge'), value: 'ridge-vent' }, ]} value={ventType} /> } - label="Add Vent" + label={t('nodes.roof.addVent')} onClick={() => activateTool(ventType)} /> @@ -444,18 +446,18 @@ export default function RoofPanel() {
- + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={handleDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/scan/definition.ts b/packages/nodes/src/scan/definition.ts index f97ee25682..b11883116f 100644 --- a/packages/nodes/src/scan/definition.ts +++ b/packages/nodes/src/scan/definition.ts @@ -40,7 +40,7 @@ export const scanDefinition: NodeDefinition = { }, presentation: { - label: 'Scan', + label: 'nodes.scan.label', description: 'A captured mesh (LiDAR / photogrammetry) imported as a scene reference.', icon: { kind: 'url', src: '/icons/mesh.png' }, paletteSection: 'site', diff --git a/packages/nodes/src/shelf/definition.ts b/packages/nodes/src/shelf/definition.ts index b5a6cdcb89..932b4b28a8 100644 --- a/packages/nodes/src/shelf/definition.ts +++ b/packages/nodes/src/shelf/definition.ts @@ -211,12 +211,12 @@ export const shelfDefinition: NodeDefinition = { preview: () => import('./preview'), tool: () => import('./tool'), toolHints: [ - { key: 'Left click', label: 'Place shelf' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.shelf.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Shelf', + label: 'nodes.shelf.label', description: 'A configurable shelving unit. Items host on each row.', icon: { kind: 'url', src: '/icons/shelf.png' }, paletteSection: 'furnish', diff --git a/packages/nodes/src/site/definition.ts b/packages/nodes/src/site/definition.ts index 70dd62b0c2..f94a8d13ca 100644 --- a/packages/nodes/src/site/definition.ts +++ b/packages/nodes/src/site/definition.ts @@ -37,7 +37,7 @@ export const siteDefinition: NodeDefinition = { }, presentation: { - label: 'Site', + label: 'nodes.site.label', description: 'The top-level container holding buildings, zones, and the property boundary.', icon: { kind: 'url', src: '/icons/site.png' }, paletteSection: 'site', diff --git a/packages/nodes/src/skylight/definition.ts b/packages/nodes/src/skylight/definition.ts index 719cdeae7d..c2ec54ca72 100644 --- a/packages/nodes/src/skylight/definition.ts +++ b/packages/nodes/src/skylight/definition.ts @@ -66,12 +66,12 @@ export const skylightDefinition: NodeDefinition = { move: () => import('./move-tool'), }, toolHints: [ - { key: 'Left click', label: 'Place skylight on roof' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.skylight.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Skylight', + label: 'nodes.skylight.label', description: 'Framed glass opening on a roof segment.', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/slab/definition.ts b/packages/nodes/src/slab/definition.ts index 5f289b89d5..ccbf5e4258 100644 --- a/packages/nodes/src/slab/definition.ts +++ b/packages/nodes/src/slab/definition.ts @@ -138,14 +138,14 @@ export const slabDefinition: NodeDefinition = { }, toolHints: [ - { key: 'Left click', label: 'Trace slab outline' }, - { key: 'Enter', label: 'Finish slab' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.slab.toolHints.trace' }, + { key: 'Enter', label: 'nodes.slab.toolHints.finish' }, + { key: 'Esc', label: 'nodes.slab.toolHints.cancel' }, ], presentation: { - label: 'Slab', - description: 'A polygon-bounded floor surface that hosts items on top.', + label: 'nodes.slab.label', + description: 'nodes.slab.description', icon: { kind: 'url', src: '/icons/floor.png' }, paletteSection: 'structure', paletteOrder: 30, diff --git a/packages/nodes/src/solar-panel/definition.ts b/packages/nodes/src/solar-panel/definition.ts index 3525519efe..5eef22a4e6 100644 --- a/packages/nodes/src/solar-panel/definition.ts +++ b/packages/nodes/src/solar-panel/definition.ts @@ -51,13 +51,13 @@ export const solarPanelDefinition: NodeDefinition = { move: () => import('./move-tool'), }, toolHints: [ - { key: 'Left click', label: 'Place solar panel array on roof' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.solarPanel.toolHints.place' }, + { key: 'Esc', label: 'nodes.solarPanel.toolHints.cancel' }, ], presentation: { - label: 'Solar Panel', - description: 'Grid of photovoltaic panels mounted on a roof segment.', + label: 'nodes.solarPanel.label', + description: 'nodes.solarPanel.description', icon: { kind: 'url', src: '/icons/roof.png' }, paletteSection: 'structure', paletteOrder: 123, diff --git a/packages/nodes/src/solar-panel/geometry.ts b/packages/nodes/src/solar-panel/geometry.ts index b11e7a7a89..36aaa2287b 100644 --- a/packages/nodes/src/solar-panel/geometry.ts +++ b/packages/nodes/src/solar-panel/geometry.ts @@ -1,3 +1,5 @@ +'use client' + import { getActiveRoofHeight, getSegmentSlopeFrame, diff --git a/packages/nodes/src/solar-panel/panel.tsx b/packages/nodes/src/solar-panel/panel.tsx index 099510cc2f..0a2154147f 100644 --- a/packages/nodes/src/solar-panel/panel.tsx +++ b/packages/nodes/src/solar-panel/panel.tsx @@ -19,6 +19,7 @@ import { SegmentedControl, SliderControl, triggerSFX, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { LayoutGrid, Trash2 } from 'lucide-react' @@ -56,6 +57,7 @@ function num(value: unknown, fallback: number): number { } export default function SolarPanelPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) @@ -184,7 +186,7 @@ export default function SolarPanelPanel() { icon="/icons/roof.png" onBack={node.roofSegmentId ? handleBack : undefined} onClose={handleClose} - title={node.name || 'Solar Panel'} + title={node.name || t('nodes.solarPanel.fallbackTitle')} width={300} > diff --git a/packages/nodes/src/solar-panel/tool.tsx b/packages/nodes/src/solar-panel/tool.tsx index afed1a46ae..70fbd883ee 100644 --- a/packages/nodes/src/solar-panel/tool.tsx +++ b/packages/nodes/src/solar-panel/tool.tsx @@ -9,7 +9,7 @@ import { sceneRegistry, useScene, } from '@pascal-app/core' -import { triggerSFX } from '@pascal-app/editor' +import { triggerSFX, useTranslations } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import * as THREE from 'three' @@ -29,6 +29,7 @@ const worldPoint = new THREE.Vector3() * in the node so the renderer reproduces the same orientation. */ const SolarPanelTool = () => { + const t = useTranslations() const activeBuildingId = useViewer((s) => s.selection.buildingId) const setSelection = useViewer((s) => s.setSelection) @@ -96,6 +97,7 @@ const SolarPanelTool = () => { ) if (!hit) return const state = useScene.getState() + const panelCount = Object.values(state.nodes).filter((n) => (n as any).type === 'solarPanel').length // Use the raycast hit Y (segment-local) and analytical normal so the // committed panel sits exactly where the ghost was rendered. The @@ -106,7 +108,7 @@ const SolarPanelTool = () => { const panel = SolarPanelNode.parse({ ...solarPanelDefinition.defaults(), - name: 'Solar Panel', + name: t('nodes.solarPanel.defaultName', { count: panelCount + 1 }), roofSegmentId: hit.segment.id, position: [hit.localX, hit.localY, hit.localZ], rotation: 0, diff --git a/packages/nodes/src/spawn/definition.ts b/packages/nodes/src/spawn/definition.ts index 543f986991..35268d955b 100644 --- a/packages/nodes/src/spawn/definition.ts +++ b/packages/nodes/src/spawn/definition.ts @@ -52,12 +52,12 @@ export const spawnDefinition: NodeDefinition = { floorplan: buildSpawnFloorplan, tool: () => import('./tool'), toolHints: [ - { key: 'Left click', label: 'Place spawn point' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.spawn.toolHints.place' }, + { key: 'Esc', label: 'common.cancel' }, ], presentation: { - label: 'Spawn Point', + label: 'nodes.spawn.label', description: 'Player or camera origin within a level. One per level.', icon: { kind: 'url', src: '/icons/site.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/stair-segment/definition.ts b/packages/nodes/src/stair-segment/definition.ts index 125788855d..01eb0d04ac 100644 --- a/packages/nodes/src/stair-segment/definition.ts +++ b/packages/nodes/src/stair-segment/definition.ts @@ -131,7 +131,7 @@ export const stairSegmentDefinition: NodeDefinition = { }, presentation: { - label: 'Stair Segment', + label: 'nodes.stairSegment.label', description: 'A single flight of a parent stair.', icon: { kind: 'url', src: '/icons/stairs.png' }, paletteSection: 'structure', diff --git a/packages/nodes/src/stair/definition.ts b/packages/nodes/src/stair/definition.ts index 6f53cbb37f..18e5a128e5 100644 --- a/packages/nodes/src/stair/definition.ts +++ b/packages/nodes/src/stair/definition.ts @@ -365,9 +365,8 @@ export const stairDefinition: NodeDefinition = { }, presentation: { - label: 'Stair', - description: - 'A stair composed of one or more flights with configurable treads, risers, railings.', + label: 'nodes.stair.label', + description: 'nodes.stair.description', icon: { kind: 'url', src: '/icons/stairs.png' }, paletteSection: 'structure', paletteOrder: 110, diff --git a/packages/nodes/src/stair/panel.tsx b/packages/nodes/src/stair/panel.tsx index 3ecdc50575..74927934ef 100644 --- a/packages/nodes/src/stair/panel.tsx +++ b/packages/nodes/src/stair/panel.tsx @@ -26,6 +26,7 @@ import { ToggleControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Plus, Trash2 } from 'lucide-react' @@ -33,29 +34,32 @@ import { useCallback } from 'react' import { useShallow } from 'zustand/react/shallow' const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [ - { label: 'None', value: 'none' }, - { label: 'Left', value: 'left' }, - { label: 'Right', value: 'right' }, - { label: 'Both', value: 'both' }, + { label: 'nodes.stair.none', value: 'none' }, + { label: 'nodes.stair.left', value: 'left' }, + { label: 'nodes.stair.right', value: 'right' }, + { label: 'nodes.stair.both', value: 'both' }, ] const STAIR_TYPE_OPTIONS: { label: string; value: StairType }[] = [ - { label: 'Straight', value: 'straight' }, - { label: 'Curved', value: 'curved' }, - { label: 'Spiral', value: 'spiral' }, + { label: 'nodes.stair.straight', value: 'straight' }, + { label: 'nodes.stair.curved', value: 'curved' }, + { label: 'nodes.stair.spiral', value: 'spiral' }, ] const TOP_LANDING_MODE_OPTIONS: { label: string; value: StairTopLandingMode }[] = [ - { label: 'None', value: 'none' }, - { label: 'Integrated', value: 'integrated' }, + { label: 'nodes.stair.none', value: 'none' }, + { label: 'nodes.stair.integrated', value: 'integrated' }, ] const STAIR_SLAB_OPENING_OPTIONS: { label: string; value: StairSlabOpeningMode }[] = [ - { label: 'None', value: 'none' }, - { label: 'Destination', value: 'destination' }, + { label: 'nodes.stair.none', value: 'none' }, + { label: 'nodes.stair.destination', value: 'destination' }, ] export default function StairPanel() { + const t = useTranslations() + const resolveOpts = (opts: { label: string; value: any }[]) => + opts.map((o) => ({ ...o, label: t(o.label) })) const selectedId = useViewer((s) => s.selection.selectedIds[0]) const selectedCount = useViewer((s) => s.selection.selectedIds.length) const setSelection = useViewer((s) => s.setSelection) @@ -191,10 +195,10 @@ export default function StairPanel() { - + handleUpdate( @@ -207,16 +211,16 @@ export default function StairPanel() { : { stairType: value }, ) } - options={STAIR_TYPE_OPTIONS} + options={resolveOpts(STAIR_TYPE_OPTIONS)} value={node.stairType ?? 'straight'} /> - +
handleUpdate({ slabOpeningMode: checked ? 'destination' : 'none', @@ -226,7 +230,7 @@ export default function StairPanel() {
- From Level + {t('nodes.stair.fromLevel')}
handleUpdate({ slabOpeningMode: value as StairSlabOpeningMode })} - options={STAIR_SLAB_OPENING_OPTIONS} + options={resolveOpts(STAIR_SLAB_OPENING_OPTIONS)} value={node.slabOpeningMode ?? 'none'} /> {(node.slabOpeningMode ?? 'none') === 'destination' ? ( handleUpdate({ openingOffset: value })} @@ -280,7 +284,7 @@ export default function StairPanel() { {node.stairType === 'straight' && ( - +
{segments.map((seg, i) => (
@@ -310,9 +314,9 @@ export default function StairPanel() { )} {(node.stairType === 'curved' || node.stairType === 'spiral') && ( - + handleUpdate({ width: value })} @@ -322,7 +326,7 @@ export default function StairPanel() { value={Math.round((node.width ?? 1) * 100) / 100} /> handleUpdate({ totalRise: value })} @@ -332,7 +336,7 @@ export default function StairPanel() { value={Math.round((node.totalRise ?? 2.5) * 100) / 100} /> handleUpdate({ stepCount: Math.max(2, Math.round(value)) })} @@ -344,13 +348,13 @@ export default function StairPanel() { {node.stairType !== 'spiral' && ( handleUpdate({ fillToFloor: checked })} /> )} {(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && ( handleUpdate({ thickness: value })} @@ -361,7 +365,7 @@ export default function StairPanel() { /> )} handleUpdate({ innerRadius: value })} @@ -371,7 +375,7 @@ export default function StairPanel() { value={Math.round((node.innerRadius ?? 0.9) * 100) / 100} /> handleUpdate({ sweepAngle: (degrees * Math.PI) / 180 })} @@ -384,12 +388,12 @@ export default function StairPanel() { <> handleUpdate({ topLandingMode: value })} - options={TOP_LANDING_MODE_OPTIONS} + options={resolveOpts(TOP_LANDING_MODE_OPTIONS)} value={node.topLandingMode ?? 'none'} /> {(node.topLandingMode ?? 'none') === 'integrated' && ( handleUpdate({ topLandingDepth: value })} @@ -401,12 +405,12 @@ export default function StairPanel() { )} handleUpdate({ showCenterColumn: checked })} /> handleUpdate({ showStepSupports: checked })} /> @@ -414,7 +418,7 @@ export default function StairPanel() { )} - + { @@ -471,14 +475,14 @@ export default function StairPanel() { />
{ triggerSFX('sfx:item-rotate') handleUpdate({ rotation: node.rotation - Math.PI / 4 }) }} /> { triggerSFX('sfx:item-rotate') handleUpdate({ rotation: node.rotation + Math.PI / 4 }) @@ -487,15 +491,15 @@ export default function StairPanel() {
- + handleUpdate({ railingMode: value })} - options={RAILING_MODE_OPTIONS} + options={resolveOpts(RAILING_MODE_OPTIONS)} value={node.railingMode ?? 'none'} /> {(node.railingMode ?? 'none') !== 'none' && ( handleUpdate({ railingHeight: value })} @@ -507,18 +511,18 @@ export default function StairPanel() { )} - + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={handleDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/wall/definition.ts b/packages/nodes/src/wall/definition.ts index 2462a26253..dd54131930 100644 --- a/packages/nodes/src/wall/definition.ts +++ b/packages/nodes/src/wall/definition.ts @@ -107,14 +107,14 @@ export const wallDefinition: NodeDefinition = { floorplanSiblingOverrides: wallFloorplanSiblingOverrides, toolHints: [ - { key: 'Left click', label: 'Set wall start / end' }, - { key: 'Shift', label: 'Allow non-45° angles' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.wall.toolHints.setStartEnd' }, + { key: 'Shift', label: 'nodes.wall.toolHints.allowAngles' }, + { key: 'Esc', label: 'nodes.wall.toolHints.cancel' }, ], presentation: { - label: 'Wall', - description: 'A straight or curved wall segment. Hosts doors, windows, and wall-mounted items.', + label: 'nodes.wall.label', + description: 'nodes.wall.description', icon: { kind: 'url', src: '/icons/wall.png' }, paletteSection: 'structure', paletteOrder: 10, diff --git a/packages/nodes/src/wall/tool.tsx b/packages/nodes/src/wall/tool.tsx index d9d743053e..3828ce46e0 100644 --- a/packages/nodes/src/wall/tool.tsx +++ b/packages/nodes/src/wall/tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { calculateLevelMiters, emitter, @@ -21,6 +23,7 @@ import { type SegmentAngleReference, snapWallDraftPoint, triggerSFX, + useTranslations, WALL_FINE_GRID_STEP, type WallPlanPoint, } from '@pascal-app/editor' @@ -125,12 +128,12 @@ function getWallEndpointKind(point: WallPlanPoint, wall: WallNode): 'start' | 'e return null } -function buildDraftWall(start: WallPlanPoint, end: WallPlanPoint): WallNode { +function buildDraftWall(start: WallPlanPoint, end: WallPlanPoint, name: string): WallNode { return { object: 'node', id: 'wall_draft' as WallNode['id'], type: 'wall', - name: 'Draft wall', + name, parentId: null, visible: true, metadata: {}, @@ -259,10 +262,11 @@ function getDraftAngleLabels( end: WallPlanPoint, walls: WallNode[], baseY: number, + draftName: string, ): DraftAngleLabel[] { const draftFromStart: WallPlanPoint = [end[0] - start[0], end[1] - start[1]] const draftFromEnd: WallPlanPoint = [start[0] - end[0], start[1] - end[1]] - const draftWall = buildDraftWall(start, end) + const draftWall = buildDraftWall(start, end, draftName) const miterData = calculateLevelMiters([...walls, draftWall]) const endpoints = [ { id: 'start', point: start, draftVector: draftFromStart }, @@ -335,6 +339,7 @@ function getDraftMeasurementState( walls: WallNode[], unit: 'metric' | 'imperial', baseY: number, + draftName: string, ): DraftMeasurementState { const dx = end[0] - start[0] const dz = end[1] - start[1] @@ -343,7 +348,7 @@ function getDraftMeasurementState( return { lengthLabel: formatMeasurement(length, unit), lengthPosition: [(start[0] + end[0]) / 2, baseY + DRAFT_LABEL_Y, (start[1] + end[1]) / 2], - angleLabels: getDraftAngleLabels(start, end, walls, baseY), + angleLabels: getDraftAngleLabels(start, end, walls, baseY, draftName), } } @@ -381,6 +386,7 @@ function getCurrentLevelWalls(): WallNode[] { } export const WallTool: React.FC = () => { + const t = useTranslations() const unit = useViewer((state) => state.unit) const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark') const cursorRef = useRef(null) @@ -442,6 +448,7 @@ export const WallTool: React.FC = () => { walls, unit, startingPoint.current.y, + t('nodes.wall.draftName'), ), ) } else { diff --git a/packages/nodes/src/window/definition.ts b/packages/nodes/src/window/definition.ts index d083a4bde0..2a067c571d 100644 --- a/packages/nodes/src/window/definition.ts +++ b/packages/nodes/src/window/definition.ts @@ -176,13 +176,13 @@ export const windowDefinition: NodeDefinition = { }, toolHints: [ - { key: 'Left click', label: 'Place window on wall' }, - { key: 'Esc', label: 'Cancel' }, + { key: 'Left click', label: 'nodes.window.toolHints.place' }, + { key: 'Esc', label: 'nodes.window.toolHints.cancel' }, ], presentation: { - label: 'Window', - description: 'A window cut into a wall. Animated open/close for opening windows.', + label: 'nodes.window.label', + description: 'nodes.window.description', icon: { kind: 'url', src: '/icons/window.png' }, paletteSection: 'structure', paletteOrder: 60, diff --git a/packages/nodes/src/window/move-tool.tsx b/packages/nodes/src/window/move-tool.tsx index bbb0504ec1..1d75c13269 100644 --- a/packages/nodes/src/window/move-tool.tsx +++ b/packages/nodes/src/window/move-tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, diff --git a/packages/nodes/src/window/panel.tsx b/packages/nodes/src/window/panel.tsx index bdcfc82a36..479b84ad87 100644 --- a/packages/nodes/src/window/panel.tsx +++ b/packages/nodes/src/window/panel.tsx @@ -18,6 +18,7 @@ import { ToggleControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react' @@ -69,15 +70,15 @@ function isSameRadiusTuple( } const windowTypeOptions: Array<{ label: string; value: WindowNode['windowType'] }> = [ - { label: 'Fixed', value: 'fixed' }, - { label: 'Sliding', value: 'sliding' }, - { label: 'Casement', value: 'casement' }, - { label: 'Awning', value: 'awning' }, - { label: 'Single Hung', value: 'single-hung' }, - { label: 'Double Hung', value: 'double-hung' }, - { label: 'Bay', value: 'bay' }, - { label: 'Bow', value: 'bow' }, - { label: 'Louvered', value: 'louvered' }, + { label: 'nodes.window.typeOptions.fixed', value: 'fixed' }, + { label: 'nodes.window.typeOptions.sliding', value: 'sliding' }, + { label: 'nodes.window.typeOptions.casement', value: 'casement' }, + { label: 'nodes.window.typeOptions.awning', value: 'awning' }, + { label: 'nodes.window.typeOptions.singleHung', value: 'single-hung' }, + { label: 'nodes.window.typeOptions.doubleHung', value: 'double-hung' }, + { label: 'nodes.window.typeOptions.bay', value: 'bay' }, + { label: 'nodes.window.typeOptions.bow', value: 'bow' }, + { label: 'nodes.window.typeOptions.louvered', value: 'louvered' }, ] const shapedWindowTypes = new Set([ @@ -91,6 +92,7 @@ const shapedWindowTypes = new Set([ const silllessWindowTypes = new Set(['bay', 'bow']) export default function WindowPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const deleteNode = useScene((s) => s.deleteNode) @@ -292,13 +294,13 @@ export default function WindowPanel() { const showFlipSide = !isOpening const operationLabel = isTrackSashWindow ? windowType === 'sliding' - ? 'Slide' - : 'Raise' + ? t('nodes.window.operationLabels.slide') + : t('nodes.window.operationLabels.raise') : windowType === 'casement' - ? 'Swing' + ? t('nodes.window.operationLabels.swing') : windowType === 'louvered' - ? 'Slats' - : 'Tilt' + ? t('nodes.window.operationLabels.slats') + : t('nodes.window.operationLabels.tilt') const setOperationState = (value: number) => { useInteractive.getState().cancelWindowAnimation(node.id) @@ -383,10 +385,10 @@ export default function WindowPanel() { - + handleUpdate({ @@ -404,15 +406,15 @@ export default function WindowPanel() { }) } options={[ - { value: 'window', label: 'Window' }, - { value: 'opening', label: 'Opening' }, + { value: 'window', label: t('nodes.window.typeOptions.window') }, + { value: 'opening', label: t('nodes.window.typeOptions.opening') }, ]} value={node.openingKind ?? 'window'} /> {showWindowTypeSection && ( - +
{windowTypeOptions.map((option) => { const isSelected = displayedWindowType === option.value @@ -437,7 +439,7 @@ export default function WindowPanel() { } type="button" > - {option.label} + {t(option.label)} ) })} @@ -452,8 +454,8 @@ export default function WindowPanel() { }) } options={[ - { value: 'up', label: 'Up' }, - { value: 'down', label: 'Down' }, + { value: 'up', label: t('common.directions.up') }, + { value: 'down', label: t('common.directions.down') }, ]} value={awningDirection} /> @@ -466,8 +468,8 @@ export default function WindowPanel() { handleUpdate({ casementStyle: value as WindowNode['casementStyle'] }) } options={[ - { value: 'single', label: 'Single' }, - { value: 'french', label: 'French' }, + { value: 'single', label: t('nodes.window.casementStyleOptions.single') }, + { value: 'french', label: t('nodes.window.casementStyleOptions.french') }, ]} value={node.casementStyle ?? 'single'} /> @@ -477,8 +479,8 @@ export default function WindowPanel() { handleUpdate({ hingesSide: value as WindowNode['hingesSide'] }) } options={[ - { value: 'left', label: 'Left' }, - { value: 'right', label: 'Right' }, + { value: 'left', label: t('common.directions.left') }, + { value: 'right', label: t('common.directions.right') }, ]} value={node.hingesSide ?? 'left'} /> @@ -502,7 +504,7 @@ export default function WindowPanel() { )} - + @@ -532,16 +534,16 @@ export default function WindowPanel() { } - label="Flip Side" + label={t('nodes.window.flipSide')} onClick={handleFlip} />
)}
- + handleUpdate(getDimensionUpdates({ width: v }))} precision={2} @@ -551,7 +553,7 @@ export default function WindowPanel() { value={Math.round(node.width * 100) / 100} /> handleUpdate(getDimensionUpdates({ height: v }))} precision={2} @@ -563,7 +565,7 @@ export default function WindowPanel() { {showWindowShapeSection && ( - + handleUpdate({ @@ -581,9 +583,9 @@ export default function WindowPanel() { }) } options={[ - { value: 'rectangle', label: 'Rect' }, - { value: 'rounded', label: 'Rounded' }, - { value: 'arch', label: 'Arch' }, + { value: 'rectangle', label: t('nodes.window.topShapeOptions.rect') }, + { value: 'rounded', label: t('nodes.window.topShapeOptions.rounded') }, + { value: 'arch', label: t('nodes.window.topShapeOptions.arch') }, ]} value={windowShape} /> @@ -594,14 +596,14 @@ export default function WindowPanel() { handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] }) } options={[ - { value: 'all', label: 'All' }, - { value: 'individual', label: 'Individual' }, + { value: 'all', label: t('nodes.window.radiusModeOptions.all') }, + { value: 'individual', label: t('nodes.window.radiusModeOptions.individual') }, ]} value={openingRadiusMode} /> {openingRadiusMode === 'all' ? ( previewWindowUpdate('cornerRadius', value)} @@ -614,10 +616,10 @@ export default function WindowPanel() { ) : ( <> {[ - ['Top Left', 0], - ['Top Right', 1], - ['Bottom Right', 2], - ['Bottom Left', 3], + [t('common.corners.topLeft'), 0], + [t('common.corners.topRight'), 1], + [t('common.corners.bottomRight'), 2], + [t('common.corners.bottomLeft'), 3], ].map(([label, index]) => ( )} previewWindowUpdate('openingRevealRadius', value)} @@ -650,7 +652,7 @@ export default function WindowPanel() { {windowShape === 'arch' && (
handleUpdate({ archHeight: value })} @@ -666,15 +668,15 @@ export default function WindowPanel() { )} {showOpeningShapeSection && ( - + handleUpdate({ openingShape: value as WindowNode['openingShape'] }) } options={[ - { value: 'rectangle', label: 'Rect' }, - { value: 'rounded', label: 'Rounded' }, - { value: 'arch', label: 'Arch' }, + { value: 'rectangle', label: t('nodes.window.openingShapeOptions.rect') }, + { value: 'rounded', label: t('nodes.window.openingShapeOptions.rounded') }, + { value: 'arch', label: t('nodes.window.openingShapeOptions.arch') }, ]} value={openingShape} /> @@ -685,14 +687,14 @@ export default function WindowPanel() { handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] }) } options={[ - { value: 'all', label: 'All' }, - { value: 'individual', label: 'Individual' }, + { value: 'all', label: t('nodes.window.radiusModeOptions.all') }, + { value: 'individual', label: t('nodes.window.radiusModeOptions.individual') }, ]} value={openingRadiusMode} /> {openingRadiusMode === 'all' ? ( previewWindowUpdate('cornerRadius', value)} @@ -705,10 +707,10 @@ export default function WindowPanel() { ) : ( <> {[ - ['Top Left', 0], - ['Top Right', 1], - ['Bottom Right', 2], - ['Bottom Left', 3], + [t('common.corners.topLeft'), 0], + [t('common.corners.topRight'), 1], + [t('common.corners.bottomRight'), 2], + [t('common.corners.bottomLeft'), 3], ].map(([label, index]) => ( )} previewWindowUpdate('openingRevealRadius', value)} @@ -741,7 +743,7 @@ export default function WindowPanel() { {openingShape === 'arch' && (
handleUpdate({ archHeight: value })} @@ -759,9 +761,9 @@ export default function WindowPanel() { {!isOpening && ( <> {showFrameSection && ( - + handleUpdate({ frameThickness: v })} precision={3} @@ -770,7 +772,7 @@ export default function WindowPanel() { value={Math.round(node.frameThickness * 1000) / 1000} /> handleUpdate({ frameDepth: v })} precision={3} @@ -782,9 +784,9 @@ export default function WindowPanel() { )} {showGridSection && ( - + { @@ -796,7 +798,7 @@ export default function WindowPanel() { value={numCols} /> { @@ -811,7 +813,7 @@ export default function WindowPanel() { {numCols > 1 && (
- Col Widths + {t('nodes.window.colWidths')}
{normCols.map((ratio, i) => ( handleUpdate({ columnDividerThickness: v })} @@ -844,7 +846,7 @@ export default function WindowPanel() { {numRows > 1 && (
- Row Heights + {t('nodes.window.rowHeights')}
{normRows.map((ratio, i) => ( handleUpdate({ rowDividerThickness: v })} @@ -877,16 +879,16 @@ export default function WindowPanel() { )} {showSillSection && ( - + handleUpdate({ sill: checked })} /> {node.sill && (
handleUpdate({ sillDepth: v })} precision={3} @@ -895,7 +897,7 @@ export default function WindowPanel() { value={Math.round(node.sillDepth * 1000) / 1000} /> handleUpdate({ sillThickness: v })} precision={3} @@ -910,18 +912,18 @@ export default function WindowPanel() { )} - + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Duplicate" + label={t('common.duplicate')} onClick={handleDuplicate} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/window/tool.tsx b/packages/nodes/src/window/tool.tsx index c0b7b52856..d26a89dd88 100644 --- a/packages/nodes/src/window/tool.tsx +++ b/packages/nodes/src/window/tool.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, @@ -16,6 +18,7 @@ import { isValidWallSideFace, snapToHalf, triggerSFX, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef } from 'react' @@ -36,6 +39,7 @@ const edgeMaterial = new LineBasicNodeMaterial({ * Shows a rectangle cursor (green = valid, red = invalid) matching window dimensions. */ const WindowTool: React.FC = () => { + const t = useTranslations() const draftRef = useRef(null) const cursorGroupRef = useRef(null!) const edgesRef = useRef(null!) @@ -256,7 +260,7 @@ const WindowTool: React.FC = () => { const wall = n.parentId ? state.nodes[n.parentId as AnyNodeId] : undefined return wall?.parentId === levelId }).length - const name = `Window ${windowCount + 1}` + const name = t('nodes.window.defaultName', { count: windowCount + 1 }) const node = WindowNode.parse({ name, diff --git a/packages/nodes/src/zone/definition.ts b/packages/nodes/src/zone/definition.ts index a1e44d2e85..0b143e43b7 100644 --- a/packages/nodes/src/zone/definition.ts +++ b/packages/nodes/src/zone/definition.ts @@ -56,7 +56,7 @@ export const zoneDefinition: NodeDefinition = { }, presentation: { - label: 'Zone', + label: 'nodes.zone.label', description: 'A polygonal site zone (lawn, water, paving) with a TSL gradient material.', icon: { kind: 'url', src: '/icons/zone.png' }, paletteSection: 'site', diff --git a/packages/viewer/src/components/error-boundary.tsx b/packages/viewer/src/components/error-boundary.tsx index eada133aca..c23beb692e 100644 --- a/packages/viewer/src/components/error-boundary.tsx +++ b/packages/viewer/src/components/error-boundary.tsx @@ -1,3 +1,5 @@ +'use client' + import type { ErrorInfo, ReactNode } from 'react' import { Component } from 'react' diff --git a/packages/viewer/src/components/viewer/frame-limiter.tsx b/packages/viewer/src/components/viewer/frame-limiter.tsx index 962dd82692..ea9b6d0674 100644 --- a/packages/viewer/src/components/viewer/frame-limiter.tsx +++ b/packages/viewer/src/components/viewer/frame-limiter.tsx @@ -1,3 +1,5 @@ +'use client' + import { useThree } from '@react-three/fiber' import { useLayoutEffect } from 'react' diff --git a/packages/viewer/src/components/viewer/ground-occluder.tsx b/packages/viewer/src/components/viewer/ground-occluder.tsx index b939fb16f5..ded8666d9a 100644 --- a/packages/viewer/src/components/viewer/ground-occluder.tsx +++ b/packages/viewer/src/components/viewer/ground-occluder.tsx @@ -1,3 +1,5 @@ +'use client' + import { type LevelNode, useScene } from '@pascal-app/core' import { useMemo } from 'react' import * as THREE from 'three' diff --git a/packages/viewer/src/components/viewer/lights.tsx b/packages/viewer/src/components/viewer/lights.tsx index df4ef80d9b..8045566cfb 100644 --- a/packages/viewer/src/components/viewer/lights.tsx +++ b/packages/viewer/src/components/viewer/lights.tsx @@ -1,3 +1,5 @@ +'use client' + import { sceneRegistry } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' import { useMemo, useRef } from 'react' diff --git a/packages/viewer/src/components/viewer/perf-monitor.tsx b/packages/viewer/src/components/viewer/perf-monitor.tsx index 689b0a663e..b279fb0632 100644 --- a/packages/viewer/src/components/viewer/perf-monitor.tsx +++ b/packages/viewer/src/components/viewer/perf-monitor.tsx @@ -1,3 +1,5 @@ +'use client' + import { useScene } from '@pascal-app/core' import { Html } from '@react-three/drei' import { useFrame, useThree } from '@react-three/fiber' diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx index 67645cf5b4..2bd23700ac 100644 --- a/packages/viewer/src/components/viewer/post-processing.tsx +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -1,3 +1,5 @@ +'use client' + import { useFrame, useThree } from '@react-three/fiber' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Color, Layers, type Object3D, UnsignedByteType } from 'three' diff --git a/packages/viewer/src/components/viewer/scene-bvh.tsx b/packages/viewer/src/components/viewer/scene-bvh.tsx index abe1e3e61d..9995e3a17d 100644 --- a/packages/viewer/src/components/viewer/scene-bvh.tsx +++ b/packages/viewer/src/components/viewer/scene-bvh.tsx @@ -1,3 +1,5 @@ +'use client' + import { useThree } from '@react-three/fiber' import { forwardRef, type ReactNode, useEffect, useImperativeHandle, useRef } from 'react' import { type BufferGeometry, type Group, Mesh } from 'three' diff --git a/packages/viewer/src/components/viewer/viewer-camera.tsx b/packages/viewer/src/components/viewer/viewer-camera.tsx index adb24e9ff3..d81f724692 100644 --- a/packages/viewer/src/components/viewer/viewer-camera.tsx +++ b/packages/viewer/src/components/viewer/viewer-camera.tsx @@ -1,3 +1,5 @@ +'use client' + import { OrthographicCamera, PerspectiveCamera } from '@react-three/drei' import useViewer from '../../store/use-viewer' diff --git a/packages/viewer/src/hooks/use-asset-url.ts b/packages/viewer/src/hooks/use-asset-url.ts index 57bf04d371..8d0a7c1f1e 100644 --- a/packages/viewer/src/hooks/use-asset-url.ts +++ b/packages/viewer/src/hooks/use-asset-url.ts @@ -1,3 +1,5 @@ +'use client' + import { loadAssetUrl } from '@pascal-app/core' import { useEffect, useState } from 'react' diff --git a/packages/viewer/src/hooks/use-gltf-ktx2.tsx b/packages/viewer/src/hooks/use-gltf-ktx2.tsx index 040beaf7e4..a52ee14130 100644 --- a/packages/viewer/src/hooks/use-gltf-ktx2.tsx +++ b/packages/viewer/src/hooks/use-gltf-ktx2.tsx @@ -1,3 +1,5 @@ +'use client' + import { useGLTF } from '@react-three/drei' import { useThree } from '@react-three/fiber' import { KTX2Loader } from 'three/examples/jsm/Addons.js' diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index a33beab3c2..4e577152bc 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeType, diff --git a/packages/viewer/src/lib/ink-edges.ts b/packages/viewer/src/lib/ink-edges.ts index 19ea36fb9d..83eb0b3782 100644 --- a/packages/viewer/src/lib/ink-edges.ts +++ b/packages/viewer/src/lib/ink-edges.ts @@ -1,3 +1,5 @@ +'use client' + import { abs, colorToDirection, diff --git a/packages/viewer/src/lib/materials.ts b/packages/viewer/src/lib/materials.ts index 0e4b9264a7..d14cc9eaef 100644 --- a/packages/viewer/src/lib/materials.ts +++ b/packages/viewer/src/lib/materials.ts @@ -1,3 +1,5 @@ +'use client' + import { getMaterialPresetByRef, type MaterialMapProperties, diff --git a/packages/viewer/src/lib/merged-outline-node.ts b/packages/viewer/src/lib/merged-outline-node.ts index e1bb5ff7aa..0e94a2e1b7 100644 --- a/packages/viewer/src/lib/merged-outline-node.ts +++ b/packages/viewer/src/lib/merged-outline-node.ts @@ -1,5 +1,6 @@ // @ts-nocheck — Three.js TSL/WebGPU internal APIs have incomplete type definitions; // this file is a fork of OutlineNode and is intentionally exempt from strict TS checking. +'use client' /** * MergedOutlineNode — a fork of Three.js OutlineNode that processes two object diff --git a/packages/viewer/src/systems/ceiling/ceiling-system.tsx b/packages/viewer/src/systems/ceiling/ceiling-system.tsx index 43a2aa24a2..9e3b2dc9ae 100644 --- a/packages/viewer/src/systems/ceiling/ceiling-system.tsx +++ b/packages/viewer/src/systems/ceiling/ceiling-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type CeilingNode, diff --git a/packages/viewer/src/systems/column/column-geometry.ts b/packages/viewer/src/systems/column/column-geometry.ts index 3319cfd653..bf22cecf7b 100644 --- a/packages/viewer/src/systems/column/column-geometry.ts +++ b/packages/viewer/src/systems/column/column-geometry.ts @@ -1,3 +1,5 @@ +'use client' + import { BoxGeometry, type BufferGeometry, diff --git a/packages/viewer/src/systems/door/door-animation-system.tsx b/packages/viewer/src/systems/door/door-animation-system.tsx index 2c2f380ac7..33b04dbd07 100644 --- a/packages/viewer/src/systems/door/door-animation-system.tsx +++ b/packages/viewer/src/systems/door/door-animation-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type DoorNode, emitter, useInteractive, useScene } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' diff --git a/packages/viewer/src/systems/door/door-system.tsx b/packages/viewer/src/systems/door/door-system.tsx index 8ab66177bd..1a1077d4bd 100644 --- a/packages/viewer/src/systems/door/door-system.tsx +++ b/packages/viewer/src/systems/door/door-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, clampDoorOperationState, diff --git a/packages/viewer/src/systems/elevator/elevator-interaction-system.tsx b/packages/viewer/src/systems/elevator/elevator-interaction-system.tsx index 2b5699dba5..2187b007c8 100644 --- a/packages/viewer/src/systems/elevator/elevator-interaction-system.tsx +++ b/packages/viewer/src/systems/elevator/elevator-interaction-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, openElevatorDoor, diff --git a/packages/viewer/src/systems/fence/fence-system.tsx b/packages/viewer/src/systems/fence/fence-system.tsx index 31807dad09..d898270963 100644 --- a/packages/viewer/src/systems/fence/fence-system.tsx +++ b/packages/viewer/src/systems/fence/fence-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type FenceNode, diff --git a/packages/viewer/src/systems/floor-elevation/floor-elevation-system.tsx b/packages/viewer/src/systems/floor-elevation/floor-elevation-system.tsx index 01b52c2bde..f47e3a1c5c 100644 --- a/packages/viewer/src/systems/floor-elevation/floor-elevation-system.tsx +++ b/packages/viewer/src/systems/floor-elevation/floor-elevation-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, diff --git a/packages/viewer/src/systems/guide/guide-system.tsx b/packages/viewer/src/systems/guide/guide-system.tsx index bf0f951f11..b974a69bce 100644 --- a/packages/viewer/src/systems/guide/guide-system.tsx +++ b/packages/viewer/src/systems/guide/guide-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { sceneRegistry } from '@pascal-app/core' import { useEffect } from 'react' import useViewer from '../../store/use-viewer' diff --git a/packages/viewer/src/systems/item-light/item-light-system.tsx b/packages/viewer/src/systems/item-light/item-light-system.tsx index b0e2185a9e..f1cd8c23b2 100644 --- a/packages/viewer/src/systems/item-light/item-light-system.tsx +++ b/packages/viewer/src/systems/item-light/item-light-system.tsx @@ -1,3 +1,5 @@ +'use client' + import type { AnyNodeId, LevelNode } from '@pascal-app/core' import { sceneRegistry, useInteractive, useScene } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' diff --git a/packages/viewer/src/systems/item/item-system.tsx b/packages/viewer/src/systems/item/item-system.tsx index 8b86454eeb..2d4106d46e 100644 --- a/packages/viewer/src/systems/item/item-system.tsx +++ b/packages/viewer/src/systems/item/item-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, type ItemNode, diff --git a/packages/viewer/src/systems/level/level-system.tsx b/packages/viewer/src/systems/level/level-system.tsx index 8867d27b43..73878fc94f 100644 --- a/packages/viewer/src/systems/level/level-system.tsx +++ b/packages/viewer/src/systems/level/level-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type LevelNode, sceneRegistry, useScene } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' import { lerp } from 'three/src/math/MathUtils.js' diff --git a/packages/viewer/src/systems/roof/roof-system.tsx b/packages/viewer/src/systems/roof/roof-system.tsx index e2e6909492..ed3aea8e08 100644 --- a/packages/viewer/src/systems/roof/roof-system.tsx +++ b/packages/viewer/src/systems/roof/roof-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, diff --git a/packages/viewer/src/systems/scan/scan-system.tsx b/packages/viewer/src/systems/scan/scan-system.tsx index 5704c690ad..2d13ca70f7 100644 --- a/packages/viewer/src/systems/scan/scan-system.tsx +++ b/packages/viewer/src/systems/scan/scan-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { sceneRegistry } from '@pascal-app/core' import { useEffect } from 'react' import useViewer from '../../store/use-viewer' diff --git a/packages/viewer/src/systems/slab/slab-system.tsx b/packages/viewer/src/systems/slab/slab-system.tsx index 403f0438e7..b07c448187 100644 --- a/packages/viewer/src/systems/slab/slab-system.tsx +++ b/packages/viewer/src/systems/slab/slab-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, getRenderableSlabPolygon, diff --git a/packages/viewer/src/systems/stair/stair-system.tsx b/packages/viewer/src/systems/stair/stair-system.tsx index 545df04574..18a4f4451f 100644 --- a/packages/viewer/src/systems/stair/stair-system.tsx +++ b/packages/viewer/src/systems/stair/stair-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, diff --git a/packages/viewer/src/systems/wall/wall-cutout.tsx b/packages/viewer/src/systems/wall/wall-cutout.tsx index 89cc6bfafe..94a58612df 100644 --- a/packages/viewer/src/systems/wall/wall-cutout.tsx +++ b/packages/viewer/src/systems/wall/wall-cutout.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, sceneRegistry, useScene, type WallNode } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' import { useEffect, useRef } from 'react' diff --git a/packages/viewer/src/systems/wall/wall-materials.ts b/packages/viewer/src/systems/wall/wall-materials.ts index 3c807dc8ba..5e876d0200 100644 --- a/packages/viewer/src/systems/wall/wall-materials.ts +++ b/packages/viewer/src/systems/wall/wall-materials.ts @@ -1,3 +1,5 @@ +'use client' + import { getEffectiveWallSurfaceMaterial, getMaterialPresetByRef, diff --git a/packages/viewer/src/systems/wall/wall-system.tsx b/packages/viewer/src/systems/wall/wall-system.tsx index 5a578cc49c..233a171981 100644 --- a/packages/viewer/src/systems/wall/wall-system.tsx +++ b/packages/viewer/src/systems/wall/wall-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNode, type AnyNodeId, diff --git a/packages/viewer/src/systems/window/window-animation-system.tsx b/packages/viewer/src/systems/window/window-animation-system.tsx index bf6dbfd900..1db0dd7e41 100644 --- a/packages/viewer/src/systems/window/window-animation-system.tsx +++ b/packages/viewer/src/systems/window/window-animation-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, emitter, diff --git a/packages/viewer/src/systems/window/window-system.tsx b/packages/viewer/src/systems/window/window-system.tsx index e86413fe4d..23bc7c101e 100644 --- a/packages/viewer/src/systems/window/window-system.tsx +++ b/packages/viewer/src/systems/window/window-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { type AnyNodeId, getEffectiveNode, diff --git a/packages/viewer/src/systems/zone/zone-system.tsx b/packages/viewer/src/systems/zone/zone-system.tsx index 917def3af0..7fa17e9395 100644 --- a/packages/viewer/src/systems/zone/zone-system.tsx +++ b/packages/viewer/src/systems/zone/zone-system.tsx @@ -1,3 +1,5 @@ +'use client' + import { sceneRegistry, useScene } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' import { useRef } from 'react' From f7359f9e2838e905ad9fa527b919f6dd1c79489a Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 09:25:02 +0800 Subject: [PATCH 002/106] =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=962?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ifc-converter/components/IfcConverter.tsx | 6 +- .../components/PreviewToolbar.tsx | 6 +- .../editor/first-person-controls.tsx | 18 +- .../src/components/editor/floorplan-panel.tsx | 4 +- .../editor/src/components/editor/index.tsx | 14 +- .../ui/action-menu/view-toggles.tsx | 11 +- .../ui/controls/material-picker.tsx | 15 +- .../components/ui/floating-level-selector.tsx | 4 +- .../components/ui/helpers/building-helper.tsx | 10 +- .../ui/helpers/registered-tool-helper.tsx | 4 +- .../src/components/ui/helpers/roof-helper.tsx | 6 +- .../ui/panels/mobile-panel-sheet.tsx | 4 +- .../ui/panels/mobile-selection-bar.tsx | 11 +- .../src/components/ui/panels/paint-panel.tsx | 6 +- .../ui/panels/parametric-inspector.tsx | 8 +- .../components/ui/panels/reference-panel.tsx | 34 +-- .../ui/primitives/error-boundary.tsx | 41 ++-- .../src/components/ui/primitives/sidebar.tsx | 9 +- .../panels/site-panel/building-tree-node.tsx | 4 +- .../ui/sidebar/panels/site-panel/index.tsx | 10 +- packages/editor/src/lib/i18n/en.json | 199 ++++++++++++++++++ packages/editor/src/lib/i18n/zh.json | 196 +++++++++++++++++ packages/nodes/src/box-vent/panel.tsx | 50 ++--- packages/nodes/src/ceiling/panel.tsx | 2 +- packages/nodes/src/chimney/panel.tsx | 126 +++++------ packages/nodes/src/column/panel.tsx | 116 +++++----- .../src/dormer/panel-position-section.tsx | 6 +- .../nodes/src/dormer/panel-window-section.tsx | 4 +- packages/nodes/src/dormer/panel.tsx | 34 +-- packages/nodes/src/elevator/panel.tsx | 120 +++++------ .../nodes/src/fence/inspector-editors.tsx | 8 +- packages/nodes/src/item/panel.tsx | 30 +-- packages/nodes/src/ridge-vent/panel.tsx | 42 ++-- packages/nodes/src/roof-segment/panel.tsx | 95 +++++---- packages/nodes/src/roof/panel.tsx | 8 +- packages/nodes/src/skylight/panel.tsx | 72 ++++--- packages/nodes/src/slab/panel.tsx | 30 +-- packages/nodes/src/solar-panel/panel.tsx | 40 ++-- packages/nodes/src/spawn/panel.tsx | 22 +- packages/nodes/src/stair-segment/panel.tsx | 67 +++--- packages/nodes/src/stair/panel.tsx | 6 +- packages/nodes/src/wall/panel.tsx | 18 +- 42 files changed, 996 insertions(+), 520 deletions(-) diff --git a/apps/ifc-converter/components/IfcConverter.tsx b/apps/ifc-converter/components/IfcConverter.tsx index 6e9c2f3025..abcac1fb3c 100644 --- a/apps/ifc-converter/components/IfcConverter.tsx +++ b/apps/ifc-converter/components/IfcConverter.tsx @@ -4,6 +4,7 @@ import { convertIfcToPascal, type PascalSceneGraph } from '@pascal-app/ifc-conve import dynamic from 'next/dynamic' import { useCallback, useEffect, useMemo, useState } from 'react' import { availableTestFiles, exampleFileUrl, testFiles } from '@/lib/test-files' +import { useTranslations } from '../../packages/editor/src/lib/i18n' // The viewer uses three's WebGPU renderer + the registry-driven scene // store, neither of which run during SSR — dynamic-import with ssr:false @@ -32,6 +33,7 @@ function meta(node: { metadata?: unknown } | null | undefined): ConverterMetadat } export default function IfcConverter() { + const t = useTranslations() const [pascalData, setPascalData] = useState(null) const [status, setStatus] = useState('idle') const [error, setError] = useState(null) @@ -687,7 +689,7 @@ export default function IfcConverter() {
{!hasPlacedSpawn && (
- Place a Spawn Point from the Build tab to control where walkthrough starts. + {t('viewer.spawnPointHint')}
)} @@ -1356,15 +1358,15 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => { {isLocked && (
- +
- - - - + + + +
- Click to look around + {t('editor.clickToLookAround')}
diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index e41a9ab753..85ed13dcbb 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -71,6 +71,7 @@ import { import { guideEmitter } from '../../lib/guide-events' import { sfxEmitter } from '../../lib/sfx-bus' import { cn } from '../../lib/utils' +import { useTranslations } from '../../lib/i18n' import type { GuideUiState } from '../../store/use-editor' import useEditor from '../../store/use-editor' import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay' @@ -4043,6 +4044,7 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({ }) export function FloorplanPanel() { + const t = useTranslations() const viewportHostRef = useRef(null) const svgRef = useRef(null) const floorplanSceneRef = useRef(null) @@ -8637,7 +8639,7 @@ export function FloorplanPanel() {
-
Set overlay scale
+
{t('editor.setOverlayScale')}
Enter the real-world length of the line you just drew. The image will resize to match it. diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index f8366bad5f..353140137b 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -28,7 +28,7 @@ import { writePersistedSelection, } from '../../lib/scene' import { initSFXBus } from '../../lib/sfx-bus' -import { messages, useLocale } from '../../lib/i18n' +import { messages, useLocale, useTranslations } from '../../lib/i18n' import useEditor from '../../store/use-editor' import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system' import { CeilingSystem } from '../systems/ceiling/ceiling-system' @@ -165,10 +165,11 @@ export interface EditorProps { } function EditorSceneCrashFallback() { + const t = useTranslations() return (
-

The editor scene failed to render

+

{t('editor.sceneFailedToRender')}

You can retry the scene or return home without reloading the whole app shell.

@@ -178,7 +179,7 @@ function EditorSceneCrashFallback() { onClick={() => window.location.reload()} type="button" > - Reload editor + {t('editor.reloadEditor')} s.width) const isCollapsed = useSidebarStore((s) => s.isCollapsed) const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed) @@ -266,7 +268,7 @@ function SidebarSlot({ children }: { children: ReactNode }) {
) : ( children @@ -469,7 +471,7 @@ function ViewerCanvasControlsHint({ return (
@@ -480,7 +482,7 @@ function ViewerCanvasControlsHint({ ))}
@@ -206,7 +211,7 @@ export function MaterialPicker({ : 'border-gray-300 hover:border-gray-400' }`} onClick={handleCustomOpen} - title="Custom" + title={t('editor.custom')} type="button" > Custom diff --git a/packages/editor/src/components/ui/floating-level-selector.tsx b/packages/editor/src/components/ui/floating-level-selector.tsx index 3e8d5c535f..0073346f07 100644 --- a/packages/editor/src/components/ui/floating-level-selector.tsx +++ b/packages/editor/src/components/ui/floating-level-selector.tsx @@ -41,7 +41,7 @@ import { buildLevelDuplicateCreateOps, type LevelDuplicatePreset, } from '../../lib/level-duplication' -import { useLocale, messages } from '../../lib/i18n' +import { useLocale, messages, useTranslations } from '../../lib/i18n' import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name' import { deleteLevelWithFallbackSelection } from '../../lib/level-selection' import { @@ -220,7 +220,7 @@ function LevelRow({ type="button" > - Duplicate level + {t('editor.duplicateLevel')}
) diff --git a/packages/editor/src/components/ui/helpers/registered-tool-helper.tsx b/packages/editor/src/components/ui/helpers/registered-tool-helper.tsx index cc48a3fbac..a4216c8b91 100644 --- a/packages/editor/src/components/ui/helpers/registered-tool-helper.tsx +++ b/packages/editor/src/components/ui/helpers/registered-tool-helper.tsx @@ -1,3 +1,4 @@ +import { useIntl } from 'react-intl' import type { ToolHint } from '@pascal-app/core' import { ShortcutToken } from '../primitives/shortcut-token' @@ -11,13 +12,14 @@ import { ShortcutToken } from '../primitives/shortcut-token' * their hints as static data in their `NodeDefinition`. */ export function RegisteredToolHelper({ hints }: { hints: ToolHint[] }) { + const { formatMessage } = useIntl() if (hints.length === 0) return null return (
{hints.map((hint) => (
- {hint.label} + {formatMessage({ id: hint.label })}
))}
diff --git a/packages/editor/src/components/ui/helpers/roof-helper.tsx b/packages/editor/src/components/ui/helpers/roof-helper.tsx index 6dea4a4f4a..52747c8fa7 100644 --- a/packages/editor/src/components/ui/helpers/roof-helper.tsx +++ b/packages/editor/src/components/ui/helpers/roof-helper.tsx @@ -1,15 +1,17 @@ +import { useTranslations } from '../../../lib/i18n' import { ShortcutToken } from '../primitives/shortcut-token' export function RoofHelper() { + const t = useTranslations() return (
- Set corner + {t('editor.setCorner')}
- Cancel + {t('common.cancel')}
) diff --git a/packages/editor/src/components/ui/panels/mobile-panel-sheet.tsx b/packages/editor/src/components/ui/panels/mobile-panel-sheet.tsx index 8239ea48a4..685f01e908 100644 --- a/packages/editor/src/components/ui/panels/mobile-panel-sheet.tsx +++ b/packages/editor/src/components/ui/panels/mobile-panel-sheet.tsx @@ -5,6 +5,7 @@ import { AnimatePresence, motion } from 'motion/react' import Image from 'next/image' import { type ReactNode, useEffect, useState } from 'react' import { createPortal } from 'react-dom' +import { useTranslations } from '../../../lib/i18n' import useEditor from '../../../store/use-editor' interface MobilePanelSheetProps { @@ -19,6 +20,7 @@ const HEIGHT_VH = 50 const DRAG_CLOSE_THRESHOLD_PX = 120 export function MobilePanelSheet({ open, onClose, icon, title, children }: MobilePanelSheetProps) { + const t = useTranslations() const [mounted, setMounted] = useState(false) const setMobilePanelSheetHeight = useEditor((s) => s.setMobilePanelSheetHeight) @@ -88,7 +90,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
-
+ this.setState({ hasError: false, error: null })} + /> ) } return this.props.children } } + +function ErrorBoundaryFallback({ + errorMessage, + onReset, +}: { + errorMessage?: string + onReset: () => void +}) { + const t = useTranslations() + return ( +
+

{t('editor.somethingWentWrong')}

+
+        {errorMessage}
+      
+ +
+ ) +} diff --git a/packages/editor/src/components/ui/primitives/sidebar.tsx b/packages/editor/src/components/ui/primitives/sidebar.tsx index 6ec2ea1b01..3eb21213a3 100644 --- a/packages/editor/src/components/ui/primitives/sidebar.tsx +++ b/packages/editor/src/components/ui/primitives/sidebar.tsx @@ -24,6 +24,7 @@ import { TooltipTrigger, } from './../../../components/ui/primitives/tooltip' import { useIsMobile } from './../../../hooks/use-mobile' +import { useTranslations } from './../../../lib/i18n' import { cn } from './../../../lib/utils' const SIDEBAR_COOKIE_NAME = 'sidebar_state' @@ -245,6 +246,7 @@ function Sidebar({ variant?: 'sidebar' | 'floating' | 'inset' collapsible?: 'offcanvas' | 'icon' | 'none' }) { + const t = useTranslations() const { isMobile, state, openMobile, setOpenMobile } = useSidebar() if (collapsible === 'none') { @@ -279,7 +281,7 @@ function Sidebar({ > Sidebar - Displays the mobile sidebar. + {t('editor.displaysMobileSidebar')}
{children}
@@ -362,10 +364,11 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { const { toggleSidebar } = useSidebar() + const t = useTranslations() return ( - Add new level + {t('editor.addNewLevel')}
} diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx index 1e75a008bb..c98e508663 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx @@ -236,7 +236,7 @@ const PropertyLineSection = memo(function PropertyLineSection() { onClick={handleAddPoint} > - Add point + {t('editor.addPoint')}
)} @@ -787,7 +787,7 @@ const LevelItem = memo(function LevelItem({ }} > - View snapshot + {t('editor.viewSnapshot')} )} )} )}
- + {node.holes && node.holes.length > 0 ? (
{node.holes.map((hole, index) => { diff --git a/packages/nodes/src/chimney/panel.tsx b/packages/nodes/src/chimney/panel.tsx index 32a2a313e6..377ca88a46 100644 --- a/packages/nodes/src/chimney/panel.tsx +++ b/packages/nodes/src/chimney/panel.tsx @@ -40,18 +40,18 @@ const cn = (...classes: Array): string => type ChimneyType = 'cap' | 'flues' | 'shoulder' | 'bands' | 'cricket' | 'panels' -const CHIMNEY_TYPE_OPTIONS: Array<{ label: string; value: ChimneyType }> = [ - { label: 'Cap', value: 'cap' }, - { label: 'Flues', value: 'flues' }, - { label: 'Shoulder', value: 'shoulder' }, - { label: 'Bands', value: 'bands' }, - { label: 'Cricket', value: 'cricket' }, - { label: 'Panels', value: 'panels' }, -] - export default function ChimneyPanel() { const t = useTranslations() const [chimneyType, setChimneyType] = useState('cap') + + const CHIMNEY_TYPE_OPTIONS: Array<{ label: string; value: ChimneyType }> = [ + { label: t('nodes.chimney.cap'), value: 'cap' }, + { label: t('nodes.chimney.flues'), value: 'flues' }, + { label: t('nodes.chimney.shoulder'), value: 'shoulder' }, + { label: t('nodes.chimney.bands'), value: 'bands' }, + { label: t('nodes.chimney.cricket'), value: 'cricket' }, + { label: t('nodes.chimney.panels'), value: 'panels' }, + ] const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) @@ -346,7 +346,7 @@ export default function ChimneyPanel() { title={node.name || t('nodes.chimney.fallbackTitle')} width={300} > - + applyPreset(v as ChimneyPresetKey)} options={CHIMNEY_PRESET_KEYS.map((k) => ({ @@ -359,12 +359,12 @@ export default function ChimneyPanel() { /> - + handleUpdate({ bodyShape: v })} options={[ - { label: 'Square', value: 'square' }, - { label: 'Round', value: 'round' }, + { label: t('nodes.chimney.square'), value: 'square' }, + { label: t('nodes.chimney.round'), value: 'round' }, ]} value={node.bodyShape ?? 'square'} /> @@ -382,7 +382,7 @@ export default function ChimneyPanel() { /> {(node.bodyShape ?? 'square') !== 'round' && ( previewProp({ depth: v })} @@ -395,7 +395,7 @@ export default function ChimneyPanel() { /> )} previewProp({ bodyHollowDepth: v })} @@ -407,7 +407,7 @@ export default function ChimneyPanel() { value={Math.round((node.bodyHollowDepth ?? 0.6) * 100) / 100} /> previewProp({ bodyHollowMargin: v })} @@ -420,7 +420,7 @@ export default function ChimneyPanel() { /> {(node.bodyShape ?? 'square') !== 'round' && ( previewProp({ cornerBevel: v })} @@ -434,9 +434,9 @@ export default function ChimneyPanel() { )} - + previewProp({ heightAboveRidge: v })} @@ -448,7 +448,7 @@ export default function ChimneyPanel() { value={Math.round(node.heightAboveRidge * 100) / 100} /> previewProp({ cutoutOffset: v })} @@ -461,9 +461,9 @@ export default function ChimneyPanel() { /> - + { @@ -482,7 +482,7 @@ export default function ChimneyPanel() { value={Math.round(worldX_now * 100) / 100} /> { @@ -498,7 +498,7 @@ export default function ChimneyPanel() { value={Math.round(worldZ_now * 100) / 100} /> { @@ -524,7 +524,7 @@ export default function ChimneyPanel() { /> - +
{CHIMNEY_TYPE_OPTIONS.filter((option) => { // Cricket and Panels both rely on a flat face — hide them for @@ -559,17 +559,17 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ cap: v !== 'none', capShape: v })} options={[ - { label: 'None', value: 'none' }, - { label: 'Sloped', value: 'sloped' }, - { label: 'Flat', value: 'flat' }, - { label: 'Stepped', value: 'stepped' }, + { label: t('nodes.chimney.none'), value: 'none' }, + { label: t('nodes.chimney.sloped'), value: 'sloped' }, + { label: t('nodes.chimney.flat'), value: 'flat' }, + { label: t('nodes.chimney.stepped'), value: 'stepped' }, ]} value={node.capShape ?? 'sloped'} /> {(node.capShape ?? 'sloped') !== 'none' && ( <> previewProp({ capOverhang: v })} @@ -581,7 +581,7 @@ export default function ChimneyPanel() { value={Math.round((node.capOverhang ?? 0.04) * 1000) / 1000} /> previewProp({ capThickness: v })} @@ -603,16 +603,16 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ shoulderStyle: v })} options={[ - { label: 'None', value: 'none' }, - { label: 'Tapered', value: 'tapered' }, - { label: 'Corbeled', value: 'corbeled' }, + { label: t('nodes.chimney.none'), value: 'none' }, + { label: t('nodes.chimney.tapered'), value: 'tapered' }, + { label: t('nodes.chimney.corbeled'), value: 'corbeled' }, ]} value={node.shoulderStyle ?? 'none'} /> {(node.shoulderStyle ?? 'none') !== 'none' && ( <> previewProp({ shoulderHeight: v })} @@ -624,7 +624,7 @@ export default function ChimneyPanel() { value={Math.round((node.shoulderHeight ?? 0.5) * 100) / 100} /> previewProp({ shoulderExtent: v })} @@ -643,7 +643,7 @@ export default function ChimneyPanel() { {chimneyType === 'flues' && ( <> previewProp({ flueCount: Math.round(v) })} @@ -659,13 +659,13 @@ export default function ChimneyPanel() { handleUpdate({ flueShape: v })} options={[ - { label: 'Round', value: 'round' }, - { label: 'Square', value: 'square' }, + { label: t('nodes.chimney.round'), value: 'round' }, + { label: t('nodes.chimney.square'), value: 'square' }, ]} value={node.flueShape ?? 'round'} /> previewProp({ flueDiameter: v })} @@ -677,7 +677,7 @@ export default function ChimneyPanel() { value={Math.round((node.flueDiameter ?? 0.22) * 100) / 100} /> previewProp({ flueHeight: v })} @@ -690,7 +690,7 @@ export default function ChimneyPanel() { /> {(node.flueCount ?? 1) > 1 && ( previewProp({ flueSpacing: v })} @@ -702,7 +702,7 @@ export default function ChimneyPanel() { /> )} previewProp({ flueWallThickness: v })} @@ -724,16 +724,16 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ bandStyle: v })} options={[ - { label: 'None', value: 'none' }, - { label: 'Single', value: 'single' }, - { label: 'Double', value: 'double' }, + { label: t('nodes.chimney.none'), value: 'none' }, + { label: t('nodes.chimney.single'), value: 'single' }, + { label: t('nodes.chimney.double'), value: 'double' }, ]} value={node.bandStyle ?? 'none'} /> {(node.bandStyle ?? 'none') !== 'none' && ( <> previewProp({ bandHeight: v })} @@ -745,7 +745,7 @@ export default function ChimneyPanel() { value={Math.round((node.bandHeight ?? 0.1) * 100) / 100} /> previewProp({ bandExtent: v })} @@ -757,7 +757,7 @@ export default function ChimneyPanel() { value={Math.round((node.bandExtent ?? 0.04) * 1000) / 1000} /> previewProp({ bandOffset: v })} @@ -779,8 +779,8 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ cricketStyle: v })} options={[ - { label: 'None', value: 'none' }, - { label: 'Simple', value: 'simple' }, + { label: t('nodes.chimney.none'), value: 'none' }, + { label: t('nodes.chimney.simple'), value: 'simple' }, ]} value={node.cricketStyle ?? 'none'} /> @@ -790,13 +790,13 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ cricketSide: v })} options={[ - { label: 'Front', value: 'front' }, - { label: 'Back', value: 'back' }, + { label: t('nodes.chimney.front'), value: 'front' }, + { label: t('nodes.chimney.back'), value: 'back' }, ]} value={node.cricketSide ?? 'front'} /> previewProp({ cricketLength: v })} @@ -808,7 +808,7 @@ export default function ChimneyPanel() { value={Math.round((node.cricketLength ?? 0.6) * 100) / 100} /> previewProp({ cricketHeight: v })} @@ -830,15 +830,15 @@ export default function ChimneyPanel() { className="mt-2" onChange={(v) => handleUpdate({ panelStyle: v })} options={[ - { label: 'None', value: 'none' }, - { label: 'Rectangular', value: 'rectangular' }, + { label: t('nodes.chimney.none'), value: 'none' }, + { label: t('nodes.chimney.rectangular'), value: 'rectangular' }, ]} value={node.panelStyle ?? 'none'} /> {(node.panelStyle ?? 'none') !== 'none' && ( <> previewProp({ panelDepth: v })} @@ -850,7 +850,7 @@ export default function ChimneyPanel() { value={Math.round((node.panelDepth ?? 0.03) * 1000) / 1000} /> previewProp({ panelHeight: v })} @@ -862,7 +862,7 @@ export default function ChimneyPanel() { value={Math.round((node.panelHeight ?? 0.8) * 100) / 100} /> previewProp({ panelOffsetTop: v })} @@ -874,7 +874,7 @@ export default function ChimneyPanel() { value={Math.round((node.panelOffsetTop ?? 0.15) * 100) / 100} /> previewProp({ panelMargin: v })} @@ -891,12 +891,12 @@ export default function ChimneyPanel() { )} - + } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/column/panel.tsx b/packages/nodes/src/column/panel.tsx index 4e29ff1a14..5475bcb16e 100644 --- a/packages/nodes/src/column/panel.tsx +++ b/packages/nodes/src/column/panel.tsx @@ -413,7 +413,7 @@ export default function ColumnPanel() { [ { value: 'round', - label: 'Round', + label: t('nodes.column.round'), icon: ( - + {!isBraceSupport && ( @@ -672,14 +672,14 @@ export default function ColumnPanel() { } value={shaftProfile} > - - - - + + + + {shaftProfile === 'straight' && ( handleUpdate({ shaftStartScale: value, shaftEndScale: value })} @@ -691,7 +691,7 @@ export default function ColumnPanel() { {shaftProfile === 'tapered' && ( <> handleUpdate({ shaftStartScale: value })} @@ -700,7 +700,7 @@ export default function ColumnPanel() { value={node.shaftStartScale ?? 0.82} /> handleUpdate({ shaftEndScale: value })} @@ -709,7 +709,7 @@ export default function ColumnPanel() { value={node.shaftEndScale ?? 0.72} /> handleUpdate({ shaftTaper: value })} @@ -722,7 +722,7 @@ export default function ColumnPanel() { {shaftProfile === 'bulged' && ( <> handleUpdate({ shaftStartScale: value, shaftEndScale: value })} @@ -731,7 +731,7 @@ export default function ColumnPanel() { value={node.shaftStartScale ?? 0.68} /> handleUpdate({ shaftBulge: value })} @@ -744,7 +744,7 @@ export default function ColumnPanel() { {shaftProfile === 'hourglass' && ( <> handleUpdate({ shaftStartScale: value, shaftEndScale: value })} @@ -753,7 +753,7 @@ export default function ColumnPanel() { value={node.shaftStartScale ?? 0.84} /> handleUpdate({ shaftBulge: value })} @@ -764,7 +764,7 @@ export default function ColumnPanel() { )} @@ -782,7 +782,7 @@ export default function ColumnPanel() { /> {Math.abs(node.shaftTwistStep ?? 0) > 0.001 && ( handleUpdate({ shaftSegmentCount: Math.round(value) })} @@ -792,7 +792,7 @@ export default function ColumnPanel() { /> )} @@ -809,7 +809,7 @@ export default function ColumnPanel() { /> {(node.ringCount ?? 0) > 0 && ( handleUpdate({ ringThickness: value })} @@ -821,7 +821,7 @@ export default function ColumnPanel() { )} {(node.ringCount ?? 0) > 0 && ( handleUpdate({ ringSpread: value, ringPlacement: 'ends' })} @@ -834,7 +834,7 @@ export default function ColumnPanel() { )} {!isBraceSupport && ( - + {node.capitalStyle !== 'none' && ( handleUpdate({ capitalHeight: value })} @@ -885,7 +885,7 @@ export default function ColumnPanel() { )} {node.capitalStyle !== 'none' && ( @@ -901,7 +901,7 @@ export default function ColumnPanel() { )} {node.capitalStyle !== 'none' && node.crossSection === 'rectangular' && ( handleUpdate({ capitalDepthScale: value })} @@ -912,7 +912,7 @@ export default function ColumnPanel() { )} {node.capitalStyle === 'stepped' && ( handleUpdate({ capitalTierCount: Math.round(value) })} @@ -923,7 +923,7 @@ export default function ColumnPanel() { )} {node.capitalStyle === 'stepped' && ( handleUpdate({ capitalStepSpread: value })} @@ -975,15 +975,15 @@ export default function ColumnPanel() { }} value={node.baseStyle ?? 'square-plinth'} > - - - - - + + + + + {node.baseStyle !== 'none' && ( handleUpdate({ baseHeight: value })} @@ -995,7 +995,7 @@ export default function ColumnPanel() { )} {node.baseStyle !== 'none' && ( @@ -1011,7 +1011,7 @@ export default function ColumnPanel() { )} {node.baseStyle !== 'none' && node.crossSection === 'rectangular' && ( handleUpdate({ baseDepthScale: value })} @@ -1022,7 +1022,7 @@ export default function ColumnPanel() { )} {node.baseStyle === 'round-rings' && ( handleUpdate({ basePlinthHeightRatio: value })} @@ -1033,7 +1033,7 @@ export default function ColumnPanel() { )} {node.baseStyle === 'round-rings' && ( handleUpdate({ baseRoundBandScale: value })} @@ -1044,7 +1044,7 @@ export default function ColumnPanel() { )} {node.baseStyle === 'round-rings' && ( handleUpdate({ baseNeckScale: value })} @@ -1055,7 +1055,7 @@ export default function ColumnPanel() { )} {node.baseStyle === 'stepped-square' && ( handleUpdate({ baseTierCount: Math.round(value) })} @@ -1066,7 +1066,7 @@ export default function ColumnPanel() { )} {node.baseStyle === 'stepped-square' && ( handleUpdate({ baseStepSpread: value })} @@ -1078,9 +1078,9 @@ export default function ColumnPanel() { )} - + handleUpdate({ rotation: (value * Math.PI) / 180 })} @@ -1091,13 +1091,13 @@ export default function ColumnPanel() { /> - + - } label="Move" onClick={handleMove} /> + } label={t('common.move')} onClick={handleMove} /> } - label="Delete" + label={t('common.delete')} onClick={handleDelete} /> diff --git a/packages/nodes/src/dormer/panel-position-section.tsx b/packages/nodes/src/dormer/panel-position-section.tsx index fca882d15b..3698cd7096 100644 --- a/packages/nodes/src/dormer/panel-position-section.tsx +++ b/packages/nodes/src/dormer/panel-position-section.tsx @@ -168,7 +168,7 @@ export function DormerPositionSection({ return ( { @@ -184,7 +184,7 @@ export function DormerPositionSection({ value={Math.round(worldX_now * 100) / 100} /> { @@ -200,7 +200,7 @@ export function DormerPositionSection({ value={Math.round(worldZ_now * 100) / 100} /> { diff --git a/packages/nodes/src/dormer/panel-window-section.tsx b/packages/nodes/src/dormer/panel-window-section.tsx index 759a72f14e..274c852c0b 100644 --- a/packages/nodes/src/dormer/panel-window-section.tsx +++ b/packages/nodes/src/dormer/panel-window-section.tsx @@ -98,7 +98,7 @@ export function DormerWindowSection({ value={Math.round(node.windowHeight * 100) / 100} /> previewProp({ windowOffsetX: v })} @@ -110,7 +110,7 @@ export function DormerWindowSection({ value={Math.round(node.windowOffsetX * 100) / 100} /> previewProp({ windowOffsetY: v })} diff --git a/packages/nodes/src/dormer/panel.tsx b/packages/nodes/src/dormer/panel.tsx index 49a54c98fe..e38bf83bf6 100644 --- a/packages/nodes/src/dormer/panel.tsx +++ b/packages/nodes/src/dormer/panel.tsx @@ -19,7 +19,7 @@ import { useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { DormerActionsSection } from './panel-actions-section' import { DormerPositionSection } from './panel-position-section' import { DormerWindowSection } from './panel-window-section' @@ -28,23 +28,33 @@ type RoofType = DormerNode['roofType'] type DormerSection = 'dormer' | 'window' const ROOF_TYPE_OPTIONS: Array<{ label: string; value: RoofType }> = [ - { label: 'Gable', value: 'gable' }, - { label: 'Hip', value: 'hip' }, - { label: 'Shed', value: 'shed' }, - { label: 'Gambrel', value: 'gambrel' }, - { label: 'Dutch', value: 'dutch' }, - { label: 'Mansard', value: 'mansard' }, - { label: 'Flat', value: 'flat' }, + { label: 'nodes.dormer.gable', value: 'gable' }, + { label: 'nodes.dormer.hip', value: 'hip' }, + { label: 'nodes.dormer.shed', value: 'shed' }, + { label: 'nodes.dormer.gambrel', value: 'gambrel' }, + { label: 'nodes.dormer.dutch', value: 'dutch' }, + { label: 'nodes.dormer.mansard', value: 'mansard' }, + { label: 'nodes.dormer.flat', value: 'flat' }, ] const SECTION_OPTIONS: Array<{ label: string; value: DormerSection }> = [ - { label: 'Dormer', value: 'dormer' }, - { label: 'Window', value: 'window' }, + { label: 'nodes.dormer.dormer', value: 'dormer' }, + { label: 'nodes.dormer.window', value: 'window' }, ] export default function DormerPanel() { const t = useTranslations() const [section, setSection] = useState('dormer') + + const resolvedSectionOptions = useMemo( + () => SECTION_OPTIONS.map((o) => ({ ...o, label: t(o.label) })), + [t], + ) + + const resolvedRoofTypeOptions = useMemo( + () => ROOF_TYPE_OPTIONS.map((o) => ({ ...o, label: t(o.label) })), + [t], + ) const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) @@ -178,7 +188,7 @@ export default function DormerPanel() {
- {SECTION_OPTIONS.map((option) => { + {resolvedSectionOptions.map((option) => { const isSelected = section === option.value return (
@@ -852,7 +852,7 @@ export default function ElevatorPanel() {
- +
{servedLevels.map((level) => { const isActive = activeLevelId === level.id @@ -877,16 +877,16 @@ export default function ElevatorPanel() { {level.name || `Level ${level.level}`} {isDisabled ? ( - Disabled + {t('nodes.elevator.disabledButton')} ) : isServiceOnly ? ( - Service + {t('nodes.elevator.serviceButton')} ) : ( stopOrder && ( - Stop {stopOrder} + {t('nodes.elevator.stopLabel', { n: stopOrder })} ) )} @@ -904,9 +904,9 @@ export default function ElevatorPanel() {
- + handleUpdate({ speed: value })} @@ -916,7 +916,7 @@ export default function ElevatorPanel() { value={node.speed} /> handleUpdate({ doorDurationMs: value })} @@ -925,7 +925,7 @@ export default function ElevatorPanel() { value={node.doorDurationMs} /> handleUpdate({ dwellMs: value })} diff --git a/packages/nodes/src/fence/inspector-editors.tsx b/packages/nodes/src/fence/inspector-editors.tsx index d8ef95938e..d0b7794365 100644 --- a/packages/nodes/src/fence/inspector-editors.tsx +++ b/packages/nodes/src/fence/inspector-editors.tsx @@ -7,7 +7,7 @@ import { getWallCurveLength, normalizeWallCurveOffset, } from '@pascal-app/core' -import { SliderControl } from '@pascal-app/editor' +import { SliderControl, useTranslations } from '@pascal-app/editor' /** * Custom inspector editors for fence fields that don't map to a single @@ -30,6 +30,7 @@ export function FenceLengthEditor({ node: FenceNode onUpdate: (patch: Partial) => void }) { + const t = useTranslations() const length = getWallCurveLength(node) const handleChange = (newLength: number) => { @@ -49,7 +50,7 @@ export function FenceLengthEditor({ return ( ) => void }) { + const t = useTranslations() const curveOffset = getClampedWallCurveOffset(node) const maxCurveOffset = getMaxWallCurveOffset(node) return ( onUpdate({ curveOffset: normalizeWallCurveOffset(node, value) })} diff --git a/packages/nodes/src/item/panel.tsx b/packages/nodes/src/item/panel.tsx index 3cd4e317aa..2fdb4d322f 100644 --- a/packages/nodes/src/item/panel.tsx +++ b/packages/nodes/src/item/panel.tsx @@ -10,6 +10,7 @@ import { SliderControl, triggerSFX, useEditor, + useTranslations, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Link, Link2Off, Move, Trash2 } from 'lucide-react' @@ -30,6 +31,7 @@ import { useCallback, useRef, useState } from 'react' * (see the wiki / plan recipe). */ export default function ItemPanel() { + const t = useTranslations() const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const deleteNode = useScene((s) => s.deleteNode) @@ -105,7 +107,7 @@ export default function ItemPanel() { title={node.name || node.asset.name} width={300} > - + @@ -156,7 +158,7 @@ export default function ItemPanel() { /> - + @@ -176,7 +178,7 @@ export default function ItemPanel() { />
{ triggerSFX('sfx:item-rotate') const currentDegrees = (node.rotation[1] * 180) / Math.PI @@ -185,7 +187,7 @@ export default function ItemPanel() { }} /> { triggerSFX('sfx:item-rotate') const currentDegrees = (node.rotation[1] * 180) / Math.PI @@ -196,10 +198,10 @@ export default function ItemPanel() {
- +
- Uniform Scale + {t('nodes.item.uniformScale')} + + + {t('editor.dismiss')} + +
-
- {hints.map((hint) => ( - +
+ {hints.map((hint, index) => ( + <> + {index > 0 &&
} + + ))}
- - - - - - {t('editor.dismiss')} - -
) diff --git a/packages/editor/src/components/ui/sidebar/panels/file-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/file-panel/index.tsx new file mode 100644 index 0000000000..25ee098770 --- /dev/null +++ b/packages/editor/src/components/ui/sidebar/panels/file-panel/index.tsx @@ -0,0 +1,39 @@ +'use client' + +import Link from 'next/link' +import { FolderOpen, Plus } from 'lucide-react' +import { messages, useLocale } from '../../../../../lib/i18n' +import { Button } from '../../../../../components/ui/primitives/button' + +export function FilePanel() { + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + + return ( +
+
+ + +
+ +
+ + +
+
+ ) +} diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 9546515108..c6b49d101a 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -144,6 +144,7 @@ export { SceneLoader } from './components/ui/scene-loader' export type { ExtraPanel } from './components/ui/sidebar/icon-rail' export { ItemsPanel } from './components/ui/sidebar/panels/items-panel' export type { FunctionTreeNode } from './components/ui/sidebar/panels/items-panel/function-tree-panel' +export { FilePanel } from './components/ui/sidebar/panels/file-panel' export { type ProjectVisibility, SettingsPanel, diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json index 74a87406fc..294eeb55fb 100644 --- a/packages/editor/src/lib/i18n/en.json +++ b/packages/editor/src/lib/i18n/en.json @@ -126,8 +126,10 @@ "editor.cameraPan": "Pan", "editor.cameraRotate": "Rotate", "editor.cameraZoom": "Zoom", + "editor.create": "Create", "editor.createNew": "Create new", "editor.grid": "Grid", + "editor.recent": "Recent", "editor.delete": "Delete", "editor.cancel": "Cancel", "editor.clearScale": "Clear Scale", @@ -843,6 +845,7 @@ "scenes.nodes": "{count} nodes", "scenes.sceneCount": "{count} scenes", "scenes.title": "Your scenes", + "scenes.noScenesSaved": "No scenes saved yet", "settings.adjustVolume": "Adjust volume levels and mute settings", "settings.audio": "Audio", "settings.audioSettings": "Audio Settings", @@ -920,6 +923,7 @@ "shortcuts.toggleSidebar": "Toggle sidebar", "shortcuts.undo": "Undo", "shortcuts.worksWhileInSelectMode": "Works while in Select mode.", + "sidebar.file": "File", "sidebar.items": "Items", "sidebar.scene": "Scene", "sidebar.site": "Site", diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json index be92fe9028..758421d646 100644 --- a/packages/editor/src/lib/i18n/zh.json +++ b/packages/editor/src/lib/i18n/zh.json @@ -126,8 +126,10 @@ "editor.cameraPan": "平移", "editor.cameraRotate": "旋转", "editor.cameraZoom": "缩放", + "editor.create": "创建", "editor.createNew": "创建新场景", "editor.grid": "网格", + "editor.recent": "最近", "editor.delete": "删除", "editor.cancel": "取消", "editor.clearScale": "清除比例", @@ -836,6 +838,7 @@ "scenes.nodes": "{count} 个节点", "scenes.sceneCount": "{count} 个场景", "scenes.title": "您的场景", + "scenes.noScenesSaved": "暂无保存的场景", "settings.adjustVolume": "调整音量级别和静音设置", "settings.audio": "音频", "settings.audioSettings": "音频设置", @@ -913,6 +916,7 @@ "shortcuts.toggleSidebar": "切换侧边栏", "shortcuts.undo": "撤销", "shortcuts.worksWhileInSelectMode": "在选择模式下有效。", + "sidebar.file": "文件", "sidebar.items": "物品", "sidebar.scene": "场景", "sidebar.site": "场地", From 251081f2e13f1108e7aaadd58eaa5ca984caf573 Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 19:40:55 +0800 Subject: [PATCH 005/106] =?UTF-8?q?=E5=9B=BD=E5=AE=B6=E5=8C=965?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/components/save-button.tsx | 46 +++++++++++++++---------- apps/editor/components/scene-loader.tsx | 35 ++++++++++++------- apps/editor/messages/en.json | 23 ++++++++++++- apps/editor/messages/zh.json | 23 ++++++++++++- 4 files changed, 94 insertions(+), 33 deletions(-) diff --git a/apps/editor/components/save-button.tsx b/apps/editor/components/save-button.tsx index d35deb159c..353412e15c 100644 --- a/apps/editor/components/save-button.tsx +++ b/apps/editor/components/save-button.tsx @@ -1,6 +1,7 @@ 'use client' import type { SceneGraph } from '@pascal-app/editor' +import { useLocale, messages } from '@pascal-app/editor' import { useRouter } from 'next/navigation' import { useCallback, useState } from 'react' @@ -16,11 +17,17 @@ interface SaveButtonProps { getGraph: () => SceneGraph | null } +const t = (key: string, locale: string) => { + const msgs = messages[locale] as Record + return msgs[key] || key +} + /** * Creates a new empty scene and navigates the user to it. */ -export function CreateSceneButton({ label = 'Create new scene' }: { label?: string } = {}) { +export function CreateSceneButton({ label }: { label?: string } = {}) { const router = useRouter() + const { locale } = useLocale() const [isCreating, setIsCreating] = useState(false) const [error, setError] = useState(null) @@ -31,20 +38,20 @@ export function CreateSceneButton({ label = 'Create new scene' }: { label?: stri const response = await fetch('/api/scenes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'Untitled scene', graph: EMPTY_GRAPH }), + body: JSON.stringify({ name: t('untitledScene', locale), graph: EMPTY_GRAPH }), }) if (!response.ok) { - setError(`Failed to create scene (${response.status})`) + setError(`${t('failedToCreateScene', locale)} (${response.status})`) return } const meta = (await response.json()) as { id: string } router.push(`/scene/${meta.id}`) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to create scene') + setError(err instanceof Error ? err.message : t('failedToCreateScene', locale)) } finally { setIsCreating(false) } - }, [router]) + }, [locale, router]) return (
@@ -55,7 +62,7 @@ export function CreateSceneButton({ label = 'Create new scene' }: { label?: stri onClick={handleCreate} type="button" > - {isCreating ? 'Creating…' : label} + {isCreating ? t('creating', locale) : label ?? t('createNewScene', locale)}
) @@ -68,13 +75,14 @@ export function CreateSceneButton({ label = 'Create new scene' }: { label?: stri */ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps) { const router = useRouter() + const { locale } = useLocale() const [isSaving, setIsSaving] = useState(false) const [status, setStatus] = useState(null) const handleSave = useCallback(async () => { const graph = getGraph() if (!graph) { - setStatus('No scene to save') + setStatus(t('noSceneToSave', locale)) return } setIsSaving(true) @@ -89,28 +97,28 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps body: JSON.stringify({ name, graph }), }) if (response.status === 409) { - setStatus('Conflict — reload to continue') + setStatus(t('conflictReload', locale)) return } if (!response.ok) { - setStatus(`Save failed (${response.status})`) + setStatus(`${t('saveFailed', locale)} (${response.status})`) return } - setStatus('Saved') + setStatus(t('saved', locale)) } catch (error) { - setStatus(error instanceof Error ? error.message : 'Save failed') + setStatus(error instanceof Error ? error.message : t('saveFailed', locale)) } finally { setIsSaving(false) } - }, [getGraph, name, sceneId, version]) + }, [getGraph, locale, name, sceneId, version]) const handleSaveAs = useCallback(async () => { const graph = getGraph() if (!graph) { - setStatus('No scene to save') + setStatus(t('noSceneToSave', locale)) return } - const newName = typeof window !== 'undefined' ? window.prompt('New scene name', name) : null + const newName = typeof window !== 'undefined' ? window.prompt(t('newSceneName', locale), name) : null if (!newName) return setIsSaving(true) setStatus(null) @@ -121,17 +129,17 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps body: JSON.stringify({ name: newName, graph }), }) if (!response.ok) { - setStatus(`Save-as failed (${response.status})`) + setStatus(`${t('saveAsFailed', locale)} (${response.status})`) return } const meta = (await response.json()) as { id: string } router.push(`/scene/${meta.id}`) } catch (error) { - setStatus(error instanceof Error ? error.message : 'Save-as failed') + setStatus(error instanceof Error ? error.message : t('saveAsFailed', locale)) } finally { setIsSaving(false) } - }, [getGraph, name, router]) + }, [getGraph, locale, name, router]) return (
@@ -141,7 +149,7 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps onClick={handleSave} type="button" > - {isSaving ? 'Saving…' : 'Save'} + {isSaving ? t('saving', locale) : t('save', locale)} {status && {status}}
diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 1d8dd224f1..ae9b1e84b6 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -6,6 +6,8 @@ import { applySceneGraphToEditor, Editor, + useLocale, + messages, type SceneGraph, type SidebarTab, } from '@pascal-app/editor' @@ -31,7 +33,7 @@ const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [ { id: 'site', label: 'Scene', - component: () => null, // Built-in SitePanel handles this + component: () => null, }, ] @@ -63,12 +65,21 @@ function sceneGraphSignature(graph: SceneGraphWithCollections): string { export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const router = useRouter() + const { locale } = useLocale() + const t = (key: string) => (messages[locale] as Record)[key] || key + + const sceneTabLabel = t('scene.scene') + const versionRef = useRef(meta.version) const lastRemoteGraphJsonRef = useRef(null) const suppressRemoteSaveUntilRef = useRef(0) const [conflict, setConflict] = useState(false) const [saveError, setSaveError] = useState(null) + const sidebarTabs: (SidebarTab & { component: React.ComponentType })[] = [ + { ...SIDEBAR_TABS[0], label: sceneTabLabel }, + ] + const handleLoad = useCallback(async () => initialScene, [initialScene]) const handleSave = useCallback( @@ -98,7 +109,7 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { } if (!response.ok) { - setSaveError(`Save failed (${response.status})`) + setSaveError(`${t('scene.saveFailed')} (${response.status})`) return } @@ -106,10 +117,10 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { versionRef.current = next.version setSaveError(null) } catch (error) { - setSaveError(error instanceof Error ? error.message : 'Save failed') + setSaveError(error instanceof Error ? error.message : t('scene.saveFailed')) } }, - [meta.id, meta.name], + [meta.id, meta.name, t], ) useEffect(() => { @@ -135,12 +146,12 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { source.addEventListener('error', () => { if (source.readyState === EventSource.CLOSED) { - setSaveError('Live scene connection closed') + setSaveError(t('scene.connectionClosed')) } }) return () => source.close() - }, [meta.id]) + }, [meta.id, t]) const handleThumb = useCallback( async (_blob: Blob) => { @@ -160,9 +171,9 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
{conflict && (
-

Another session saved first — refresh?

+

{t('scene.sessionConflict')}

- Your changes haven't been saved. Reload to pick up the latest version. + {t('scene.sessionConflictDesc')}

@@ -192,7 +203,7 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { className="pointer-events-auto rounded-md border border-border bg-background/90 px-3 py-1.5 font-medium text-xs shadow-sm backdrop-blur hover:bg-accent/40" href="/scenes" > - All scenes + {t('scene.allScenes')}
} viewerToolbarRight={} /> diff --git a/apps/editor/messages/en.json b/apps/editor/messages/en.json index 91d14f11b7..5e02cde502 100644 --- a/apps/editor/messages/en.json +++ b/apps/editor/messages/en.json @@ -35,7 +35,28 @@ "back": "Back", "next": "Next", "yes": "Yes", - "no": "No" + "no": "No", + "creating": "Creating…", + "saving": "Saving…", + "saved": "Saved", + "save": "Save", + "saveAs": "Save as…", + "noSceneToSave": "No scene to save", + "failedToCreateScene": "Failed to create scene", + "saveFailed": "Save failed", + "saveAsFailed": "Save-as failed", + "conflictReload": "Conflict — reload to continue", + "newSceneName": "New scene name", + "untitledScene": "Untitled scene", + "createNewScene": "Create new scene", + "scene.sessionConflict": "Another session saved first — refresh?", + "scene.sessionConflictDesc": "Your changes haven't been saved. Reload to pick up the latest version.", + "scene.reload": "Reload", + "scene.dismiss": "Dismiss", + "scene.allScenes": "All scenes", + "scene.scene": "Scene", + "scene.connectionClosed": "Live scene connection closed", + "scene.saveFailed": "Save failed" }, "nav": { "home": "Home", diff --git a/apps/editor/messages/zh.json b/apps/editor/messages/zh.json index 0e1fbd647d..67690d6a34 100644 --- a/apps/editor/messages/zh.json +++ b/apps/editor/messages/zh.json @@ -35,7 +35,28 @@ "back": "返回", "next": "下一步", "yes": "是", - "no": "否" + "no": "否", + "creating": "创建中…", + "saving": "保存中…", + "saved": "已保存", + "save": "保存", + "saveAs": "另存为…", + "noSceneToSave": "没有可保存的场景", + "failedToCreateScene": "创建场景失败", + "saveFailed": "保存失败", + "saveAsFailed": "另存失败", + "conflictReload": "冲突 — 请重新加载后继续", + "newSceneName": "新场景名称", + "untitledScene": "未命名场景", + "createNewScene": "创建新场景", + "scene.sessionConflict": "其他会话已先保存 — 是否刷新?", + "scene.sessionConflictDesc": "您的更改尚未保存。请重新加载以获取最新版本。", + "scene.reload": "重新加载", + "scene.dismiss": "关闭", + "scene.allScenes": "所有场景", + "scene.scene": "场景", + "scene.connectionClosed": "实时场景连接已关闭", + "scene.saveFailed": "保存失败" }, "nav": { "home": "首页", From ca129b5bd4dfda6dfcf2e080d187e5320ad6fc16 Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 19:41:08 +0800 Subject: [PATCH 006/106] =?UTF-8?q?=E5=B7=A6=E4=BE=A7icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/app/page.tsx | 17 ++++++++------- apps/editor/components/icons/file-icon.tsx | 17 +++++++++++++++ apps/editor/components/icons/scene-icon.tsx | 21 +++++++++++++++++++ .../editor/components/icons/settings-icon.tsx | 18 ++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 apps/editor/components/icons/file-icon.tsx create mode 100644 apps/editor/components/icons/scene-icon.tsx create mode 100644 apps/editor/components/icons/settings-icon.tsx diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index 36909927ee..a16da112df 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -1,7 +1,10 @@ 'use client' import { Editor, FilePanel, ItemsPanel, useLocale, messages } from '@pascal-app/editor' -import { Hexagon, Boxes, Cog, FileText } from 'lucide-react' +import { Boxes } from 'lucide-react' +import { SceneIcon } from '@/components/icons/scene-icon' +import { FileIcon } from '@/components/icons/file-icon' +import { SettingsIcon } from '@/components/icons/settings-icon' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight, @@ -13,16 +16,16 @@ const SIDEBAR_TABS = [ labelKey: 'sidebar.file', component: FilePanel, mobileDefaultSnap: 0.5, - mobileIcon: , - icon: , + mobileIcon: , + icon: , }, { id: 'site', labelKey: 'sidebar.scene', component: () => null, mobileDefaultSnap: 0.5, - mobileIcon: , - icon: , + mobileIcon: , + icon: , }, { id: 'items', @@ -37,8 +40,8 @@ const SIDEBAR_TABS = [ labelKey: 'common.settings', component: () => null, mobileDefaultSnap: 0.5, - mobileIcon: , - icon: , + mobileIcon: , + icon: , }, ] diff --git a/apps/editor/components/icons/file-icon.tsx b/apps/editor/components/icons/file-icon.tsx new file mode 100644 index 0000000000..cbd89e3eb5 --- /dev/null +++ b/apps/editor/components/icons/file-icon.tsx @@ -0,0 +1,17 @@ +'use client' + +/** File icon — folder with document, supports currentColor */ +export function FileIcon({ className }: { className?: string }) { + return ( + + + + ) +} diff --git a/apps/editor/components/icons/scene-icon.tsx b/apps/editor/components/icons/scene-icon.tsx new file mode 100644 index 0000000000..6b812f8910 --- /dev/null +++ b/apps/editor/components/icons/scene-icon.tsx @@ -0,0 +1,21 @@ +'use client' + +/** Scene icon — layered house SVG, supports currentColor */ +export function SceneIcon({ className }: { className?: string }) { + return ( + + + + + ) +} diff --git a/apps/editor/components/icons/settings-icon.tsx b/apps/editor/components/icons/settings-icon.tsx new file mode 100644 index 0000000000..c2cd4a7e9d --- /dev/null +++ b/apps/editor/components/icons/settings-icon.tsx @@ -0,0 +1,18 @@ +'use client' + +/** Settings icon — gear/cog, supports currentColor */ +export function SettingsIcon({ className }: { className?: string }) { + return ( + + + + ) +} From 11a3bce920eb09a4276095dcbfcc7e08f6712dd3 Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 19:41:15 +0800 Subject: [PATCH 007/106] =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E6=9D=A1=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/editor/app/globals.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/editor/app/globals.css b/apps/editor/app/globals.css index 390aa6e4a9..77b5b56b6f 100644 --- a/apps/editor/app/globals.css +++ b/apps/editor/app/globals.css @@ -166,6 +166,25 @@ scrollbar-width: none; /* Firefox */ } +/* Custom scrollbar — thin, dark-themed */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-track { + background: #2c2c2e; +} + +::-webkit-scrollbar-thumb { + background: oklch(1 0 0 / 30%); + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background: oklch(1 0 0 / 50%); +} + @media (prefers-reduced-motion: reduce) { *, *::before, From 55fc7fd32b5dc53c9d8c6979782ce3d9c92a036c Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 19:41:28 +0800 Subject: [PATCH 008/106] dialog style --- packages/editor/src/components/ui/primitives/dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/ui/primitives/dialog.tsx b/packages/editor/src/components/ui/primitives/dialog.tsx index e88503f68e..3bd0a0ce85 100644 --- a/packages/editor/src/components/ui/primitives/dialog.tsx +++ b/packages/editor/src/components/ui/primitives/dialog.tsx @@ -60,7 +60,7 @@ function DialogContent({ {children} {showCloseButton && ( From 3dc03813298516ae9f454678e211313e8b5f4e19 Mon Sep 17 00:00:00 2001 From: leapar Date: Sun, 31 May 2026 19:41:39 +0800 Subject: [PATCH 009/106] settings-panel style --- .../ui/sidebar/panels/settings-panel/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx index ebf6bd23ee..e98bcd0399 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx @@ -360,7 +360,7 @@ export function SettingsPanel({ )} {/* Export Section */} -
+
{/* Audio Section */} -
+
{/* Keyboard Section */} -
+
{/* Scene Graph */} -
+
@@ -458,7 +458,7 @@ export function SettingsPanel({ {t('settings.exploreSceneGraph')} - + {t('settings.sceneGraph')}
{/* Danger Zone */} -
+
- {panel.label} + {label} ) })} @@ -127,6 +132,7 @@ export function IconRail({ {/* Settings panel */} {[settingsPanel].map((panel) => { const isActive = activePanel === panel.id + const label = labelOf(panel) return ( @@ -139,7 +145,7 @@ export function IconRail({ type="button" > {panel.label} - {panel.label} + {label} ) })} @@ -163,4 +169,4 @@ export function IconRail({ ) } -export { panels } +export { panels } \ No newline at end of file From d7a991b4f450fa7f7014e92f18c2cf5b889567a0 Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 21:16:08 +0800 Subject: [PATCH 012/106] Restore i18n on command palette, settings panel, level selector, view toggles, viewer toolbar Continues the i18n restore from b8c87042 and af9d9eab. 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). --- apps/editor/components/viewer-toolbar.tsx | 202 ++++++++++-------- .../ui/action-menu/view-toggles.tsx | 97 +++++---- .../ui/command-palette/editor-commands.tsx | 140 ++++++------ .../components/ui/command-palette/index.tsx | 59 ++--- .../components/ui/floating-level-selector.tsx | 37 ++-- .../sidebar/panels/settings-panel/index.tsx | 87 ++++---- .../keyboard-shortcuts-dialog.tsx | 166 +++++++------- .../settings-panel/load-build-dialog.tsx | 56 ++--- packages/editor/src/lib/i18n/en.json | 109 +++++++++- packages/editor/src/lib/i18n/zh.json | 109 +++++++++- 10 files changed, 669 insertions(+), 393 deletions(-) diff --git a/apps/editor/components/viewer-toolbar.tsx b/apps/editor/components/viewer-toolbar.tsx index a707288ecf..71f410a7f5 100644 --- a/apps/editor/components/viewer-toolbar.tsx +++ b/apps/editor/components/viewer-toolbar.tsx @@ -14,6 +14,7 @@ import { useFloorplanAnnotationVisibility, useFloorplanMode, useSidebarStore, + useTranslations, type ViewMode, } from '@pascal-app/editor' import { @@ -84,10 +85,10 @@ function ToolbarTooltip({ children, label }: { children: ReactNode; label: strin ) } -const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [ +const VIEW_MODES: { id: ViewMode; labelKey: string; icon: React.ReactNode }[] = [ { id: '3d', - label: '3D', + labelKey: 'viewer.viewMode3D', icon: ( , }, ] const levelModeOrder = ['stacked', 'exploded', 'solo'] as const -const levelModeLabels: Record = { - manual: 'Stack', - stacked: 'Stack', - exploded: 'Exploded', - solo: 'Solo', -} - -const wallModeOrder = ['cutaway', 'up', 'down', 'translucent'] as const -const wallModeConfig: Record = { - up: { icon: '/icons/room.webp', label: 'Full height' }, - cutaway: { icon: '/icons/wallcut.webp', label: 'Cutaway' }, - down: { icon: '/icons/walllow.webp', label: 'Low' }, - translucent: { icon: '/icons/wall.webp', label: 'Translucent' }, +const levelModeLabels = { + manual: 'viewer.stack', + stacked: 'viewer.stack', + exploded: 'viewer.exploded', + solo: 'viewer.solo', +} as const satisfies Record + +const wallModeOrder = ['cutaway', 'up', 'down'] as const +const wallModeConfig: Record = { + up: { icon: '/icons/room.webp', labelKey: 'viewer.fullHeight' }, + cutaway: { icon: '/icons/wallcut.webp', labelKey: 'viewer.cutaway' }, + down: { icon: '/icons/walllow.webp', labelKey: 'viewer.low' }, + translucent: { icon: '/icons/wall.webp', labelKey: 'viewer.translucent' }, } const SHADING_OPTIONS = [ - { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box }, - { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles }, + { id: 'solid', nameKey: 'viewer.solid', detailKey: 'viewer.flatAndFast', icon: Box }, + { id: 'rendered', nameKey: 'viewer.rendered', detailKey: 'viewer.fullAO', icon: Sparkles }, ] as const const FLOORPLAN_ANNOTATION_OPTIONS = [ - { id: 'automaticDimensions', name: 'Automatic dimensions', icon: Ruler }, - { id: 'manualDimensions', name: 'Manual dimensions', icon: Ruler }, - { id: 'measurements', name: 'Measurements', icon: ScanLine }, - { id: 'openingMarks', name: 'Door/window marks', icon: Tag }, - { id: 'structuralGrids', name: 'Structural grids & column centers', icon: Grid2X2 }, - { id: 'roomLabels', name: 'Room labels', icon: SquareUserRound }, - { id: 'stairAnnotations', name: 'Stair annotations', icon: Footprints }, + { id: 'automaticDimensions', nameKey: 'viewer.automaticDimensions', icon: Ruler }, + { id: 'manualDimensions', nameKey: 'viewer.manualDimensions', icon: Ruler }, + { id: 'measurements', nameKey: 'viewer.measurements', icon: ScanLine }, + { id: 'openingMarks', nameKey: 'viewer.openingMarks', icon: Tag }, + { id: 'structuralGrids', nameKey: 'viewer.structuralGrids', icon: Grid2X2 }, + { id: 'roomLabels', nameKey: 'viewer.roomLabels', icon: SquareUserRound }, + { id: 'stairAnnotations', nameKey: 'viewer.stairAnnotations', icon: Footprints }, ] as const const FLOORPLAN_MODE_OPTIONS = [ { id: 'default', - name: 'Default', - detail: 'Clean plan; dimensions appear with selection', + nameKey: 'viewer.floorplanDefault', + detailKey: 'viewer.floorplanDefaultDetail', }, { id: 'expert', - name: 'Expert', - detail: 'Full documentation and annotation controls', + nameKey: 'viewer.floorplanExpert', + detailKey: 'viewer.floorplanExpertDetail', }, ] as const const FLOORPLAN_WALL_DIMENSION_REFERENCE_OPTIONS = [ - { id: 'finished-faces', name: 'Finished faces', detail: 'Full wall thickness' }, - { id: 'centerline', name: 'Wall centerline', detail: 'Single wall axis' }, - { id: 'stud-faces', name: 'Face of stud', detail: 'Structural core face' }, + { id: 'finished-faces', nameKey: 'viewer.finishedFaces', detailKey: 'viewer.finishedFacesDetail' }, + { id: 'centerline', nameKey: 'viewer.wallCenterline', detailKey: 'viewer.wallCenterlineDetail' }, + { id: 'stud-faces', nameKey: 'viewer.faceOfStud', detailKey: 'viewer.faceOfStudDetail' }, ] as const function ViewModeControl() { + const t = useTranslations() const viewMode = useEditor((state) => state.viewMode) const setViewMode = useEditor((state) => state.setViewMode) @@ -176,10 +178,11 @@ function ViewModeControl() {
{VIEW_MODES.map((mode) => { const isActive = viewMode === mode.id + const label = t(mode.labelKey) return ( - + ) @@ -201,6 +204,7 @@ function ViewModeControl() { } function CollapseSidebarButton() { + const t = useTranslations() const isCollapsed = useSidebarStore((state) => state.isCollapsed) const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed) @@ -210,9 +214,11 @@ function CollapseSidebarButton() { return (
- + ) } function WallModeToggle() { + const t = useTranslations() const wallMode = useViewer((state) => state.wallMode) const setWallMode = useViewer((state) => state.setWallMode) const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway! @@ -281,8 +293,11 @@ function WallModeToggle() { if (next) setWallMode(next) } + const labelText = t(config.labelKey) + const tooltipLabel = t('viewer.wallsWithMode', { mode: labelText }) + return ( - + ) @@ -305,14 +320,15 @@ function WallModeToggle() { // camera projection, units, render mode, edges and scene theme. const EDGE_OPTIONS = [ - { id: 'off', name: 'Off', detail: 'No edge lines' }, - { id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' }, - { id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' }, -] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[] + { id: 'off', nameKey: 'viewer.off_edge', detailKey: 'viewer.noEdgeLines' }, + { id: 'soft', nameKey: 'viewer.soft', detailKey: 'viewer.faintOutline' }, + { id: 'strong', nameKey: 'viewer.strong', detailKey: 'viewer.crispEdges' }, +] as const satisfies readonly { id: EdgeMode; nameKey: string; detailKey: string }[] const SUBMENU_CONTENT_CLASS = 'min-w-56 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl' function DisplayMenu() { + const t = useTranslations() const viewMode = useEditor((state) => state.viewMode) const showGrid = useViewer((state) => state.showGrid) const setShowGrid = useViewer((state) => state.setShowGrid) @@ -358,15 +374,15 @@ function DisplayMenu() { return ( - + @@ -378,7 +394,7 @@ function DisplayMenu() { > keepOpen(e, () => setShowGrid(!showGrid))}> - Grid + {t('viewer.grid')} {showGrid ? ( ) : ( @@ -390,7 +406,9 @@ function DisplayMenu() { onSelect={(e) => keepOpen(e, () => setShowMeasurements(!showMeasurements))} > - {viewMode === 'split' ? '3D measurements' : 'Measurements'} + + {viewMode === 'split' ? t('viewer.measurements3d') : t('viewer.measurements')} + {showMeasurements ? ( ) : ( @@ -403,17 +421,17 @@ function DisplayMenu() { - Floor plan mode + {t('viewer.floorplanMode')} - {floorplanMode === 'default' ? 'Default' : 'Expert'} + {floorplanMode === 'default' ? t('viewer.floorplanDefault') : t('viewer.floorplanExpert')} {FLOORPLAN_MODE_OPTIONS.map((option) => ( setFloorplanMode(option.id)}>
- {option.name} - {option.detail} + {t(option.nameKey)} + {t(option.detailKey)}
{floorplanMode === option.id ? ( @@ -427,7 +445,7 @@ function DisplayMenu() { - Floor plan annotations + {t('viewer.floorplanAnnotations')} {FLOORPLAN_ANNOTATION_OPTIONS.map((option) => { @@ -441,7 +459,7 @@ function DisplayMenu() { } > - {option.name} + {t(option.nameKey)} {visible ? ( ) : ( @@ -455,13 +473,13 @@ function DisplayMenu() { - Wall dimensions + {t('viewer.wallDimensions')} - { + {t( FLOORPLAN_WALL_DIMENSION_REFERENCE_OPTIONS.find( (option) => option.id === wallDimensionReference, - )?.name - } + )?.nameKey ?? 'viewer.finishedFaces', + )} @@ -473,8 +491,8 @@ function DisplayMenu() { } >
- {option.name} - {option.detail} + {t(option.nameKey)} + {t(option.detailKey)}
{wallDimensionReference === option.id ? ( @@ -489,15 +507,17 @@ function DisplayMenu() { ) : null} keepOpen(e, () => setMagneticSnap(!magneticSnap))}> - Magnetic snap + {t('viewer.magneticSnap')} - {magneticSnap ? 'On' : 'Off'} + {magneticSnap ? t('viewer.on') : t('viewer.off')} keepOpen(e, () => setShadows(!shadows))}> - Shadows - {shadows ? 'On' : 'Off'} + {t('viewer.shadows')} + + {shadows ? t('viewer.on') : t('viewer.off')} + @@ -511,9 +531,9 @@ function DisplayMenu() { icon={cameraMode === 'perspective' ? 'icon-park-outline:perspective' : 'vaadin:grid'} width={16} /> - Camera + {t('viewer.camera')} - {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'} + {cameraMode === 'perspective' ? t('viewer.perspective') : t('viewer.orthographic')} @@ -521,13 +541,13 @@ function DisplayMenu() { {unit === 'imperial' ? 'ft' : metricNotation === 'millimeters' ? 'mm' : 'm'} - Units + {t('viewer.units')} {unit === 'imperial' - ? 'Feet & inches' + ? t('viewer.imperial') : metricNotation === 'millimeters' - ? 'Millimeters' - : 'Meters'} + ? t('viewer.millimeters') + : t('viewer.meters')} @@ -535,7 +555,7 @@ function DisplayMenu() { m - Meters + {t('viewer.meters')} {unit === 'metric' && metricNotation === 'meters' ? ( ) : null} @@ -544,7 +564,7 @@ function DisplayMenu() { mm - Millimeters + {t('viewer.millimeters')} {unit === 'metric' && metricNotation === 'millimeters' ? ( ) : null} @@ -553,7 +573,7 @@ function DisplayMenu() { ft - Feet & inches + {t('viewer.imperial')} {unit === 'imperial' ? : null}
@@ -564,8 +584,10 @@ function DisplayMenu() { - Render - {activeShading.name} + {t('viewer.render')} + + {t(activeShading.nameKey)} + {SHADING_OPTIONS.map((option) => { @@ -574,8 +596,8 @@ function DisplayMenu() { setShading(option.id)}>
- {option.name} - {option.detail} + {t(option.nameKey)} + {t(option.detailKey)}
{shading === option.id ? ( @@ -589,15 +611,17 @@ function DisplayMenu() { - Edges - {activeEdges.name} + {t('viewer.edges')} + + {t(activeEdges.nameKey)} + {EDGE_OPTIONS.map((option) => ( setEdges(option.id)}>
- {option.name} - {option.detail} + {t(option.nameKey)} + {t(option.detailKey)}
{edges === option.id ? : null}
@@ -608,7 +632,7 @@ function DisplayMenu() { - Theme + {t('viewer.sceneTheme')} {activeTheme.name} @@ -643,6 +667,7 @@ function DisplayMenu() { } function WalkthroughButton() { + const t = useTranslations() const isFirstPersonMode = useEditor((state) => state.isFirstPersonMode) const setFirstPersonMode = useEditor((state) => state.setFirstPersonMode) const handleClick = useCallback(() => { @@ -656,7 +681,7 @@ function WalkthroughButton() { }, [isFirstPersonMode, setFirstPersonMode]) return ( - + ) diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index e3023323cf..0a60854fc5 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -14,6 +14,7 @@ import { useCallback, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import { getLevelDisplayName } from '@pascal-app/core' import { createLocalGuideImage } from '../../../lib/local-guide-image' +import { useTranslations } from '../../../lib/i18n' import { cn } from '../../../lib/utils' import useEditor from '../../../store/use-editor' import { useUploadStore } from '../../../store/use-upload' @@ -23,8 +24,6 @@ import { ActionButton } from './action-button' const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif' -const REFERENCES_EMPTY_TEXT = - 'Upload GLB meshes as scan references or blueprint images as guide references.' // ── Helper: get guide images for the current level ────────────────────────── @@ -83,6 +82,7 @@ function useLowerReferenceLevels(): LevelNode[] { // ── Shared upload button for dropdowns ────────────────────────────────────── function UploadButton({ onError }: { onError: (message: string | null) => void }) { + const t = useTranslations() const fileInputRef = useRef(null) const levelId = useViewer((s) => s.selection.levelId) const setSelection = useViewer((s) => s.setSelection) @@ -100,7 +100,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } onError(null) if (file.size > MAX_FILE_SIZE) { - onError('File is too large. Maximum size is 200 MB.') + onError(t('viewer.fileTooLarge')) return } @@ -108,7 +108,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } file.name.toLowerCase().endsWith('.glb') || file.name.toLowerCase().endsWith('.gltf') const isImage = file.type.startsWith('image/') if (!(isScan || isImage)) { - onError('Upload a .glb/.gltf scan or an image.') + onError(t('viewer.uploadGlbOrImage')) return } @@ -120,7 +120,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } setSelectedReferenceId(guide.id) setSelection({ selectedIds: [], zoneId: null }) } catch { - onError('Could not add that guide image.') + onError(t('viewer.couldNotAddGuideImage')) } finally { setIsAddingGuide(false) } @@ -129,26 +129,26 @@ function UploadButton({ onError }: { onError: (message: string | null) => void } const { uploadHandler } = useUploadStore.getState() if (!uploadHandler) { - onError('Scan upload is unavailable.') + onError(t('viewer.scanUploadUnavailable')) return } const projectId = window.location.pathname.split('/editor/')[1]?.split('/')[0] if (!projectId) { - onError('Open a project before uploading a scan.') + onError(t('viewer.openProjectBeforeUpload')) return } useUploadStore.getState().clearUpload(levelId) uploadHandler(projectId, levelId, file, 'scan') }, - [createNode, levelId, onError, setSelectedReferenceId, setSelection, setShowGuides], + [createNode, levelId, onError, setSelectedReferenceId, setSelection, setShowGuides, t], ) return ( <>
handleOpacityChange(guide.id, value)} @@ -337,7 +338,7 @@ function GuidesControl() {
) : (
- {REFERENCES_EMPTY_TEXT} + {t('viewer.referencesEmptyText')}
)}
@@ -349,6 +350,7 @@ function GuidesControl() { // ── Scans toggle + dropdown ───────────────────────────────────────────────── function ScansControl() { + const t = useTranslations() const showScans = useViewer((state) => state.showScans) const setShowScans = useViewer((state) => state.setShowScans) const setSelection = useViewer((state) => state.setSelection) @@ -389,7 +391,7 @@ function ScansControl() { ? 'bg-white/15' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0', )} - label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`} + label={t('viewer.scansState', { state: showScans ? t('viewer.visible') : t('viewer.hidden') })} onClick={() => setShowScans(!showScans)} size="icon" variant="ghost" @@ -406,7 +408,7 @@ function ScansControl() {
handleOpacityChange(scan.id, value)} @@ -512,7 +514,7 @@ function ScansControl() {
) : (
- {REFERENCES_EMPTY_TEXT} + {t('viewer.referencesEmptyText')}
)}
@@ -545,6 +547,7 @@ function ReferenceListSection({ setShow: (show: boolean) => void onError: (message: string | null) => void }) { + const t = useTranslations() const setSelection = useViewer((state) => state.setSelection) const updateNode = useScene((state) => state.updateNode) const deleteNode = useScene((state) => state.deleteNode) @@ -572,12 +575,12 @@ function ReferenceListSection({ {hasItems && (

{nodes.length} {noun} - {nodes.length !== 1 ? 's' : ''} on this level + {nodes.length !== 1 ? 's' : ''} {t('viewer.onThisLevel')}

)}
@@ -657,6 +661,7 @@ function ReferenceListSection({ } function ReferencesControl() { + const t = useTranslations() const showScans = useViewer((state) => state.showScans) const setShowScans = useViewer((state) => state.setShowScans) const showGuides = useViewer((state) => state.showGuides) @@ -685,7 +690,7 @@ function ReferencesControl() { ? 'bg-white/15' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0', )} - label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`} + label={t('viewer.referencesState', { state: anyVisible ? t('viewer.visible') : t('viewer.hidden') })} onClick={toggleAll} size="icon" variant="ghost" @@ -705,7 +710,7 @@ function ReferencesControl() { ) })}
) : (
- No lower floor available. + {t('viewer.noLowerFloor')}
)}
@@ -921,6 +927,7 @@ function ReferenceFloorControl() { // ── Riser diagram control ──────────────────────────────────────────────────── function RiserControl() { + const t = useTranslations() const isRiserOpen = useEditor((state) => state.isRiserOpen) const toggleRiserOpen = useEditor((state) => state.toggleRiserOpen) @@ -931,7 +938,7 @@ function RiserControl() { ? 'bg-white/15' : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0', )} - label="Riser diagram" + label={t('viewer.riserDiagram')} onClick={toggleRiserOpen} size="icon" variant="ghost" diff --git a/packages/editor/src/components/ui/command-palette/editor-commands.tsx b/packages/editor/src/components/ui/command-palette/editor-commands.tsx index 0fdd85d978..4d98109d3a 100644 --- a/packages/editor/src/components/ui/command-palette/editor-commands.tsx +++ b/packages/editor/src/components/ui/command-palette/editor-commands.tsx @@ -39,12 +39,14 @@ import { import { useEffect } from 'react' import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history' import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection' +import { useTranslations } from '../../../lib/i18n' import { useCommandRegistry } from '../../../store/use-command-registry' import type { StructureTool } from '../../../store/use-editor' import useEditor from '../../../store/use-editor' import { useCommandPalette } from './index' export function EditorCommands() { + const t = useTranslations() const register = useCommandRegistry((s) => s.register) const { navigateTo, setInputValue, setOpen } = useCommandPalette() @@ -78,72 +80,72 @@ export function EditorCommands() { // ── Scene ──────────────────────────────────────────────────────────── { id: 'editor.tool.wall', - label: 'Wall Tool', - group: 'Scene', + label: t('commands.wallTool'), + group: t('commands.groupScene'), icon: , keywords: ['draw', 'build', 'structure'], execute: () => activateTool('wall'), }, { id: 'editor.tool.slab', - label: 'Slab Tool', - group: 'Scene', + label: t('commands.slabTool'), + group: t('commands.groupScene'), icon: , keywords: ['floor', 'build'], execute: () => activateTool('slab'), }, { id: 'editor.tool.ceiling', - label: 'Ceiling Tool', - group: 'Scene', + label: t('commands.ceilingTool'), + group: t('commands.groupScene'), icon: , keywords: ['top', 'build'], execute: () => activateTool('ceiling'), }, { id: 'editor.tool.door', - label: 'Door Tool', - group: 'Scene', + label: t('commands.doorTool'), + group: t('commands.groupScene'), icon: , keywords: ['opening', 'entrance'], execute: () => activateTool('door'), }, { id: 'editor.tool.window', - label: 'Window Tool', - group: 'Scene', + label: t('commands.windowTool'), + group: t('commands.groupScene'), icon: , keywords: ['opening', 'glass'], execute: () => activateTool('window'), }, { id: 'editor.tool.item', - label: 'Item Tool', - group: 'Scene', + label: t('commands.itemTool'), + group: t('commands.groupScene'), icon: , keywords: ['furniture', 'object', 'asset', 'furnish'], execute: () => activateTool('item'), }, { id: 'editor.tool.stair', - label: 'Stair Tool', - group: 'Scene', + label: t('commands.stairTool'), + group: t('commands.groupScene'), icon: , keywords: ['stairs', 'staircase', 'flight', 'landing', 'steps'], execute: () => activateTool('stair'), }, { id: 'editor.tool.zone', - label: 'Zone Tool', - group: 'Scene', + label: t('commands.zoneTool'), + group: t('commands.groupScene'), icon: , keywords: ['area', 'room', 'space'], execute: () => activateTool('zone'), }, { id: 'editor.delete-selection', - label: 'Delete Selection', - group: 'Scene', + label: t('commands.deleteSelection'), + group: t('commands.groupScene'), icon: , keywords: ['remove', 'erase'], shortcut: ['⌫'], @@ -156,8 +158,8 @@ export function EditorCommands() { }, { id: 'editor.mode.material-paint', - label: 'Material Paint', - group: 'Scene', + label: t('commands.materialPaint'), + group: t('commands.groupScene'), icon: , keywords: ['paint', 'material', 'texture', 'bucket', 'surface'], shortcut: ['P'], @@ -171,8 +173,8 @@ export function EditorCommands() { }, { id: 'editor.mode.terrain-sculpt', - label: 'Sculpt Terrain', - group: 'Scene', + label: t('commands.sculptTerrain'), + group: t('commands.groupScene'), icon: , keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'], shortcut: ['G'], @@ -184,8 +186,8 @@ export function EditorCommands() { // ── Levels ─────────────────────────────────────────────────────────── { id: 'editor.level.goto', - label: 'Go to Level', - group: 'Levels', + label: t('commands.gotoLevel'), + group: t('commands.groupLevels'), icon: , keywords: ['level', 'floor', 'go', 'navigate', 'switch', 'select'], navigate: true, @@ -194,8 +196,8 @@ export function EditorCommands() { }, { id: 'editor.level.add', - label: 'Add Level', - group: 'Levels', + label: t('commands.addLevel'), + group: t('commands.groupLevels'), icon: , keywords: ['level', 'floor', 'add', 'create', 'new'], execute: () => @@ -218,8 +220,8 @@ export function EditorCommands() { }, { id: 'editor.level.rename', - label: 'Rename Level', - group: 'Levels', + label: t('commands.renameLevel'), + group: t('commands.groupLevels'), icon: , keywords: ['level', 'floor', 'rename', 'name'], navigate: true, @@ -234,8 +236,8 @@ export function EditorCommands() { }, { id: 'editor.level.delete', - label: 'Delete Level', - group: 'Levels', + label: t('commands.deleteLevel'), + group: t('commands.groupLevels'), icon: , keywords: ['level', 'floor', 'delete', 'remove'], when: () => { @@ -255,26 +257,42 @@ export function EditorCommands() { // ── Viewer Controls ────────────────────────────────────────────────── { id: 'editor.viewer.wall-mode', - label: 'Wall Mode', - group: 'Viewer Controls', + label: t('commands.wallMode'), + group: t('commands.groupViewerControls'), icon: , keywords: ['wall', 'cutaway', 'up', 'down', 'translucent', 'view'], badge: () => { const mode = useViewer.getState().wallMode - return { cutaway: 'Cutaway', up: 'Up', down: 'Down', translucent: 'Translucent' }[mode] + const labelKey = + mode === 'cutaway' + ? 'commands.cutaway' + : mode === 'up' + ? 'commands.up' + : mode === 'down' + ? 'commands.down' + : 'commands.translucent' + return t(labelKey) }, navigate: true, execute: () => navigateTo('wall-mode'), }, { id: 'editor.viewer.level-mode', - label: 'Level Mode', - group: 'Viewer Controls', + label: t('commands.levelMode'), + group: t('commands.groupViewerControls'), icon: , keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'], badge: () => { const mode = useViewer.getState().levelMode - return { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo' }[mode] + const labelKey = + mode === 'manual' + ? 'commands.manual' + : mode === 'stacked' + ? 'commands.stacked' + : mode === 'exploded' + ? 'commands.exploded' + : 'commands.solo' + return t(labelKey) }, navigate: true, execute: () => navigateTo('level-mode'), @@ -283,9 +301,10 @@ export function EditorCommands() { id: 'editor.viewer.camera-mode', label: () => { const mode = useViewer.getState().cameraMode - return `Camera: Switch to ${mode === 'perspective' ? 'Orthographic' : 'Perspective'}` + const next = mode === 'perspective' ? t('commands.orthographic') : t('commands.perspective') + return t('commands.cameraSwitchTo', { mode: next }) }, - group: 'Viewer Controls', + group: t('commands.groupViewerControls'), icon:
{projectId && (
- -
Project ID
+ +
{t('settings.projectId')}
@@ -397,12 +403,15 @@ export function SettingsPanel({ {/* Visibility Section (only for cloud projects) */} {projectId && !isLocalProject && (
- +
-
Public
+
{t('settings.visibilityPublic')}
- {projectVisibility?.isPrivate ? 'Only you' : 'Anyone'} can view + {projectVisibility?.isPrivate + ? t('settings.visibilityOnlyYou') + : t('settings.visibilityAnyone')}{' '} + {t('settings.visibilityCanView')}
-
Show 3D Scans
-
Visible to public viewers
+
{t('settings.visibilityShow3DScans')}
+
{t('settings.visibilityVisiblePublic')}
-
Show Floorplans
-
Visible to public viewers
+
{t('settings.visibilityShowFloorplans')}
+
{t('settings.visibilityVisiblePublic')}
-
Shadows
-
Cast shadows from lights
+
{t('settings.shadows')}
+
{t('settings.visibilityCastShadows')}
- +
-
3D model
+
{t('settings.export3DModel')}
-
Visible nodes only
+
{t('settings.visibleNodesOnly')}
- Exclude hidden furniture and other hidden scene nodes + {t('settings.visibleNodesOnlyDesc')}
@@ -464,7 +473,7 @@ export function SettingsPanel({ variant="outline" > - Export GLB + {t('settings.exportedGLB')} @@ -488,8 +497,8 @@ export function SettingsPanel({
- Floor plan - {floorplanMode === 'default' ? 'Default mode' : 'Expert mode'} + {t('settings.floorPlan')} + {floorplanMode === 'default' ? t('settings.floorplanDefault') : t('settings.floorplanExpert')}
@@ -513,7 +522,7 @@ export function SettingsPanel({ {/* Thumbnail Section (only for cloud projects) */} {projectId && !isLocalProject && (
- +
)} {/* Save/Load Section */}
- + - +
{/* Keyboard Section */}
- +
{/* Scene Graph */}
- + - Scene Graph + {t('settings.sceneGraph')}
- +
diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx index b81d279ff7..9777cde76c 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx @@ -1,4 +1,5 @@ import { Keyboard } from 'lucide-react' +import { useTranslations } from '../../../../../lib/i18n' import { Button } from './../../../../../components/ui/primitives/button' import { Dialog, @@ -15,193 +16,190 @@ import { type Shortcut = { keys: string[] - action: string - note?: string + actionKey: string + noteKey?: string } type ShortcutCategory = { - title: string + titleKey: string shortcuts: Shortcut[] } const SHORTCUT_CATEGORIES: ShortcutCategory[] = [ { - title: 'Editor Navigation', + titleKey: 'shortcuts.editorNavigation', shortcuts: [ - { keys: ['1'], action: 'Switch to Site phase' }, - { keys: ['2'], action: 'Switch to Structure phase' }, - { keys: ['3'], action: 'Switch to Furnish phase' }, - { keys: ['F'], action: 'Switch to Furnish layer' }, - { keys: ['Z'], action: 'Switch to Zones layer' }, + { keys: ['1'], actionKey: 'shortcuts.switchToSitePhase' }, + { keys: ['2'], actionKey: 'shortcuts.switchToStructurePhase' }, + { keys: ['3'], actionKey: 'shortcuts.switchToFurnishPhase' }, + { keys: ['F'], actionKey: 'shortcuts.switchToFurnishLayer' }, + { keys: ['Z'], actionKey: 'shortcuts.switchToZonesLayer' }, { keys: ['Cmd/Ctrl', 'Arrow Up'], - action: 'Select next level in the active building', + actionKey: 'shortcuts.selectNextLevel', }, { keys: ['Cmd/Ctrl', 'Arrow Down'], - action: 'Select previous level in the active building', + actionKey: 'shortcuts.selectPreviousLevel', }, - { keys: ['Cmd/Ctrl', 'B'], action: 'Toggle sidebar' }, + { keys: ['Cmd/Ctrl', 'B'], actionKey: 'shortcuts.toggleSidebar' }, ], }, { - title: 'Modes & History', + titleKey: 'shortcuts.modesAndHistory', shortcuts: [ - { keys: ['V'], action: 'Switch to Select mode' }, - { keys: ['B'], action: 'Switch to Build mode' }, - { keys: ['M'], action: 'Activate the last measurement tool' }, - { keys: ['X'], action: 'Switch to Delete mode' }, + { keys: ['V'], actionKey: 'shortcuts.switchToSelectMode' }, + { keys: ['B'], actionKey: 'shortcuts.switchToBuildMode' }, + { keys: ['M'], actionKey: 'shortcuts.activateMeasurementTool' }, + { keys: ['X'], actionKey: 'shortcuts.switchToDeleteMode' }, { keys: ['Esc'], - action: 'Cancel the active tool and return to Select mode', - note: 'Mid-draw it cancels only the chain in progress and keeps the tool armed; press it again to leave the tool.', + actionKey: 'shortcuts.cancelTool', + noteKey: 'shortcuts.cancelToolNote', }, - { keys: ['Delete / Backspace'], action: 'Delete selected objects' }, - { keys: ['Cmd/Ctrl', 'Z'], action: 'Undo' }, - { keys: ['Cmd/Ctrl', 'Shift', 'Z'], action: 'Redo' }, + { keys: ['Delete / Backspace'], actionKey: 'shortcuts.deleteSelected' }, + { keys: ['Cmd/Ctrl', 'Z'], actionKey: 'shortcuts.undo' }, + { keys: ['Cmd/Ctrl', 'Shift', 'Z'], actionKey: 'shortcuts.redo' }, ], }, { - title: 'Selection', + titleKey: 'shortcuts.selection', shortcuts: [ { keys: ['Cmd/Ctrl', 'C'], - action: 'Copy the selected objects', - note: 'The copied selection can be pasted into another level, project, or browser tab.', + actionKey: 'shortcuts.copySelection', + noteKey: 'shortcuts.copySelectionNote', }, { keys: ['Cmd/Ctrl', 'X'], - action: 'Cut the selected objects', - note: 'Copies the selection to the clipboard, then removes it from this scene.', + actionKey: 'shortcuts.cutSelection', + noteKey: 'shortcuts.cutSelectionNote', }, { keys: ['Cmd/Ctrl', 'V'], - action: 'Paste and place copied objects', - note: - 'Carries a preview under the cursor. Click to place it, or press Escape to cancel.', + actionKey: 'shortcuts.pasteSelection', + noteKey: 'shortcuts.pasteSelectionNote', }, { keys: ['Cmd/Ctrl', 'Left click'], - action: 'Add or remove an object from multi-selection', - note: 'Works in Select mode on the 3D canvas, the 2D floor plan, and the scene graph.', + actionKey: 'shortcuts.addToSelection', + noteKey: 'shortcuts.addToSelectionNote', }, { keys: ['Shift', 'Left click'], - action: 'Add or remove an object from canvas multi-selection', - note: 'In the scene graph, Shift-click selects the visible range like a file browser.', + actionKey: 'shortcuts.addToCanvasSelection', + noteKey: 'shortcuts.addToCanvasSelectionNote', }, { keys: ['Left click'], - action: 'Move the whole multi-selection', - note: - 'With 2+ objects selected, in 2D and 3D alike: drag the selection (or its dashed box) to slide it; click it to pick it up and place with the next click.', + actionKey: 'shortcuts.moveMultiSelection', + noteKey: 'shortcuts.moveMultiSelectionNote', }, { keys: ['R', 'T'], - action: 'Rotate a multi-selection ±45° around its center', - note: 'Also works mid-move while carrying the selection.', + actionKey: 'shortcuts.rotateMultiSelection', + noteKey: 'shortcuts.rotateMultiSelectionNote', }, { keys: ['Cmd/Ctrl', 'G'], - action: 'Group the multi-selection (session only)', - note: - 'Editor-only. Plain click a member later to reselect the whole group. Not saved with the project.', + actionKey: 'shortcuts.groupSelection', + noteKey: 'shortcuts.groupSelectionNote', }, { keys: ['Cmd/Ctrl', 'Shift', 'G'], - action: 'Ungroup the session selection', - note: 'Keeps the current selection; only dissolves the session group.', + actionKey: 'shortcuts.ungroupSelection', + noteKey: 'shortcuts.ungroupSelectionNote', }, { keys: ['Esc'], - action: 'Clear the selection', - note: 'Clicking empty space does the same.', + actionKey: 'shortcuts.clearSelection', + noteKey: 'shortcuts.clearSelectionNote', }, ], }, { - title: 'Direct Manipulation', + titleKey: 'shortcuts.directManipulation', shortcuts: [ { keys: ['Cmd/Ctrl', 'Left click'], - action: 'Move the selected movable object under the cursor', - note: 'Drag in Select mode with a single object selected. Guided snapping and guides are enabled by default.', + actionKey: 'shortcuts.moveUnderCursor', + noteKey: 'shortcuts.moveUnderCursorNote', }, { keys: ['Cmd/Ctrl', 'Right click'], - action: 'Rotate the selected object under the cursor', - note: 'Drag left or right in Select mode with a single object selected. Rotation snaps to 15° increments by default.', + actionKey: 'shortcuts.rotateUnderCursor', + noteKey: 'shortcuts.rotateUnderCursorNote', }, { keys: ['Cmd/Ctrl', 'Shift', 'Right click'], - action: 'Rotate freely', - note: 'Hold Shift during the drag to bypass the 15° rotation increment.', + actionKey: 'shortcuts.rotateFreely', + noteKey: 'shortcuts.rotateFreelyNote', }, ], }, { - title: 'Drawing Tools', + titleKey: 'shortcuts.drawingTools', shortcuts: [ // Shift and Ctrl each mean one thing held and another tapped, and only // the hold was documented — which read as the taps not existing. Both // taps are listed first because they are the ones nobody discovers. { keys: ['Shift'], - action: 'Cycle the snapping mode', - note: 'Tap and release without pressing anything else, while a drawing or move gesture is available.', + actionKey: 'shortcuts.cycleSnapMode', + noteKey: 'shortcuts.cycleSnapModeNote', }, { keys: ['Cmd/Ctrl'], - action: 'Cycle the grid step: 0.5 m → 0.25 m → 0.1 m → 0.05 m', - note: 'Tap and release on its own. Use it when the default half-metre grid is too coarse — placing a window, for instance.', + actionKey: 'shortcuts.cycleGridStep', + noteKey: 'shortcuts.cycleGridStepNote', }, { keys: ['Shift'], - action: 'Bypass guided snapping and angle constraints', - note: 'Hold during the active gesture. Passive guide or measurement feedback may stay visible.', + actionKey: 'shortcuts.bypassGuidedConstraints', + noteKey: 'shortcuts.bypassGuidedConstraintsNote', }, { keys: ['Shift'], - action: 'Rotate freely, bypassing the default 15° rotation snap', - note: 'Hold while dragging a rotate handle or direct-rotation gesture.', + actionKey: 'shortcuts.bypassRotationSnap', + noteKey: 'shortcuts.bypassRotationSnapNote', }, ], }, { - title: 'Item Placement', + titleKey: 'shortcuts.itemPlacement', shortcuts: [ { keys: ['R', 'T'], - action: 'Rotate item; with a door selected, R toggles open/closed and T closes', + actionKey: 'shortcuts.rotateItemOrToggleDoor', }, { keys: ['E'], - action: 'Operate the selected node — doors, windows, and cabinet doors/drawers animate open/closed', + actionKey: 'shortcuts.operateSelectedNode', }, { keys: ['Shift'], - action: 'Temporarily bypass placement validation constraints', - note: 'Hold while placing.', + actionKey: 'shortcuts.bypassPlacementValidation', + noteKey: 'shortcuts.holdWhilePlacing', }, ], }, { - title: 'Camera', + titleKey: 'shortcuts.camera', shortcuts: [ { keys: ['W', 'A', 'S', 'D'], - action: 'Pan camera', - note: 'Moves in screen space, similar to dragging the camera view.', + actionKey: 'shortcuts.panCamera', + noteKey: 'shortcuts.panCameraNote', }, { keys: ['Middle click'], - action: 'Pan camera', - note: 'Drag with the middle mouse button, or hold Space while dragging with the left mouse button.', + actionKey: 'shortcuts.panCameraMiddle', + noteKey: 'shortcuts.dragMiddleMouseOrHoldSpace', }, { keys: ['Right click'], - action: 'Orbit camera', - note: 'Drag with the right mouse button.', + actionKey: 'shortcuts.orbitCamera', + noteKey: 'shortcuts.dragRightMouse', }, ], }, @@ -221,38 +219,38 @@ function ShortcutKeys({ keys }: { keys: string[] }) { } export function KeyboardShortcutsDialog() { + const t = useTranslations() return ( - Keyboard Shortcuts + {t('shortcuts.keyboardShortcuts')} - Shortcuts are context-aware. Guided constraints are enabled by default; hold Shift - during an active gesture to build freely. + {t('shortcuts.contextAware')}
{SHORTCUT_CATEGORIES.map((category) => ( -
-

{category.title}

+
+

{t(category.titleKey)}

{category.shortcuts.map((shortcut, index) => (
-

{shortcut.action}

- {shortcut.note ? ( -

{shortcut.note}

+

{t(shortcut.actionKey)}

+ {shortcut.noteKey ? ( +

{t(shortcut.noteKey)}

) : null}
{index < category.shortcuts.length - 1 ? ( diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx index 1b5f7172ba..82655bd110 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx @@ -28,6 +28,7 @@ import { type LinearUnit, squareMetersToAreaUnit, } from '../../../../../lib/measurements' +import { useTranslations } from '../../../../../lib/i18n' export type PendingImport = { fileName: string @@ -43,24 +44,24 @@ type Props = { type StatRow = { icon: typeof Building2 - label: string + labelKey: string count: number } function statsRows(stats: BuildStats): StatRow[] { return ( [ - { icon: MapPin, label: 'Sites', count: stats.byType.site ?? 0 }, - { icon: Building2, label: 'Buildings', count: stats.byType.building ?? 0 }, - { icon: Layers, label: 'Levels', count: stats.byType.level ?? 0 }, - { icon: Square, label: 'Walls', count: stats.byType.wall ?? 0 }, - { icon: DoorOpen, label: 'Doors', count: stats.byType.door ?? 0 }, - { icon: AppWindow, label: 'Windows', count: stats.byType.window ?? 0 }, - { icon: Box, label: 'Items', count: stats.byType.item ?? 0 }, - { icon: Square, label: 'Slabs', count: stats.byType.slab ?? 0 }, - { icon: Square, label: 'Ceilings', count: stats.byType.ceiling ?? 0 }, - { icon: Square, label: 'Zones', count: stats.byType.zone ?? 0 }, - { icon: Scan, label: 'Scans', count: stats.byType.scan ?? 0 }, + { icon: MapPin, labelKey: 'loadBuild.sites', count: stats.byType.site ?? 0 }, + { icon: Building2, labelKey: 'loadBuild.buildings', count: stats.byType.building ?? 0 }, + { icon: Layers, labelKey: 'loadBuild.levels', count: stats.byType.level ?? 0 }, + { icon: Square, labelKey: 'loadBuild.walls', count: stats.byType.wall ?? 0 }, + { icon: DoorOpen, labelKey: 'loadBuild.doors', count: stats.byType.door ?? 0 }, + { icon: AppWindow, labelKey: 'loadBuild.windows', count: stats.byType.window ?? 0 }, + { icon: Box, labelKey: 'loadBuild.items', count: stats.byType.item ?? 0 }, + { icon: Square, labelKey: 'loadBuild.slabs', count: stats.byType.slab ?? 0 }, + { icon: Square, labelKey: 'loadBuild.ceilings', count: stats.byType.ceiling ?? 0 }, + { icon: Square, labelKey: 'loadBuild.zones', count: stats.byType.zone ?? 0 }, + { icon: Scan, labelKey: 'loadBuild.scans', count: stats.byType.scan ?? 0 }, ] satisfies StatRow[] ).filter((row) => row.count > 0) } @@ -94,6 +95,7 @@ function formatFloorArea(m2: number, unit: LinearUnit): string { } export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { + const t = useTranslations() const [showAllWarnings, setShowAllWarnings] = useState(false) const [showSchemaIssues, setShowSchemaIssues] = useState(false) const unit = useViewer((state) => state.unit) @@ -122,11 +124,10 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { ) : ( )} - {ok ? 'Ready to import' : 'Cannot import this file'} + {ok ? t('loadBuild.readyToImport') : t('loadBuild.cannotImport')} - {fileName} · {formatFileSize(fileSizeBytes)} · {stats.total} node - {stats.total === 1 ? '' : 's'} + {fileName} · {formatFileSize(fileSizeBytes)} · {t('loadBuild.nodes', { count: stats.total })} @@ -135,7 +136,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
- {errors.length} error{errors.length === 1 ? '' : 's'} + {t('loadBuild.errors', { count: errors.length })}
    {errors.map((e) => ( @@ -148,7 +149,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { {stats.total > 0 && (
    - Structure + {t('loadBuild.structure')}
    {rows.length > 0 ? (
    @@ -159,11 +160,11 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { className={`flex items-center justify-between px-3 py-2 ${ i === rows.length - 1 ? '' : 'border-b' }`} - key={row.label} + key={row.labelKey} >
    - {row.label} + {t(row.labelKey)}
    {row.count}
    @@ -171,7 +172,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { })} {stats.floorAreaM2 > 0 && (
    - Floor area + {t('loadBuild.floorArea')} {formatFloorArea(stats.floorAreaM2, unit)} @@ -180,7 +181,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
    ) : (
    - The file contains no recognised nodes. + {t('loadBuild.noRecognisedNodes')}
    )}
    @@ -190,7 +191,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
    - {warnings.length} warning{warnings.length === 1 ? '' : 's'} + {t('loadBuild.warnings', { count: warnings.length })}
      {visibleWarnings.map((w, i) => ( @@ -203,7 +204,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { onClick={() => setShowAllWarnings(true)} type="button" > - Show {hiddenWarningCount} more + {t('loadBuild.showMore', { count: hiddenWarningCount })} )}
    @@ -217,11 +218,10 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { type="button" > - Schema details ({schemaIssueCount} node - {schemaIssueCount === 1 ? '' : 's'}) + {t('loadBuild.schemaDetails', { count: schemaIssueCount })} - {showSchemaIssues ? 'Hide' : 'Show'} + {showSchemaIssues ? t('common.hide') : t('common.show')} {showSchemaIssues && ( @@ -250,7 +250,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) { diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json index 294eeb55fb..cccfb76d4f 100644 --- a/packages/editor/src/lib/i18n/en.json +++ b/packages/editor/src/lib/i18n/en.json @@ -6,6 +6,7 @@ "materials.wood": "Wood", "commands.addLevel": "Add Level", "commands.back": "back", + "commands.cameraSwitchTo": "Camera: Switch to {mode}", "commands.ceilingTool": "Ceiling Tool", "commands.close": "close", "commands.commandPalette": "Command Palette", @@ -18,8 +19,9 @@ "commands.enterPreview": "Enter Preview", "commands.exitPreview": "Exit Preview", "commands.exploded": "Exploded", - "commands.exportGLB": "Export 3D Model (GLB)", + "commands.export3DModel": "Export 3D Model (GLB)", "commands.exportJSON": "Export Scene (JSON)", + "commands.exportSceneJson": "Export Scene (JSON)", "commands.filterOptions": "Filter options…", "commands.gotoLevel": "Go to Level", "commands.group.exportShare": "Export & Share", @@ -34,8 +36,12 @@ "commands.materialPaint": "Material Paint", "commands.navigate": "navigate", "commands.noCommandsFound": "No commands found.", + "commands.orthographic": "Orthographic", + "commands.perspective": "Perspective", "commands.redo": "Redo", "commands.renameLevel": "Rename Level", + "commands.renameTo": "Rename to {name}", + "commands.sculptTerrain": "Sculpt Terrain", "commands.searchActions": "Search actions…", "commands.select": "select", "commands.slabTool": "Slab Tool", @@ -220,10 +226,12 @@ "level.floor": "Floor {n}", "level.groundFloor": "Ground Floor", "level.insertLevelHere": "Insert level here", + "level.levelHeight": "Level height", "level.noElementsOnLevel": "No elements on this level", "level.noZonesOnLevel": "No zones on this level.", "level.pasteCopied": "Paste copied selection", "level.reorder": "Reorder", + "level.reorderName": "Reorder {name}", "level.selectLevelToView": "Select a level to view content", "levelDuplicate.cancel": "Cancel", "levelDuplicate.chooseWhatToCopy": "Choose what to copy from {level}.", @@ -246,6 +254,7 @@ "loadBuild.hide": "Hide", "loadBuild.items": "Items", "loadBuild.levels": "Levels", + "loadBuild.nodes": "{count} nodes", "loadBuild.noRecognisedNodes": "The file contains no recognised nodes.", "loadBuild.readyToImport": "Ready to import", "loadBuild.replaceScene": "Replace current scene", @@ -850,18 +859,29 @@ "settings.audio": "Audio", "settings.audioSettings": "Audio Settings", "settings.clearAndStartNew": "Clear & Start New", + "settings.copied": "Copied", + "settings.copyProjectId": "Copy project ID", "settings.dangerZone": "Danger Zone", "settings.exploreSceneGraph": "Explore scene graph", "settings.export": "Export", + "settings.export3DModel": "Export 3D Model", "settings.exportedGLB": "Export GLB", "settings.exportedOBJ": "Export OBJ", "settings.exportedSTL": "Export STL", "settings.exporting": "Generating...", + "settings.floorPlan": "Floor Plan", + "settings.floorplanDefault": "Default", + "settings.floorplanExpert": "Expert", + "settings.floorplanFull": "Full Plan", + "settings.floorplanStructure": "Structure Only", "settings.generateThumbnail": "Generate Thumbnail", "settings.keyboard": "Keyboard", "settings.loadBuild": "Load Build", "settings.masterVolume": "Master Volume", "settings.muteAllSounds": "Mute All Sounds", + "settings.project": "Project", + "settings.projectId": "Project ID", + "settings.projectIdCopied": "Project ID copied", "settings.public": "Public", "settings.radioVolume": "Radio Volume", "settings.saveAndLoad": "Save & Load", @@ -885,34 +905,75 @@ "settings.visibilityShowFloorplans": "Show Floorplans", "settings.visibilityShowGrid": "Show Grid", "settings.visibilityVisiblePublic": "Visible to public viewers", + "settings.visibleNodesOnly": "Visible nodes only", + "settings.visibleNodesOnlyDesc": "Only export nodes that are currently visible in the editor.", + "shortcuts.activateMeasurementTool": "Activate the measurement tool", "shortcuts.addOrRemoveFromSelection": "Add or remove an object from multi-selection", + "shortcuts.addToCanvasSelection": "Add or remove from canvas selection", + "shortcuts.addToCanvasSelectionNote": "Use Shift + left click in empty space to extend or shrink the canvas-level selection.", "shortcuts.addToSelection": "Add or remove an object from multi-selection", + "shortcuts.addToSelectionNote": "Cmd/Ctrl toggles individual objects in and out of the selection.", + "shortcuts.bypassGuidedConstraints": "Bypass guided placement constraints", + "shortcuts.bypassGuidedConstraintsNote": "While drawing walls, slabs, or ceilings, hold Shift to ignore guided snap lines.", "shortcuts.bypassPlacementValidation": "Temporarily bypass placement validation constraints", + "shortcuts.bypassRotationSnap": "Bypass rotation snap", + "shortcuts.bypassRotationSnapNote": "While placing an item, hold Shift to rotate freely without snapping.", "shortcuts.camera": "Camera", "shortcuts.cancelTool": "Cancel the active tool and return to Select mode", + "shortcuts.cancelToolNote": "Equivalent to clicking the Select tool in the toolbar.", + "shortcuts.clearSelection": "Clear selection", + "shortcuts.clearSelectionNote": "Press Escape while the cursor is over the canvas.", "shortcuts.contextAware": "Shortcuts are context-aware and depend on the current phase or tool.", + "shortcuts.copySelection": "Copy selection", + "shortcuts.copySelectionNote": "Use Cmd/Ctrl + V to paste the clipboard contents into the scene.", + "shortcuts.cutSelection": "Cut selection", + "shortcuts.cutSelectionNote": "Cut selection to the clipboard. Paste restores it at the cursor.", + "shortcuts.cycleGridStep": "Cycle grid step", + "shortcuts.cycleGridStepNote": "Hold Cmd/Ctrl while drawing to snap to a different grid step.", + "shortcuts.cycleSnapMode": "Cycle snap mode", + "shortcuts.cycleSnapModeNote": "Hold Shift while drawing to cycle between snap modes.", "shortcuts.delete": "Delete", "shortcuts.deleteOrBackspace": "Delete selected objects", "shortcuts.deleteSelected": "Delete selected objects", + "shortcuts.directManipulation": "Direct Manipulation", "shortcuts.disableAngleSnapping": "Temporarily disable angle snapping while drawing walls, slabs, and ceilings", "shortcuts.dragMiddleMouseOrHoldSpace": "Drag with the middle mouse button, or hold Space while dragging with the left mouse button.", "shortcuts.dragRightMouse": "Drag with the right mouse button.", "shortcuts.drawingTools": "Drawing Tools", "shortcuts.editorNavigation": "Editor Navigation", + "shortcuts.groupSelection": "Group selection", + "shortcuts.groupSelectionNote": "Combines selected objects into a single transformable group.", "shortcuts.holdWhileDrawing": "Hold while drawing.", "shortcuts.holdWhilePlacing": "Hold while placing.", "shortcuts.itemPlacement": "Item Placement", "shortcuts.keyboardShortcuts": "Keyboard Shortcuts", "shortcuts.modesAndHistory": "Modes & History", + "shortcuts.moveMultiSelection": "Move multi-selection", + "shortcuts.moveMultiSelectionNote": "Left click any selected object and drag to move the whole selection.", + "shortcuts.moveUnderCursor": "Move under cursor", + "shortcuts.moveUnderCursorNote": "Cmd/Ctrl + left click and drag any object to move it under the pointer.", "shortcuts.orbitCamera": "Orbit camera", + "shortcuts.operateSelectedNode": "Operate selected node", "shortcuts.panCamera": "Pan camera", + "shortcuts.panCameraMiddle": "Pan camera (middle mouse / Space)", + "shortcuts.panCameraNote": "WASD moves the camera horizontally; QE move it vertically.", + "shortcuts.pasteSelection": "Paste selection", + "shortcuts.pasteSelectionNote": "Place copied or cut items at the cursor position.", "shortcuts.redo": "Redo", + "shortcuts.rotateFreely": "Rotate freely under cursor", + "shortcuts.rotateFreelyNote": "Cmd/Ctrl + Shift + right click and drag to rotate the object freely.", "shortcuts.rotateItemClockwise": "Rotate item clockwise, or toggle selected door open/closed", "shortcuts.rotateItemCounterClockwise": "Rotate item counter-clockwise, or close selected door", + "shortcuts.rotateItemOrToggleDoor": "Rotate item or toggle door", + "shortcuts.rotateMultiSelection": "Rotate multi-selection", + "shortcuts.rotateMultiSelectionNote": "Press R then T to enter rotate mode, then drag to rotate the selection.", + "shortcuts.rotateUnderCursor": "Rotate under cursor", + "shortcuts.rotateUnderCursorNote": "Cmd/Ctrl + right click and drag to rotate the object under the pointer.", "shortcuts.selectNextLevel": "Select next level in the active building", "shortcuts.selectPreviousLevel": "Select previous level in the active building", "shortcuts.selection": "Selection", "shortcuts.switchToBuildMode": "Switch to Build mode", + "shortcuts.switchToDeleteMode": "Switch to Delete mode", "shortcuts.switchToFurnishLayer": "Switch to Furnish layer", "shortcuts.switchToFurnishPhase": "Switch to Furnish phase", "shortcuts.switchToSelectMode": "Switch to Select mode", @@ -922,6 +983,8 @@ "shortcuts.switchToZonesLayer": "Switch to Zones layer", "shortcuts.toggleSidebar": "Toggle sidebar", "shortcuts.undo": "Undo", + "shortcuts.ungroupSelection": "Ungroup selection", + "shortcuts.ungroupSelectionNote": "Disband the selected group back into individual objects.", "shortcuts.worksWhileInSelectMode": "Works while in Select mode.", "sidebar.file": "File", "sidebar.items": "Items", @@ -971,6 +1034,7 @@ "treeActions.updateSnapshot": "Update snapshot", "treeActions.viewSnapshot": "View snapshot", "viewer.building": "Building", + "viewer.belowCount": "{count} below", "viewer.camera": "Camera", "viewer.collapseSidebar": "Collapse sidebar", "viewer.couldNotAddGuideImage": "Could not add that guide image.", @@ -978,47 +1042,84 @@ "viewer.cutaway": "Cutaway", "viewer.deleteGuideImage": "Delete guide image", "viewer.deleteScan": "Delete scan", + "viewer.display": "Display", + "viewer.displaySettings": "Display settings", "viewer.edges": "Edges", "viewer.expandSidebar": "Expand sidebar", "viewer.exploded": "Exploded", "viewer.faintOutline": "Faint outline of major creases", "viewer.fileTooLarge": "File is too large. Maximum size is 200 MB.", "viewer.flatAndFast": "Flat and fast — no ambient occlusion", + "viewer.floorplanAnnotations": "Floor plan annotations", + "viewer.floorplanDefault": "Default", + "viewer.floorplanDefaultDetail": "Hide non-essential annotations and dimensions.", + "viewer.floorplanExpert": "Expert", + "viewer.floorplanExpertDetail": "Show every dimension, mark, and structural annotation.", + "viewer.floorplanMode": "Floor plan mode", + "viewer.faceOfStud": "Face of stud", + "viewer.faceOfStudDetail": "Measure to the inside face of the wall studs.", + "viewer.finishedFaces": "Finished faces", + "viewer.finishedFacesDetail": "Measure to the inside surface of the finished wall.", "viewer.fullAO": "Full ambient occlusion", "viewer.fullHeight": "Full Height", "viewer.grid": "Grid", "viewer.gridSnap": "Grid snap", + "viewer.guideImageDefault": "Guide image {index}", + "viewer.guideImages": "Guide images", "viewer.guideImageSettings": "Guide image settings", "viewer.guideImagesOnThisLevel": "{count} guide image on this level", "viewer.guideImagesOnThisLevel_plural": "{count} guide images on this level", "viewer.guides": "Guides", + "viewer.guidesState": "Guides: {state}", "viewer.hidden": "Hidden", "viewer.hideReferenceFloor": "Hide reference floor", "viewer.imperial": "Imperial (ft)", "viewer.levels": "Levels", + "viewer.levelsWithMode": "Levels: {mode}", "viewer.low": "Low", + "viewer.magneticSnap": "Magnetic snap", "viewer.manual": "Manual", + "viewer.measurements": "Measurements", + "viewer.measurements3d": "3D measurements", + "viewer.meters": "Meters", "viewer.metric": "Metric (m)", + "viewer.millimeters": "Millimeters", + "viewer.automaticDimensions": "Automatic dimensions", + "viewer.manualDimensions": "Manual dimensions", + "viewer.openingMarks": "Opening marks", + "viewer.structuralGrids": "Structural grids", + "viewer.roomLabels": "Room labels", + "viewer.stairAnnotations": "Stair annotations", "viewer.noEdgeLines": "No edge lines", "viewer.noGuideImagesOnLevel": "No guide images on this level yet.", + "viewer.noLowerFloor": "No lower floor to reference.", "viewer.noScansOnLevel": "No scans on this level yet.", "viewer.off": "Off", "viewer.off_edge": "Off", "viewer.on": "On", + "viewer.onThisLevel": "on this level", "viewer.opacity": "Opacity", "viewer.openProjectBeforeUpload": "Open a project before uploading a scan.", "viewer.orbitLeft": "Orbit Left", "viewer.orbitRight": "Orbit Right", "viewer.orthographic": "Orthographic", "viewer.perspective": "Perspective", + "viewer.preview": "Preview", + "viewer.previewMode": "Preview mode", "viewer.referenceFloorSettings": "Reference floor settings", + "viewer.referenceFloorWithLevel": "Reference floor: {level}", + "viewer.referencesEmptyText": "No references on this level yet.", + "viewer.referencesState": "References: {state}", "viewer.render": "Render", "viewer.rendered": "Rendered", + "viewer.riserDiagram": "Riser diagram", + "viewer.scanDefault": "Scan {index}", "viewer.scanSettings": "Scan settings", "viewer.scanUploadUnavailable": "Scan upload is unavailable.", "viewer.scans": "Scans", "viewer.scansOnThisLevel": "{count} scan on this level", "viewer.scansOnThisLevel_plural": "{count} scans on this level", + "viewer.scansState": "Scans: {state}", "viewer.sceneTheme": "Scene theme", "viewer.shadows": "Shadows", "viewer.showReferenceFloor": "Show reference floor", @@ -1028,6 +1129,8 @@ "viewer.stack": "Stack", "viewer.strong": "Strong", "viewer.topView": "Top View", + "viewer.translucent": "Translucent", + "viewer.units": "Units", "viewer.uploadGlbOrImage": "Upload a .glb/.gltf scan or an image.", "viewer.uploadScanOrGuide": "Upload scan or guide image", "viewer.uploading": "Uploading", @@ -1036,6 +1139,10 @@ "viewer.viewModeSplit": "Split", "viewer.visible": "Visible", "viewer.walkthrough": "Walkthrough", + "viewer.wallsWithMode": "Walls: {mode}", + "viewer.wallCenterline": "Wall centerline", + "viewer.wallCenterlineDetail": "Measure to the structural center of the wall.", + "viewer.wallDimensions": "Wall dimensions", "viewer.spawnPointHint": "Place a Spawn Point from the Build tab to control where walkthrough starts.", "zone.addOne": "Add one", "zone.noZonesOnLevel": "No zones on this level.", diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json index 758421d646..ebd99757b6 100644 --- a/packages/editor/src/lib/i18n/zh.json +++ b/packages/editor/src/lib/i18n/zh.json @@ -6,6 +6,7 @@ "materials.wood": "木材", "commands.addLevel": "添加楼层", "commands.back": "返回", + "commands.cameraSwitchTo": "相机:切换到 {mode}", "commands.ceilingTool": "天花板工具", "commands.close": "关闭", "commands.commandPalette": "命令面板", @@ -18,8 +19,9 @@ "commands.enterPreview": "进入预览", "commands.exitPreview": "退出预览", "commands.exploded": "爆炸", - "commands.exportGLB": "导出3D模型 (GLB)", + "commands.export3DModel": "导出3D模型 (GLB)", "commands.exportJSON": "导出场景 (JSON)", + "commands.exportSceneJson": "导出场景 (JSON)", "commands.filterOptions": "筛选选项…", "commands.gotoLevel": "跳转楼层", "commands.group.exportShare": "导出与分享", @@ -34,8 +36,12 @@ "commands.materialPaint": "材质绘制", "commands.navigate": "导航", "commands.noCommandsFound": "未找到命令。", + "commands.orthographic": "正交", + "commands.perspective": "透视", "commands.redo": "重做", "commands.renameLevel": "重命名楼层", + "commands.renameTo": "重命名为 {name}", + "commands.sculptTerrain": "雕刻地形", "commands.searchActions": "搜索操作…", "commands.select": "选择", "commands.slabTool": "楼板工具", @@ -220,10 +226,12 @@ "level.floor": "第 {n} 层", "level.groundFloor": "底层", "level.insertLevelHere": "在此处插入楼层", + "level.levelHeight": "楼层高度", "level.noElementsOnLevel": "此楼层上没有元素", "level.noZonesOnLevel": "此楼层上没有区域。", "level.pasteCopied": "粘贴已复制内容", "level.reorder": "重新排序", + "level.reorderName": "重新排序 {name}", "level.selectLevelToView": "选择楼层以查看内容", "levelDuplicate.cancel": "取消", "levelDuplicate.chooseWhatToCopy": "选择要从 {level} 复制的内容。", @@ -246,6 +254,7 @@ "loadBuild.hide": "隐藏", "loadBuild.items": "物品", "loadBuild.levels": "楼层", + "loadBuild.nodes": "{count} 个节点", "loadBuild.noRecognisedNodes": "该文件不包含可识别的节点。", "loadBuild.readyToImport": "准备导入", "loadBuild.replaceScene": "替换当前场景", @@ -843,18 +852,29 @@ "settings.audio": "音频", "settings.audioSettings": "音频设置", "settings.clearAndStartNew": "清除并重新开始", + "settings.copied": "已复制", + "settings.copyProjectId": "复制项目ID", "settings.dangerZone": "危险区域", "settings.exploreSceneGraph": "探索场景图", "settings.export": "导出", + "settings.export3DModel": "导出3D模型", "settings.exportedGLB": "导出GLB", "settings.exportedOBJ": "导出OBJ", "settings.exportedSTL": "导出STL", "settings.exporting": "生成中...", + "settings.floorPlan": "平面图", + "settings.floorplanDefault": "默认", + "settings.floorplanExpert": "高级", + "settings.floorplanFull": "完整平面图", + "settings.floorplanStructure": "仅结构", "settings.generateThumbnail": "生成缩略图", "settings.keyboard": "键盘", "settings.loadBuild": "加载构建", "settings.masterVolume": "主音量", "settings.muteAllSounds": "静音所有声音", + "settings.project": "项目", + "settings.projectId": "项目ID", + "settings.projectIdCopied": "项目ID已复制", "settings.public": "公开", "settings.radioVolume": "收音机音量", "settings.saveAndLoad": "保存与加载", @@ -878,34 +898,75 @@ "settings.visibilityShowFloorplans": "显示平面图", "settings.visibilityShowGrid": "显示网格", "settings.visibilityVisiblePublic": "对公开查看者可见", + "settings.visibleNodesOnly": "仅可见节点", + "settings.visibleNodesOnlyDesc": "仅导出当前在编辑器中可见的节点。", + "shortcuts.activateMeasurementTool": "激活测量工具", "shortcuts.addOrRemoveFromSelection": "从多选中添加或移除对象", + "shortcuts.addToCanvasSelection": "添加或移除画布选中", + "shortcuts.addToCanvasSelectionNote": "在空白处按住 Shift + 左键可扩展或缩小画布级选中范围。", "shortcuts.addToSelection": "添加或移除多选中的对象", + "shortcuts.addToSelectionNote": "Cmd/Ctrl 可逐个切换对象是否进入选中状态。", + "shortcuts.bypassGuidedConstraints": "绕过引导式放置约束", + "shortcuts.bypassGuidedConstraintsNote": "绘制墙、楼板或天花板时按住 Shift 可忽略引导吸附线。", "shortcuts.bypassPlacementValidation": "暂时绕过放置验证约束", + "shortcuts.bypassRotationSnap": "绕过旋转吸附", + "shortcuts.bypassRotationSnapNote": "放置物品时按住 Shift 可自由旋转而不吸附。", "shortcuts.camera": "相机", "shortcuts.cancelTool": "取消当前工具并返回选择模式", + "shortcuts.cancelToolNote": "等同于在工具栏中点击选择工具。", + "shortcuts.clearSelection": "清除选中", + "shortcuts.clearSelectionNote": "当光标位于画布上时按 Esc。", "shortcuts.contextAware": "快捷键是上下文感知的,取决于当前阶段或工具。", + "shortcuts.copySelection": "复制选中", + "shortcuts.copySelectionNote": "使用 Cmd/Ctrl + V 将剪贴板内容粘贴到场景中。", + "shortcuts.cutSelection": "剪切选中", + "shortcuts.cutSelectionNote": "将选中对象剪切到剪贴板,粘贴时恢复到光标处。", + "shortcuts.cycleGridStep": "循环网格步长", + "shortcuts.cycleGridStepNote": "绘制时按住 Cmd/Ctrl 可使用不同的网格步长进行吸附。", + "shortcuts.cycleSnapMode": "循环吸附模式", + "shortcuts.cycleSnapModeNote": "绘制时按住 Shift 可在吸附模式间循环切换。", "shortcuts.delete": "删除", "shortcuts.deleteOrBackspace": "删除选中对象", "shortcuts.deleteSelected": "删除选中对象", + "shortcuts.directManipulation": "直接操作", "shortcuts.disableAngleSnapping": "绘制墙、楼板和天花板时暂时禁用角度吸附", "shortcuts.dragMiddleMouseOrHoldSpace": "按住中键拖动,或按住空格键同时左键拖动。", "shortcuts.dragRightMouse": "右键拖动。", "shortcuts.drawingTools": "绘制工具", "shortcuts.editorNavigation": "编辑器导航", + "shortcuts.groupSelection": "编组选中", + "shortcuts.groupSelectionNote": "将所选对象合并为一个可变换的编组。", "shortcuts.holdWhileDrawing": "绘制时按住。", "shortcuts.holdWhilePlacing": "放置时按住。", "shortcuts.itemPlacement": "物品放置", "shortcuts.keyboardShortcuts": "键盘快捷键", "shortcuts.modesAndHistory": "模式与历史", + "shortcuts.moveMultiSelection": "移动多选中", + "shortcuts.moveMultiSelectionNote": "左键单击任意选中对象并拖动可整体移动选中范围。", + "shortcuts.moveUnderCursor": "在光标下移动", + "shortcuts.moveUnderCursorNote": "Cmd/Ctrl + 左键单击并拖动任意对象可在光标下移动它。", "shortcuts.orbitCamera": "旋转相机", + "shortcuts.operateSelectedNode": "操作选中节点", "shortcuts.panCamera": "平移相机", + "shortcuts.panCameraMiddle": "平移相机(中键 / 空格)", + "shortcuts.panCameraNote": "WASD 水平移动相机,QE 垂直移动。", + "shortcuts.pasteSelection": "粘贴选中", + "shortcuts.pasteSelectionNote": "将复制或剪切的物品放置在光标位置。", "shortcuts.redo": "重做", + "shortcuts.rotateFreely": "在光标下自由旋转", + "shortcuts.rotateFreelyNote": "Cmd/Ctrl + Shift + 右键拖动可自由旋转对象。", "shortcuts.rotateItemClockwise": "顺时针旋转物品,或切换选中门的开关", "shortcuts.rotateItemCounterClockwise": "逆时针旋转物品,或关闭选中的门", + "shortcuts.rotateItemOrToggleDoor": "旋转物品或切换门", + "shortcuts.rotateMultiSelection": "旋转多选中", + "shortcuts.rotateMultiSelectionNote": "先按 R 再按 T 进入旋转模式,然后拖动旋转选中范围。", + "shortcuts.rotateUnderCursor": "在光标下旋转", + "shortcuts.rotateUnderCursorNote": "Cmd/Ctrl + 右键拖动可旋转光标下的对象。", "shortcuts.selectNextLevel": "选择当前建筑的下一层", "shortcuts.selectPreviousLevel": "选择当前建筑的上一层", "shortcuts.selection": "选择", "shortcuts.switchToBuildMode": "切换到构建模式", + "shortcuts.switchToDeleteMode": "切换到删除模式", "shortcuts.switchToFurnishLayer": "切换到家具层", "shortcuts.switchToFurnishPhase": "切换到家具阶段", "shortcuts.switchToSelectMode": "切换到选择模式", @@ -915,6 +976,8 @@ "shortcuts.switchToZonesLayer": "切换到区域层", "shortcuts.toggleSidebar": "切换侧边栏", "shortcuts.undo": "撤销", + "shortcuts.ungroupSelection": "取消编组", + "shortcuts.ungroupSelectionNote": "将所选编组拆分为独立对象。", "shortcuts.worksWhileInSelectMode": "在选择模式下有效。", "sidebar.file": "文件", "sidebar.items": "物品", @@ -964,6 +1027,7 @@ "treeActions.updateSnapshot": "更新快照", "treeActions.viewSnapshot": "查看快照", "viewer.building": "建筑", + "viewer.belowCount": "下方 {count} 层", "viewer.camera": "相机", "viewer.collapseSidebar": "收起侧边栏", "viewer.couldNotAddGuideImage": "无法添加该参考图。", @@ -971,47 +1035,84 @@ "viewer.cutaway": "剖面", "viewer.deleteGuideImage": "删除参考图", "viewer.deleteScan": "删除扫描", + "viewer.display": "显示", + "viewer.displaySettings": "显示设置", "viewer.edges": "边缘线", "viewer.expandSidebar": "展开侧边栏", "viewer.exploded": "爆炸", + "viewer.faceOfStud": "立柱面", + "viewer.faceOfStudDetail": "测量到墙内立柱的内侧。", "viewer.faintOutline": "仅主要转折处的淡轮廓", "viewer.fileTooLarge": "文件过大。最大支持 200 MB。", + "viewer.finishedFaces": "成品面", + "viewer.finishedFacesDetail": "测量到成品墙的内表面。", "viewer.flatAndFast": "快速平坦 — 无环境光遮蔽", + "viewer.floorplanAnnotations": "平面图标注", + "viewer.floorplanDefault": "默认", + "viewer.floorplanDefaultDetail": "隐藏非必要的标注与尺寸。", + "viewer.floorplanExpert": "高级", + "viewer.floorplanExpertDetail": "显示所有尺寸、标记和结构标注。", + "viewer.floorplanMode": "平面图模式", "viewer.fullAO": "完整环境光遮蔽", "viewer.fullHeight": "全高", "viewer.grid": "网格", "viewer.gridSnap": "网格吸附", + "viewer.guideImageDefault": "参考图 {index}", + "viewer.guideImages": "参考图", "viewer.guideImageSettings": "参考图设置", "viewer.guideImagesOnThisLevel": "此楼层有 {count} 张参考图", "viewer.guideImagesOnThisLevel_plural": "此楼层有 {count} 张参考图", "viewer.guides": "参考图", + "viewer.guidesState": "参考图:{state}", "viewer.hidden": "隐藏", "viewer.hideReferenceFloor": "隐藏参考楼层", "viewer.imperial": "英制 (ft)", "viewer.levels": "楼层", + "viewer.levelsWithMode": "楼层:{mode}", "viewer.low": "低", + "viewer.magneticSnap": "磁性吸附", "viewer.manual": "手动", + "viewer.measurements": "尺寸标注", + "viewer.measurements3d": "3D 尺寸标注", + "viewer.meters": "米", "viewer.metric": "公制 (m)", + "viewer.millimeters": "毫米", + "viewer.automaticDimensions": "自动尺寸", + "viewer.manualDimensions": "手动尺寸", + "viewer.openingMarks": "开口标记", + "viewer.structuralGrids": "结构网格", + "viewer.roomLabels": "房间标签", + "viewer.stairAnnotations": "楼梯标注", "viewer.noEdgeLines": "无边缘线", "viewer.noGuideImagesOnLevel": "此楼层暂无参考图。", + "viewer.noLowerFloor": "下方没有可参考的楼层。", "viewer.noScansOnLevel": "此楼层暂无扫描。", "viewer.off": "关", "viewer.off_edge": "关闭", "viewer.on": "开", + "viewer.onThisLevel": "在此楼层", "viewer.opacity": "不透明度", "viewer.openProjectBeforeUpload": "请先打开项目再上传扫描。", "viewer.orbitLeft": "左旋转", "viewer.orbitRight": "右旋转", "viewer.orthographic": "正交", "viewer.perspective": "透视", + "viewer.preview": "预览", + "viewer.previewMode": "预览模式", "viewer.referenceFloorSettings": "参考楼层设置", + "viewer.referenceFloorWithLevel": "参考楼层:{level}", + "viewer.referencesEmptyText": "此楼层暂无参考。", + "viewer.referencesState": "参考:{state}", "viewer.render": "渲染", "viewer.rendered": "渲染", + "viewer.riserDiagram": "梯段示意图", + "viewer.scanDefault": "扫描 {index}", "viewer.scanSettings": "扫描设置", "viewer.scanUploadUnavailable": "扫描上传不可用。", "viewer.scans": "扫描", "viewer.scansOnThisLevel": "此楼层有 {count} 个扫描", "viewer.scansOnThisLevel_plural": "此楼层有 {count} 个扫描", + "viewer.scansState": "扫描:{state}", "viewer.sceneTheme": "场景主题", "viewer.shadows": "阴影", "viewer.showReferenceFloor": "显示参考楼层", @@ -1021,6 +1122,8 @@ "viewer.stack": "堆叠", "viewer.strong": "锐利", "viewer.topView": "顶视图", + "viewer.translucent": "半透明", + "viewer.units": "单位", "viewer.uploadGlbOrImage": "请上传 .glb/.gltf 扫描文件或图片。", "viewer.uploadScanOrGuide": "上传扫描或参考图", "viewer.uploading": "上传中", @@ -1029,6 +1132,10 @@ "viewer.viewModeSplit": "分屏", "viewer.visible": "可见", "viewer.walkthrough": "导览", + "viewer.wallsWithMode": "墙体:{mode}", + "viewer.wallCenterline": "墙体中线", + "viewer.wallCenterlineDetail": "测量到墙体的结构中心。", + "viewer.wallDimensions": "墙体尺寸", "viewer.spawnPointHint": "从构建选项卡放置出生点来控制导览开始位置。", "zone.addOne": "添加", "zone.noZonesOnLevel": "此楼层暂无区域。", From 9780af44653e35ac24eaba9668ea34e78ed82953 Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 21:36:45 +0800 Subject: [PATCH 013/106] fix(i18n): address Bugbot review findings #1, #2, #3, #6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #1: FitSceneButton now calls useTranslations() (was throwing ReferenceError on render). - #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. - #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. - #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 #4 (resolveBaseUrl) and #5 (editor-layout-v2 toolbar) — those predate this branch and are out of scope for the i18n restore PR. --- apps/editor/components/save-button.tsx | 50 ++++++++-------- .../components/PreviewToolbar.tsx | 3 +- .../ui/command-palette/editor-commands.tsx | 58 +++++++++---------- packages/editor/src/lib/i18n/en.json | 20 +++++++ packages/editor/src/lib/i18n/zh.json | 20 +++++++ 5 files changed, 94 insertions(+), 57 deletions(-) diff --git a/apps/editor/components/save-button.tsx b/apps/editor/components/save-button.tsx index 08fbbca498..eaed060ed0 100644 --- a/apps/editor/components/save-button.tsx +++ b/apps/editor/components/save-button.tsx @@ -1,7 +1,7 @@ 'use client' import type { SceneGraph } from '@pascal-app/editor' -import { useLocale, messages } from '@pascal-app/editor' +import { useTranslations } from '@pascal-app/editor' import { useRouter } from 'next/navigation' import { useCallback, useState } from 'react' @@ -17,17 +17,12 @@ interface SaveButtonProps { getGraph: () => SceneGraph | null } -const t = (key: string, locale: string) => { - const msgs = messages[locale as keyof typeof messages] as Record - return msgs[key] || key -} - /** * Creates a new empty scene and navigates the user to it. */ export function CreateSceneButton({ label }: { label?: string } = {}) { + const t = useTranslations() const router = useRouter() - const { locale } = useLocale() const [isCreating, setIsCreating] = useState(false) const [error, setError] = useState(null) @@ -38,20 +33,20 @@ export function CreateSceneButton({ label }: { label?: string } = {}) { const response = await fetch('/api/scenes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: t('untitledScene', locale), graph: EMPTY_GRAPH }), + body: JSON.stringify({ name: t('save.untitledScene'), graph: EMPTY_GRAPH }), }) if (!response.ok) { - setError(`${t('failedToCreateScene', locale)} (${response.status})`) + setError(`${t('save.failedToCreateScene')} (${response.status})`) return } const meta = (await response.json()) as { id: string } router.push(`/scene/${meta.id}`) } catch (err) { - setError(err instanceof Error ? err.message : t('failedToCreateScene', locale)) + setError(err instanceof Error ? err.message : t('save.failedToCreateScene')) } finally { setIsCreating(false) } - }, [locale, router]) + }, [router, t]) return (
    @@ -62,7 +57,7 @@ export function CreateSceneButton({ label }: { label?: string } = {}) { onClick={handleCreate} type="button" > - {isCreating ? t('creating', locale) : label ?? t('createNewScene', locale)} + {isCreating ? t('save.creating') : (label ?? t('save.createNewScene'))}
    ) @@ -74,15 +69,15 @@ export function CreateSceneButton({ label }: { label?: string } = {}) { * built-in autosave plumbing. */ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps) { + const t = useTranslations() const router = useRouter() - const { locale } = useLocale() const [isSaving, setIsSaving] = useState(false) const [status, setStatus] = useState(null) const handleSave = useCallback(async () => { const graph = getGraph() if (!graph) { - setStatus(t('noSceneToSave', locale)) + setStatus(t('save.noSceneToSave')) return } setIsSaving(true) @@ -97,28 +92,29 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps body: JSON.stringify({ name, graph }), }) if (response.status === 409) { - setStatus(t('conflictReload', locale)) + setStatus(t('save.conflictReload')) return } if (!response.ok) { - setStatus(`${t('saveFailed', locale)} (${response.status})`) + setStatus(`${t('save.saveFailed')} (${response.status})`) return } - setStatus(t('saved', locale)) + setStatus(t('save.saved')) } catch (error) { - setStatus(error instanceof Error ? error.message : t('saveFailed', locale)) + setStatus(error instanceof Error ? error.message : t('save.saveFailed')) } finally { setIsSaving(false) } - }, [getGraph, locale, name, sceneId, version]) + }, [getGraph, name, sceneId, t, version]) const handleSaveAs = useCallback(async () => { const graph = getGraph() if (!graph) { - setStatus(t('noSceneToSave', locale)) + setStatus(t('save.noSceneToSave')) return } - const newName = typeof window !== 'undefined' ? window.prompt(t('newSceneName', locale), name) : null + const newName = + typeof window !== 'undefined' ? window.prompt(t('save.newSceneName'), name) : null if (!newName) return setIsSaving(true) setStatus(null) @@ -129,17 +125,17 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps body: JSON.stringify({ name: newName, graph }), }) if (!response.ok) { - setStatus(`${t('saveAsFailed', locale)} (${response.status})`) + setStatus(`${t('save.saveAsFailed')} (${response.status})`) return } const meta = (await response.json()) as { id: string } router.push(`/scene/${meta.id}`) } catch (error) { - setStatus(error instanceof Error ? error.message : t('saveAsFailed', locale)) + setStatus(error instanceof Error ? error.message : t('save.saveAsFailed')) } finally { setIsSaving(false) } - }, [getGraph, locale, name, router]) + }, [getGraph, name, router, t]) return (
    @@ -149,7 +145,7 @@ export function SaveButton({ sceneId, name, version, getGraph }: SaveButtonProps onClick={handleSave} type="button" > - {isSaving ? t('saving', locale) : t('save', locale)} + {isSaving ? t('save.saving') : t('save.save')} {status && {status}}
    ) -} +} \ No newline at end of file diff --git a/apps/ifc-converter/components/PreviewToolbar.tsx b/apps/ifc-converter/components/PreviewToolbar.tsx index bed5e98f5b..021e0a567f 100644 --- a/apps/ifc-converter/components/PreviewToolbar.tsx +++ b/apps/ifc-converter/components/PreviewToolbar.tsx @@ -121,6 +121,7 @@ export function PreviewToolbar() { } export function FitSceneButton({ onFit }: { onFit: () => void }) { + const t = useTranslations() return ( ) } diff --git a/packages/editor/src/components/ui/command-palette/editor-commands.tsx b/packages/editor/src/components/ui/command-palette/editor-commands.tsx index 4d98109d3a..f0308525e1 100644 --- a/packages/editor/src/components/ui/command-palette/editor-commands.tsx +++ b/packages/editor/src/components/ui/command-palette/editor-commands.tsx @@ -81,7 +81,7 @@ export function EditorCommands() { { id: 'editor.tool.wall', label: t('commands.wallTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['draw', 'build', 'structure'], execute: () => activateTool('wall'), @@ -89,7 +89,7 @@ export function EditorCommands() { { id: 'editor.tool.slab', label: t('commands.slabTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['floor', 'build'], execute: () => activateTool('slab'), @@ -97,7 +97,7 @@ export function EditorCommands() { { id: 'editor.tool.ceiling', label: t('commands.ceilingTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['top', 'build'], execute: () => activateTool('ceiling'), @@ -105,7 +105,7 @@ export function EditorCommands() { { id: 'editor.tool.door', label: t('commands.doorTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['opening', 'entrance'], execute: () => activateTool('door'), @@ -113,7 +113,7 @@ export function EditorCommands() { { id: 'editor.tool.window', label: t('commands.windowTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['opening', 'glass'], execute: () => activateTool('window'), @@ -121,7 +121,7 @@ export function EditorCommands() { { id: 'editor.tool.item', label: t('commands.itemTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['furniture', 'object', 'asset', 'furnish'], execute: () => activateTool('item'), @@ -129,7 +129,7 @@ export function EditorCommands() { { id: 'editor.tool.stair', label: t('commands.stairTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['stairs', 'staircase', 'flight', 'landing', 'steps'], execute: () => activateTool('stair'), @@ -137,7 +137,7 @@ export function EditorCommands() { { id: 'editor.tool.zone', label: t('commands.zoneTool'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['area', 'room', 'space'], execute: () => activateTool('zone'), @@ -145,7 +145,7 @@ export function EditorCommands() { { id: 'editor.delete-selection', label: t('commands.deleteSelection'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['remove', 'erase'], shortcut: ['⌫'], @@ -159,7 +159,7 @@ export function EditorCommands() { { id: 'editor.mode.material-paint', label: t('commands.materialPaint'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['paint', 'material', 'texture', 'bucket', 'surface'], shortcut: ['P'], @@ -174,7 +174,7 @@ export function EditorCommands() { { id: 'editor.mode.terrain-sculpt', label: t('commands.sculptTerrain'), - group: t('commands.groupScene'), + group: t('commands.group.scene'), icon: , keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'], shortcut: ['G'], @@ -187,7 +187,7 @@ export function EditorCommands() { { id: 'editor.level.goto', label: t('commands.gotoLevel'), - group: t('commands.groupLevels'), + group: t('commands.group.levels'), icon: , keywords: ['level', 'floor', 'go', 'navigate', 'switch', 'select'], navigate: true, @@ -197,7 +197,7 @@ export function EditorCommands() { { id: 'editor.level.add', label: t('commands.addLevel'), - group: t('commands.groupLevels'), + group: t('commands.group.levels'), icon: , keywords: ['level', 'floor', 'add', 'create', 'new'], execute: () => @@ -221,7 +221,7 @@ export function EditorCommands() { { id: 'editor.level.rename', label: t('commands.renameLevel'), - group: t('commands.groupLevels'), + group: t('commands.group.levels'), icon: , keywords: ['level', 'floor', 'rename', 'name'], navigate: true, @@ -237,7 +237,7 @@ export function EditorCommands() { { id: 'editor.level.delete', label: t('commands.deleteLevel'), - group: t('commands.groupLevels'), + group: t('commands.group.levels'), icon: , keywords: ['level', 'floor', 'delete', 'remove'], when: () => { @@ -258,7 +258,7 @@ export function EditorCommands() { { id: 'editor.viewer.wall-mode', label: t('commands.wallMode'), - group: t('commands.groupViewerControls'), + group: t('commands.group.viewerControls'), icon: , keywords: ['wall', 'cutaway', 'up', 'down', 'translucent', 'view'], badge: () => { @@ -279,7 +279,7 @@ export function EditorCommands() { { id: 'editor.viewer.level-mode', label: t('commands.levelMode'), - group: t('commands.groupViewerControls'), + group: t('commands.group.viewerControls'), icon: , keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'], badge: () => { @@ -304,7 +304,7 @@ export function EditorCommands() { const next = mode === 'perspective' ? t('commands.orthographic') : t('commands.perspective') return t('commands.cameraSwitchTo', { mode: next }) }, - group: t('commands.groupViewerControls'), + group: t('commands.group.viewerControls'), icon:
@@ -441,7 +446,12 @@ function ScansControl() {

{t('viewer.scans')}

{hasScans && (

- {t('viewer.scansOnThisLevel', { count: scans.length })} + {t( + scans.length === 1 + ? 'viewer.scansOnThisLevel' + : 'viewer.scansOnThisLevel_plural', + { count: scans.length }, + )}

)}
diff --git a/packages/editor/src/lib/i18n.tsx b/packages/editor/src/lib/i18n.tsx index cb80f2487b..7d2a133478 100644 --- a/packages/editor/src/lib/i18n.tsx +++ b/packages/editor/src/lib/i18n.tsx @@ -1,6 +1,6 @@ 'use client' -import { createContext, useContext, useState, useEffect, type ReactNode } from 'react' +import { createContext, useContext, useMemo, useState, useEffect, type ReactNode } from 'react' import { IntlProvider } from 'react-intl' import en from './i18n/en.json' import zh from './i18n/zh.json' @@ -32,14 +32,18 @@ export function useLocale() { export function useTranslations() { const { locale } = useContext(I18nContext) - return (key: string, params?: Record): string => { - const str = messages[locale][key] ?? key - if (!params) return str - return Object.entries(params).reduce( - (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), - str, - ) - } + return useMemo( + () => + (key: string, params?: Record): string => { + const str = messages[locale][key] ?? key + if (!params) return str + return Object.entries(params).reduce( + (s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)), + str, + ) + }, + [locale], + ) } export function I18nProvider({ children }: { children: ReactNode }) { diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json index 7f2148b05b..b76a7ff64e 100644 --- a/packages/editor/src/lib/i18n/en.json +++ b/packages/editor/src/lib/i18n/en.json @@ -857,7 +857,8 @@ "scenes.noScenes": "No scenes yet. Create one to get started.", "scenes.noThumbnail": "No thumbnail", "scenes.nodes": "{count} nodes", - "scenes.sceneCount": "{count} scenes", + "scenes.sceneCount": "{count} scene", + "scenes.sceneCount_plural": "{count} scenes", "scenes.title": "Your scenes", "scenes.noScenesSaved": "No scenes saved yet", "save.conflictReload": "Conflict — scene changed elsewhere. Reload to continue.", diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json index d0e4d16056..b4ec5be97c 100644 --- a/packages/editor/src/lib/i18n/zh.json +++ b/packages/editor/src/lib/i18n/zh.json @@ -851,6 +851,7 @@ "scenes.noThumbnail": "无缩略图", "scenes.nodes": "{count} 个节点", "scenes.sceneCount": "{count} 个场景", + "scenes.sceneCount_plural": "{count} 个场景", "scenes.title": "您的场景", "scenes.noScenesSaved": "暂无保存的场景", "save.conflictReload": "冲突 — 场景在其他地方被修改。请重新加载以继续。", From 10f2eed1258947fbe3c40404f1c916c524dacd90 Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 21:57:47 +0800 Subject: [PATCH 015/106] fix(i18n): add translucent to command palette wall mode (Bugbot D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- packages/editor/src/components/ui/command-palette/index.tsx | 5 +++-- packages/editor/src/lib/i18n/en.json | 1 + packages/editor/src/lib/i18n/zh.json | 1 + packages/viewer/src/store/use-viewer.d.ts | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/components/ui/command-palette/index.tsx b/packages/editor/src/components/ui/command-palette/index.tsx index 060b6fdb06..5067315fd1 100644 --- a/packages/editor/src/components/ui/command-palette/index.tsx +++ b/packages/editor/src/components/ui/command-palette/index.tsx @@ -246,10 +246,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm setOpen(false) } - const wallModeLabel: Record<'cutaway' | 'up' | 'down', string> = { + const wallModeLabel: Record<'cutaway' | 'up' | 'down' | 'translucent', string> = { cutaway: t('commands.cutaway'), up: t('commands.up'), down: t('commands.down'), + translucent: t('commands.translucent'), } const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = { manual: t('commands.manual'), @@ -375,7 +376,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm {/* ── Wall Mode sub-page ────────────────────────────────────── */} {page === 'wall-mode' && ( - {(['cutaway', 'up', 'down'] as const).map((mode) => ( + {(['cutaway', 'up', 'down', 'translucent'] as const).map((mode) => ( void levelMode: 'stacked' | 'exploded' | 'solo' | 'manual' setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void - wallMode: 'up' | 'cutaway' | 'down' - setWallMode: (mode: 'up' | 'cutaway' | 'down') => void + wallMode: 'up' | 'cutaway' | 'down' | 'translucent' + setWallMode: (mode: 'up' | 'cutaway' | 'down' | 'translucent') => void showScans: boolean setShowScans: (show: boolean) => void showGuides: boolean From 94419f1d7357c544e3d026cf8a0f0902393e2d43 Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 22:05:46 +0800 Subject: [PATCH 016/106] =?UTF-8?q?fix(i18n):=20address=20Bugbot=20review?= =?UTF-8?q?=20pass=203=20=E2=80=94=20E,=20F,=20G?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- apps/editor/app/scenes/scenes-list.tsx | 2 +- .../components/ui/action-menu/view-toggles.tsx | 17 ++++++++++------- packages/editor/src/lib/i18n/en.json | 2 +- packages/editor/src/lib/i18n/zh.json | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/editor/app/scenes/scenes-list.tsx b/apps/editor/app/scenes/scenes-list.tsx index 915f5c79f2..ce48447a28 100644 --- a/apps/editor/app/scenes/scenes-list.tsx +++ b/apps/editor/app/scenes/scenes-list.tsx @@ -52,7 +52,7 @@ export function ScenesList({ scenes }: { scenes: SceneMeta[] }) { {scenes.length === 0 ? (
-

{t('scenes.noScenes')}

+

{t('scenes.noScenesSaved')}

diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index c1a845159b..4dc4acff16 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -541,7 +541,8 @@ function ScansControl() { function ReferenceListSection({ title, iconSrc, - noun, + countKey, + defaultNoun, emptyText, nodes, show, @@ -550,7 +551,8 @@ function ReferenceListSection({ }: { title: string iconSrc: string - noun: string + countKey: string + defaultNoun: string emptyText: string nodes: (GuideNode | ScanNode)[] show: boolean @@ -584,8 +586,7 @@ function ReferenceListSection({

{title}

{hasItems && (

- {nodes.length} {noun} - {nodes.length !== 1 ? 's' : ''} {t('viewer.onThisLevel')} + {t(nodes.length === 1 ? countKey : `${countKey}_plural`, { count: nodes.length })}

)}
@@ -625,7 +626,7 @@ function ReferenceListSection({ />

{node.name || - `${noun.charAt(0).toUpperCase()}${noun.slice(1)} ${index + 1}`} + `${defaultNoun.charAt(0).toUpperCase()}${defaultNoun.slice(1)} ${index + 1}`}

{selectedReferenceId === node.id && ( @@ -751,10 +752,11 @@ function ReferencesControl() {
)}
Date: Mon, 31 Aug 2026 22:16:20 +0800 Subject: [PATCH 017/106] =?UTF-8?q?fix(i18n):=20address=20Bugbot=20review?= =?UTF-8?q?=20pass=204=20=E2=80=94=20H,=20I,=20J?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../ui/action-menu/view-toggles.tsx | 11 ++++----- .../components/ui/level-duplicate-dialog.tsx | 23 ++++++++----------- packages/editor/src/lib/i18n/en.json | 23 +++++++++++++++++++ packages/editor/src/lib/i18n/zh.json | 23 +++++++++++++++++++ 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index 4dc4acff16..1c5b348824 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -542,7 +542,7 @@ function ReferenceListSection({ title, iconSrc, countKey, - defaultNoun, + defaultNameKey, emptyText, nodes, show, @@ -552,7 +552,7 @@ function ReferenceListSection({ title: string iconSrc: string countKey: string - defaultNoun: string + defaultNameKey: string emptyText: string nodes: (GuideNode | ScanNode)[] show: boolean @@ -625,8 +625,7 @@ function ReferenceListSection({ src={iconSrc} />

- {node.name || - `${defaultNoun.charAt(0).toUpperCase()}${defaultNoun.slice(1)} ${index + 1}`} + {node.name || t(defaultNameKey, { index: index + 1 })}

{selectedReferenceId === node.id && ( @@ -753,7 +752,7 @@ function ReferencesControl() { )} ) => string) { + if (!level) return t('level.thisLevel') + if (level.name) return level.name + if (level.level === 0) return t('level.groundFloor') + if (level.level > 0) return t('level.floor', { n: level.level }) + return t('level.basement', { n: -level.level }) } export function LevelDuplicateDialog({ @@ -59,14 +61,7 @@ export function LevelDuplicateDialog({ onOpenChange: (open: boolean) => void }) { const [preset, setPreset] = useState('everything') - const { locale } = useLocale() - const t = (key: string, params?: Record) => { - const str = (messages[locale as 'en' | 'zh'] as Record)[key] || key - if (!params) return str - return Object.entries(params).reduce( - (s, [k, v]) => s.replace(new RegExp(`\\{${k}}`, 'g'), String(v)), str, - ) - } + const t = useTranslations() useEffect(() => { if (open) { @@ -79,7 +74,7 @@ export function LevelDuplicateDialog({ {t('levelDuplicate.duplicateLevel')} - {t('levelDuplicate.chooseWhatToCopy', { level: getLevelLabel(level, locale) })} + {t('levelDuplicate.chooseWhatToCopy', { level: getLevelLabel(level, t) })}
diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json index baa2653d4d..8a8f65fa47 100644 --- a/packages/editor/src/lib/i18n/en.json +++ b/packages/editor/src/lib/i18n/en.json @@ -235,6 +235,7 @@ "level.levelHeight": "Level height", "level.noElementsOnLevel": "No elements on this level", "level.noZonesOnLevel": "No zones on this level.", + "level.thisLevel": "this level", "level.pasteCopied": "Paste copied selection", "level.reorder": "Reorder", "level.reorderName": "Reorder {name}", @@ -1029,6 +1030,28 @@ "snapshot.saved": "Saved", "snapshot.standard": "Standard", "snapshot.viewport": "Viewport", + "structureTools.ceiling": "Ceiling", + "structureTools.column": "Column", + "structureTools.door": "Door", + "structureTools.duct": "Duct", + "structureTools.ductFitting": "Duct fitting", + "structureTools.dwvPipe": "DWV pipe", + "structureTools.elevator": "Elevator", + "structureTools.fence": "Fence", + "structureTools.gableRoof": "Gable roof", + "structureTools.hvacUnit": "HVAC unit", + "structureTools.lineset": "Line set", + "structureTools.liquidLine": "Liquid line", + "structureTools.pipeFitting": "Pipe fitting", + "structureTools.register": "Register", + "structureTools.shelf": "Shelf", + "structureTools.slab": "Slab", + "structureTools.spawnPoint": "Spawn point", + "structureTools.stairs": "Stairs", + "structureTools.trap": "Trap", + "structureTools.wall": "Wall", + "structureTools.window": "Window", + "structureTools.zone": "Zone", "toolbar.cutaway": "Cutaway", "toolbar.fullHeight": "Full Height", "toolbar.low": "Low", diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json index a08a6878fd..07a890499d 100644 --- a/packages/editor/src/lib/i18n/zh.json +++ b/packages/editor/src/lib/i18n/zh.json @@ -237,6 +237,7 @@ "level.noZonesOnLevel": "此楼层上没有区域。", "level.pasteCopied": "粘贴已复制内容", "level.reorder": "重新排序", + "level.thisLevel": "此楼层", "level.reorderName": "重新排序 {name}", "level.selectLevelToView": "选择楼层以查看内容", "levelDuplicate.cancel": "取消", @@ -1022,6 +1023,28 @@ "snapshot.saved": "已保存", "snapshot.standard": "标准", "snapshot.viewport": "视口", + "structureTools.ceiling": "天花板", + "structureTools.column": "立柱", + "structureTools.door": "门", + "structureTools.duct": "风管", + "structureTools.ductFitting": "风管配件", + "structureTools.dwvPipe": "DWV 管", + "structureTools.elevator": "电梯", + "structureTools.fence": "围栏", + "structureTools.gableRoof": "山墙屋顶", + "structureTools.hvacUnit": "暖通设备", + "structureTools.lineset": "冷媒管", + "structureTools.liquidLine": "液管", + "structureTools.pipeFitting": "管件", + "structureTools.register": "出风口", + "structureTools.shelf": "搁板", + "structureTools.slab": "楼板", + "structureTools.spawnPoint": "生成点", + "structureTools.stairs": "楼梯", + "structureTools.trap": "存水弯", + "structureTools.wall": "墙体", + "structureTools.window": "窗", + "structureTools.zone": "区域", "toolbar.cutaway": "剖面", "toolbar.fullHeight": "全高", "toolbar.low": "低", From 739004aecfe720b3b785d2eca8d95899a34bffdd Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 22:24:03 +0800 Subject: [PATCH 018/106] chore: retrigger Bugbot review From 3fb7c524c10b82ebb3a75eea9ffa6cc1cfd11712 Mon Sep 17 00:00:00 2001 From: leapar Date: Mon, 31 Aug 2026 22:40:48 +0800 Subject: [PATCH 019/106] =?UTF-8?q?fix(i18n):=20address=20Bugbot=20review?= =?UTF-8?q?=20pass=205=20=E2=80=94=20K,=20L,=20M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - K: editor.localWarning now reads 'This is a blank canvas — your saved scenes live under' (en) / '这里是空白画布 — 你保存的场景在' (zh). The previous copy claimed scenes weren't saved, but the banner also links to /scenes where they are listed, which was contradictory. - L: removed apps/editor/messages/{en,zh}.json. The nested-format duplicates were never imported anywhere — runtime translations come exclusively from packages/editor/src/lib/i18n — so they only ever drifted from the real source of truth. - M: GuideImages and Scans popover empty states (and the matching ReferenceListSection calls inside the combined References view) now use viewer.noGuideImagesOnLevel / viewer.noScansOnLevel, not the generic viewer.referencesEmptyText. Removed the now-unused viewer.referencesEmptyText key from both catalogs. --- apps/editor/messages/en.json | 317 ------------------ apps/editor/messages/zh.json | 317 ------------------ .../ui/action-menu/view-toggles.tsx | 8 +- packages/editor/src/lib/i18n/en.json | 3 +- packages/editor/src/lib/i18n/zh.json | 3 +- 5 files changed, 6 insertions(+), 642 deletions(-) delete mode 100644 apps/editor/messages/en.json delete mode 100644 apps/editor/messages/zh.json diff --git a/apps/editor/messages/en.json b/apps/editor/messages/en.json deleted file mode 100644 index 98f7a0d5ea..0000000000 --- a/apps/editor/messages/en.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "common": { - "save": "Save", - "delete": "Delete", - "cancel": "Cancel", - "confirm": "Confirm", - "close": "Close", - "settings": "Settings", - "home": "Home", - "loading": "Loading...", - "error": "Error", - "success": "Success", - "search": "Search", - "noResults": "No results found", - "untitled": "Untitled", - "edit": "Edit", - "move": "Move", - "duplicate": "Duplicate", - "copy": "Copy", - "paste": "Paste", - "cut": "Cut", - "undo": "Undo", - "redo": "Redo", - "rotate": "Rotate", - "select": "Select", - "deselect": "Deselect", - "show": "Show", - "hide": "Hide", - "open": "Open", - "add": "Add", - "remove": "Remove", - "reset": "Reset", - "apply": "Apply", - "done": "Done", - "back": "Back", - "next": "Next", - "yes": "Yes", - "no": "No", - "creating": "Creating…", - "saving": "Saving…", - "saved": "Saved", - "save": "Save", - "saveAs": "Save as…", - "noSceneToSave": "No scene to save", - "failedToCreateScene": "Failed to create scene", - "saveFailed": "Save failed", - "saveAsFailed": "Save-as failed", - "conflictReload": "Conflict — reload to continue", - "newSceneName": "New scene name", - "untitledScene": "Untitled scene", - "createNewScene": "Create new scene", - "scene.sessionConflict": "Another session saved first — refresh?", - "scene.sessionConflictDesc": "Your changes haven't been saved. Reload to pick up the latest version.", - "scene.reload": "Reload", - "scene.dismiss": "Dismiss", - "scene.allScenes": "All scenes", - "scene.scene": "Scene", - "scene.connectionClosed": "Live scene connection closed", - "scene.saveFailed": "Save failed" - }, - "nav": { - "home": "Home", - "scenes": "Scenes", - "privacy": "Privacy", - "terms": "Terms" - }, - "sidebar": { - "scene": "Scene", - "build": "Build", - "items": "Items", - "settings": "Settings", - "site": "Site", - "structure": "Structure", - "furnish": "Furnish", - "view": "View", - "paint": "Paint" - }, - "editor": { - "localWarning": "Local editor — scenes are not saved.", - "openSavedScenes": "Open saved scenes", - "openRecent": "Open recent scenes", - "createNew": "Create new", - "placeBuilding": "Place building", - "rotate": "Rotate", - "chooseRoom": "Choose room", - "selectCurvedWall": "Select curved wall", - "setDiameter": "Set diameter", - "setCorner": "Set corner", - "rotateRoofDirection": "Rotate roof direction 90°", - "place": "Place", - "forcePlace": "Force place", - "guidedConstraintsBypassed": "Guided constraints bypassed" - }, - "scenes": { - "title": "Your scenes", - "noScenes": "No scenes yet. Create one to get started.", - "noScenesSaved": "You haven't saved any scenes yet.", - "sceneCount": "{count} scene | {count} scenes", - "noThumbnail": "No thumbnail", - "nodes": "{count} nodes", - "delete": "Delete", - "deleteConfirm": "Are you sure you want to delete this scene?", - "createScene": "Create scene", - "allScenes": "All scenes" - }, - "toolbar": { - "cutaway": "Cutaway", - "low": "Low", - "up": "Up", - "fullHeight": "Full Height" - }, - "errors": { - "loadFailed": "Failed to load", - "saveFailed": "Failed to save", - "deleteFailed": "Failed to delete", - "networkError": "Network error. Please try again.", - "conflict": "Another session saved first — refresh?", - "anotherSessionSaved": "Another session saved first — refresh?", - "noActiveProject": "No active project. Please open a project first." - }, - "privacy": { - "title": "Privacy Policy", - "lastUpdated": "Last updated" - }, - "terms": { - "title": "Terms of Service", - "lastUpdated": "Last updated" - }, - "nodeTypes": { - "site": "Site", - "building": "Building", - "level": "Level", - "wall": "Wall", - "door": "Door", - "window": "Window", - "slab": "Slab", - "ceiling": "Ceiling", - "roof": "Roof", - "zone": "Zone", - "item": "Item", - "fence": "Fence", - "column": "Column", - "elevator": "Elevator", - "stair": "Stair", - "stairs": "Stairs", - "gableRoof": "Gable Roof", - "spawn": "Spawn Point", - "shelf": "Shelf", - "scan": "Scan", - "guide": "Guide", - "chimney": "Chimney", - "skyLight": "Skylight" - }, - "commands": { - "wallTool": "Wall Tool", - "slabTool": "Slab Tool", - "ceilingTool": "Ceiling Tool", - "doorTool": "Door Tool", - "windowTool": "Window Tool", - "itemTool": "Item Tool", - "stairTool": "Stair Tool", - "zoneTool": "Zone Tool", - "deleteSelection": "Delete Selection", - "materialPaint": "Material Paint", - "goToLevel": "Go to Level", - "addLevel": "Add Level", - "renameLevel": "Rename Level", - "deleteLevel": "Delete Level", - "wallMode": "Wall Mode", - "levelMode": "Level Mode", - "switchToSolid": "Switch to Solid", - "switchToRendered": "Switch to Rendered", - "takeSnapshot": "Take Snapshot", - "enterPreview": "Enter Preview", - "exitPreview": "Exit Preview", - "toggleFullscreen": "Toggle Fullscreen", - "exportSceneJson": "Export Scene (JSON)", - "export3DModel": "Export 3D Model (GLB)", - "copyShareLink": "Copy Share Link", - "takeScreenshot": "Take Screenshot" - }, - "controlModes": { - "select": "Select", - "boxSelect": "Box select", - "editSite": "Edit site", - "build": "Build", - "materialPaint": "Material Paint", - "furnish": "Furnish", - "zone": "Zone", - "exitSiteEditing": "Exit site editing", - "siteEditGroundOnly": "Site editing only available on ground floor" - }, - "structureTools": { - "wall": "Wall", - "door": "Door", - "window": "Window", - "stairs": "Stairs", - "gableRoof": "Gable Roof", - "fence": "Fence", - "column": "Column", - "elevator": "Elevator", - "slab": "Slab", - "ceiling": "Ceiling", - "zone": "Zone", - "spawnPoint": "Spawn Point", - "shelf": "Shelf", - "duct": "Duct", - "ductFitting": "Duct Fitting", - "register": "Register", - "hvacUnit": "HVAC Unit", - "dwvPipe": "DWV Pipe", - "trap": "Trap", - "pipeFitting": "Pipe Fitting", - "lineset": "Lineset", - "liquidLine": "Liquid Line" - }, - "furnishTools": { - "furniture": "Furniture", - "appliance": "Appliance", - "kitchen": "Kitchen", - "bathroom": "Bathroom", - "outdoor": "Outdoor" - }, - "itemSources": { - "library": "Library", - "community": "Community", - "mine": "Mine" - }, - "shortcuts": { - "editorNavigation": "Editor Navigation", - "modesAndHistory": "Modes & History", - "selection": "Selection", - "drawingTools": "Drawing Tools", - "itemPlacement": "Item Placement", - "camera": "Camera", - "cancelTool": "Cancel the active tool and return to Select mode", - "deleteSelected": "Delete selected objects", - "undo": "Undo", - "redo": "Redo", - "addToSelection": "Add or remove an object from multi-selection", - "rotateClockwise": "Rotate item clockwise, or toggle selected door open/closed", - "rotateCounterClockwise": "Rotate item counter-clockwise, or close selected door", - "selectNextLevel": "Select next level in the active building", - "selectPreviousLevel": "Select previous level in the active building" - }, - "panel": { - "selection": "Selection", - "scaleAndOpacity": "Scale & Opacity", - "editScale": "Edit Scale", - "setScale": "Set Scale", - "hideScale": "Hide Scale", - "showScale": "Show Scale", - "duplicateLevel": "Duplicate level", - "duplicateLevelWithOptions": "Duplicate level with options", - "deleteLevel": "Delete level", - "groundLevelCannotBeDeleted": "The ground level cannot be deleted", - "deleteGuideImage": "Delete guide image", - "deleteScan": "Delete scan", - "showReferenceFloor": "Show reference floor", - "hideReferenceFloor": "Hide reference floor", - "addLevelAbove": "Add level above", - "addLevelBelow": "Add level below", - "noEdgeLines": "No edge lines", - "off": "Off", - "cutOut": "Cut Out", - "editProperties": "Edit properties", - "closeCaptureMode": "Close capture mode", - "close": "Close", - "move": "Move", - "rename": "Rename", - "intensity": "Intensity" - }, - "dialog": { - "editorNavigation": "Editor Navigation", - "modesAndHistory": "Modes & History", - "selection": "Selection", - "drawingTools": "Drawing Tools", - "itemPlacement": "Item Placement", - "camera": "Camera" - }, - "levelDuplicate": { - "everything": "Everything", - "structureOnly": "Structure only", - "structurePlusMaterials": "Structure + materials", - "structurePlusFurniture": "Structure + furniture" - }, - "stats": { - "sites": "Sites", - "buildings": "Buildings", - "levels": "Levels", - "walls": "Walls", - "doors": "Doors", - "windows": "Windows", - "items": "Items", - "slabs": "Slabs", - "ceilings": "Ceilings", - "zones": "Zones", - "scans": "Scans" - }, - "camera": { - "leftClick": "Left click", - "middleClick": "Middle click", - "rightClick": "Right click", - "scrollWheel": "Scroll wheel", - "space": "Space" - }, - "viewer": { - "building": "Building", - "item": "Item", - "orbitLeft": "Orbit Left", - "orbitRight": "Orbit Right", - "topView": "Top View", - "fullHeight": "Full Height", - "cutaway": "Cutaway", - "low": "Low" - } -} diff --git a/apps/editor/messages/zh.json b/apps/editor/messages/zh.json deleted file mode 100644 index c494e77d5f..0000000000 --- a/apps/editor/messages/zh.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "common": { - "save": "保存", - "delete": "删除", - "cancel": "取消", - "confirm": "确认", - "close": "关闭", - "settings": "设置", - "home": "首页", - "loading": "加载中...", - "error": "错误", - "success": "成功", - "search": "搜索", - "noResults": "未找到结果", - "untitled": "未命名", - "edit": "编辑", - "move": "移动", - "duplicate": "复制", - "copy": "复制", - "paste": "粘贴", - "cut": "剪切", - "undo": "撤销", - "redo": "重做", - "rotate": "旋转", - "select": "选择", - "deselect": "取消选择", - "show": "显示", - "hide": "隐藏", - "open": "打开", - "add": "添加", - "remove": "移除", - "reset": "重置", - "apply": "应用", - "done": "完成", - "back": "返回", - "next": "下一步", - "yes": "是", - "no": "否", - "creating": "创建中…", - "saving": "保存中…", - "saved": "已保存", - "save": "保存", - "saveAs": "另存为…", - "noSceneToSave": "没有可保存的场景", - "failedToCreateScene": "创建场景失败", - "saveFailed": "保存失败", - "saveAsFailed": "另存失败", - "conflictReload": "冲突 — 请重新加载后继续", - "newSceneName": "新场景名称", - "untitledScene": "未命名场景", - "createNewScene": "创建新场景", - "scene.sessionConflict": "其他会话已先保存 — 是否刷新?", - "scene.sessionConflictDesc": "您的更改尚未保存。请重新加载以获取最新版本。", - "scene.reload": "重新加载", - "scene.dismiss": "关闭", - "scene.allScenes": "所有场景", - "scene.scene": "场景", - "scene.connectionClosed": "实时场景连接已关闭", - "scene.saveFailed": "保存失败" - }, - "nav": { - "home": "首页", - "scenes": "场景", - "privacy": "隐私", - "terms": "条款" - }, - "sidebar": { - "scene": "场景", - "build": "构建", - "items": "物品", - "settings": "设置", - "site": "场地", - "structure": "结构", - "furnish": "家具", - "view": "视图", - "paint": "绘制" - }, - "editor": { - "localWarning": "本地编辑器 — 场景不会保存。", - "openSavedScenes": "打开已保存的场景", - "openRecent": "打开最近场景", - "createNew": "创建新场景", - "placeBuilding": "放置建筑", - "rotate": "旋转", - "chooseRoom": "选择房间", - "selectCurvedWall": "选择弧形墙", - "setDiameter": "设置直径", - "setCorner": "设置角点", - "rotateRoofDirection": "旋转屋顶方向 90°", - "place": "放置", - "forcePlace": "强制放置", - "guidedConstraintsBypassed": "已绕过引导约束" - }, - "scenes": { - "title": "您的场景", - "noScenes": "还没有场景。创建一个开始吧。", - "noScenesSaved": "您还没有保存任何场景。", - "sceneCount": "{count} 个场景", - "noThumbnail": "无缩略图", - "nodes": "{count} 个节点", - "delete": "删除", - "deleteConfirm": "确定要删除这个场景吗?", - "createScene": "创建场景", - "allScenes": "所有场景" - }, - "toolbar": { - "cutaway": "剖面", - "low": "低", - "up": "高", - "fullHeight": "全高" - }, - "errors": { - "loadFailed": "加载失败", - "saveFailed": "保存失败", - "deleteFailed": "删除失败", - "networkError": "网络错误,请重试。", - "conflict": "保存失败", - "anotherSessionSaved": "另一个会话已保存 — 刷新?", - "noActiveProject": "没有活动项目。请先打开一个项目。" - }, - "privacy": { - "title": "隐私政策", - "lastUpdated": "最后更新" - }, - "terms": { - "title": "服务条款", - "lastUpdated": "最后更新" - }, - "nodeTypes": { - "site": "场地", - "building": "建筑", - "level": "楼层", - "wall": "墙", - "door": "门", - "window": "窗", - "slab": "楼板", - "ceiling": "天花板", - "roof": "屋顶", - "zone": "区域", - "item": "物品", - "fence": "围栏", - "column": "柱子", - "elevator": "电梯", - "stair": "楼梯", - "stairs": "楼梯", - "gableRoof": "人字屋顶", - "spawn": "出生点", - "shelf": "搁架", - "scan": "扫描", - "guide": "参考图", - "chimney": "烟囱", - "skyLight": "天窗" - }, - "commands": { - "wallTool": "墙体工具", - "slabTool": "楼板工具", - "ceilingTool": "天花板工具", - "doorTool": "门工具", - "windowTool": "窗工具", - "itemTool": "物品工具", - "stairTool": "楼梯工具", - "zoneTool": "区域工具", - "deleteSelection": "删除选中", - "materialPaint": "材质绘制", - "goToLevel": "跳转楼层", - "addLevel": "添加楼层", - "renameLevel": "重命名楼层", - "deleteLevel": "删除楼层", - "wallMode": "墙体模式", - "levelMode": "楼层模式", - "switchToSolid": "切换到实体", - "switchToRendered": "切换到渲染", - "takeSnapshot": "快照", - "enterPreview": "进入预览", - "exitPreview": "退出预览", - "toggleFullscreen": "切换全屏", - "exportSceneJson": "导出场景 (JSON)", - "export3DModel": "导出 3D 模型 (GLB)", - "copyShareLink": "复制分享链接", - "takeScreenshot": "截图" - }, - "controlModes": { - "select": "选择", - "boxSelect": "框选", - "editSite": "编辑场地", - "build": "构建", - "materialPaint": "材质绘制", - "furnish": "家具", - "zone": "区域", - "exitSiteEditing": "退出场地编辑", - "siteEditGroundOnly": "场地编辑仅在地面层可用" - }, - "structureTools": { - "wall": "墙", - "door": "门", - "window": "窗", - "stairs": "楼梯", - "gableRoof": "人字屋顶", - "fence": "围栏", - "column": "柱子", - "elevator": "电梯", - "slab": "楼板", - "ceiling": "天花板", - "zone": "区域", - "spawnPoint": "出生点", - "shelf": "搁架", - "duct": "风管", - "ductFitting": "风管配件", - "register": "出风口", - "hvacUnit": "暖通空调机组", - "dwvPipe": "DWV 管", - "trap": "存水弯", - "pipeFitting": "管件", - "lineset": "冷媒管", - "liquidLine": "液管" - }, - "furnishTools": { - "furniture": "家具", - "appliance": "电器", - "kitchen": "厨房", - "bathroom": "浴室", - "outdoor": "户外" - }, - "itemSources": { - "library": "库", - "community": "社区", - "mine": "我的" - }, - "shortcuts": { - "editorNavigation": "编辑器导航", - "modesAndHistory": "模式与历史", - "selection": "选择", - "drawingTools": "绘图工具", - "itemPlacement": "物品放置", - "camera": "相机", - "cancelTool": "取消当前工具并返回选择模式", - "deleteSelected": "删除选中对象", - "undo": "撤销", - "redo": "重做", - "addToSelection": "添加或移除多选中的对象", - "rotateClockwise": "顺时针旋转物品,或切换门的开/关", - "rotateCounterClockwise": "逆时针旋转物品,或关闭选中的门", - "selectNextLevel": "选择活动建筑中的下一个楼层", - "selectPreviousLevel": "选择活动建筑中的上一个楼层" - }, - "panel": { - "selection": "选择", - "scaleAndOpacity": "比例与不透明度", - "editScale": "编辑比例", - "setScale": "设置比例", - "hideScale": "隐藏比例", - "showScale": "显示比例", - "duplicateLevel": "复制楼层", - "duplicateLevelWithOptions": "带选项复制楼层", - "deleteLevel": "删除楼层", - "groundLevelCannotBeDeleted": "地面楼层无法删除", - "deleteGuideImage": "删除参考图", - "deleteScan": "删除扫描", - "showReferenceFloor": "显示参考地面", - "hideReferenceFloor": "隐藏参考地面", - "addLevelAbove": "上方添加楼层", - "addLevelBelow": "下方添加楼层", - "noEdgeLines": "无边缘线", - "off": "关闭", - "cutOut": "切除", - "editProperties": "编辑属性", - "closeCaptureMode": "关闭捕捉模式", - "close": "关闭", - "move": "移动", - "rename": "重命名", - "intensity": "强度" - }, - "dialog": { - "editorNavigation": "编辑器导航", - "modesAndHistory": "模式与历史", - "selection": "选择", - "drawingTools": "绘图工具", - "itemPlacement": "物品放置", - "camera": "相机" - }, - "levelDuplicate": { - "everything": "全部", - "structureOnly": "仅结构", - "structurePlusMaterials": "结构 + 材质", - "structurePlusFurniture": "结构 + 家具" - }, - "stats": { - "sites": "场地", - "buildings": "建筑", - "levels": "楼层", - "walls": "墙", - "doors": "门", - "windows": "窗", - "items": "物品", - "slabs": "楼板", - "ceilings": "天花板", - "zones": "区域", - "scans": "扫描" - }, - "camera": { - "leftClick": "左键", - "middleClick": "中键", - "rightClick": "右键", - "scrollWheel": "滚轮", - "space": "空格" - }, - "viewer": { - "building": "建筑", - "item": "物品", - "orbitLeft": "向左旋转", - "orbitRight": "向右旋转", - "topView": "顶视图", - "fullHeight": "全高", - "cutaway": "剖面", - "low": "低" - } -} diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index 1c5b348824..9021aa390c 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -343,7 +343,7 @@ function GuidesControl() {
) : (
- {t('viewer.referencesEmptyText')} + {t('viewer.noGuideImagesOnLevel')}
)}
@@ -524,7 +524,7 @@ function ScansControl() {
) : (
- {t('viewer.referencesEmptyText')} + {t('viewer.noScansOnLevel')}
)}
@@ -753,7 +753,7 @@ function ReferencesControl() { Date: Mon, 31 Aug 2026 23:57:20 +0800 Subject: [PATCH 020/106] fix(i18n): localize remaining Bugbot findings (N/O/P/Q/R) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded English strings and locale-agnostic helpers with useTranslations() lookups so that the level selector, conflict banner, error boundary, node action menu, and scene overlay all render in the active locale. * page.tsx — Bug N: tighten 'localWarning' from 'Open saved scenes' to 'Scenes' so it reads as a sentence fragment followed by the link. * error-boundary.tsx — Bug O: 'Try again' button now reads from editor.tryAgain (was already in the catalog). * floating-level-selector.tsx — Bug P: drop the locale-agnostic getLevelDisplayName helper from @pascal-app/core; render Ground/Floor N/Basement N via level.groundFloor / level.floor / level.basement. * node-action-menu.tsx — Bug Q: surface nodeActions.editMesh, nodeActions.findInCatalog, nodeActions.group, nodeActions.ungroup (plus groupShortcut / ungroupShortcut tooltip variants) for the floating selection pill. * scene-loader.tsx — Bug R: localize the conflict banner title/body, Reload/Dismiss buttons, Light preview toggle (label + title), and the All scenes link in the editor's top-right toolbar. * en.json / zh.json — add 11 missing keys (editor.reload, editor.conflictTitle, editor.conflictBody, editor.lightPreview, editor.lightPreviewTitle, nodeActions.editMesh, nodeActions.findInCatalog, nodeActions.group, nodeActions.groupShortcut, nodeActions.ungroup, nodeActions.ungroupShortcut) and bring both catalogs back to parity (1210 keys each). * floating-level-selector.tsx — hoist localizedLevelName to module scope so LevelRow and FloatingLevelSelector share the same helper; t is passed explicitly so neither component reaches into the other's closure. Catalogs: en.json and zh.json each have 1210 keys after this commit. Refs Bugbot review on PR #740. --- apps/editor/components/scene-loader.tsx | 16 ++++++------- .../components/editor/node-action-menu.tsx | 16 ++++++------- .../components/ui/floating-level-selector.tsx | 24 +++++++++++++++---- .../ui/primitives/error-boundary.tsx | 2 +- packages/editor/src/lib/i18n/en.json | 13 +++++++++- packages/editor/src/lib/i18n/zh.json | 13 +++++++++- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index dbcb866829..317b21a754 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -249,24 +249,22 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
{conflict && (
-

Another session saved first — refresh?

-

- Your changes haven't been saved. Reload to pick up the latest version. -

+

{t('editor.conflictTitle')}

+

{t('editor.conflictBody')}

@@ -286,16 +284,16 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { onClick={() => router.push(lightPreview ? `/scene/${meta.id}` : `/scene/${meta.id}?disable=postFx`) } - title="Skip the post-processing pipeline — lighter on the GPU, no ambient occlusion or selection outlines" + title={t('editor.lightPreviewTitle')} type="button" > - Light preview + {t('editor.lightPreview')} - All scenes + {t('scenes.allScenes')}
{onFind && ( {/* Storey height badge — opens the height popover */} @@ -668,7 +682,7 @@ export function FloatingLevelSelector() { {t('level.deleteLevelTitle')} - {t('level.confirmDelete', { name: deletingLevel ? getLevelDisplayName(deletingLevel) : '' })} + {t('level.confirmDelete', { name: deletingLevel ? localizedLevelName(deletingLevel, t) : '' })} diff --git a/packages/editor/src/components/ui/primitives/error-boundary.tsx b/packages/editor/src/components/ui/primitives/error-boundary.tsx index d6572f4979..2a030bbb86 100644 --- a/packages/editor/src/components/ui/primitives/error-boundary.tsx +++ b/packages/editor/src/components/ui/primitives/error-boundary.tsx @@ -62,7 +62,7 @@ function ErrorBoundaryFallback({ className="mt-4 rounded bg-blue-600 px-4 py-2 hover:bg-blue-700" onClick={onReset} > - Try again + {t('editor.tryAgain')}
) diff --git a/packages/editor/src/lib/i18n/en.json b/packages/editor/src/lib/i18n/en.json index fef2d556ae..11ab4904dc 100644 --- a/packages/editor/src/lib/i18n/en.json +++ b/packages/editor/src/lib/i18n/en.json @@ -160,7 +160,7 @@ "editor.setCorner": "Set corner", "editor.forcePlace": "Force place", "editor.guidedConstraintsBypassed": "Guided constraints bypassed", - "editor.openSavedScenes": "Open saved scenes", + "editor.openSavedScenes": "Scenes", "editor.place": "Place", "editor.placeBuilding": "Place building", "editor.rotate": "Rotate", @@ -195,6 +195,11 @@ "editor.duplicateLevel": "Duplicate level", "editor.tryAgain": "Try again", "editor.dismiss": "Dismiss", + "editor.reload": "Reload", + "editor.conflictTitle": "Another session saved first — refresh?", + "editor.conflictBody": "Your changes haven't been saved. Reload to pick up the latest version.", + "editor.lightPreview": "Light preview", + "editor.lightPreviewTitle": "Skip the post-processing pipeline — lighter on the GPU, no ambient occlusion or selection outlines", "editor.leftClick": "Left click", "editor.localWarning": "This is a blank canvas — your saved scenes live under", "editor.middleClick": "Middle click", @@ -283,7 +288,13 @@ "nodeActions.cutOut": "Cut Out", "nodeActions.delete": "Delete", "nodeActions.duplicate": "Duplicate", + "nodeActions.editMesh": "Edit mesh", + "nodeActions.findInCatalog": "Find in catalog", + "nodeActions.group": "Group selection", + "nodeActions.groupShortcut": "Group (Ctrl/Cmd+G)", "nodeActions.move": "Move", + "nodeActions.ungroup": "Ungroup selection", + "nodeActions.ungroupShortcut": "Ungroup (Ctrl/Cmd+Shift+G)", "nodeTypes.boxVent": "Box Vent", "nodeTypes.building": "Building", "nodeTypes.ceiling": "Ceiling", diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json index a604c8a461..a47fa9f5c5 100644 --- a/packages/editor/src/lib/i18n/zh.json +++ b/packages/editor/src/lib/i18n/zh.json @@ -160,7 +160,7 @@ "editor.setCorner": "设置角落", "editor.forcePlace": "强制放置", "editor.guidedConstraintsBypassed": "已绕过引导约束", - "editor.openSavedScenes": "打开已保存的场景", + "editor.openSavedScenes": "场景页", "editor.place": "放置", "editor.placeBuilding": "放置建筑", "editor.rotate": "旋转", @@ -195,6 +195,11 @@ "editor.duplicateLevel": "复制楼层", "editor.tryAgain": "重试", "editor.dismiss": "关闭", + "editor.reload": "重新加载", + "editor.conflictTitle": "另一个会话已先保存 — 是否刷新?", + "editor.conflictBody": "您的更改尚未保存。请重新加载以获取最新版本。", + "editor.lightPreview": "轻量预览", + "editor.lightPreviewTitle": "跳过后期处理流程 — 减少 GPU 负载,无环境光遮蔽或选择高亮", "editor.leftClick": "左键点击", "editor.localWarning": "这里是空白画布 — 你保存的场景在", "editor.middleClick": "中键点击", @@ -283,7 +288,13 @@ "nodeActions.cutOut": "切除", "nodeActions.delete": "删除", "nodeActions.duplicate": "复制", + "nodeActions.editMesh": "编辑网格", + "nodeActions.findInCatalog": "在目录中查找", + "nodeActions.group": "编组所选", + "nodeActions.groupShortcut": "编组 (Ctrl/Cmd+G)", "nodeActions.move": "移动", + "nodeActions.ungroup": "取消编组", + "nodeActions.ungroupShortcut": "取消编组 (Ctrl/Cmd+Shift+G)", "nodeTypes.boxVent": "箱式通风口", "nodeTypes.building": "建筑", "nodeTypes.ceiling": "天花板", From 3c273450c26ce771738f416e54e7f4061a781d9a Mon Sep 17 00:00:00 2001 From: leapar Date: Tue, 1 Sep 2026 00:10:26 +0800 Subject: [PATCH 021/106] chore: retrigger Bugbot review From 0b1dc2b9ba1f6c9352498e100a93cab05875bb8c Mon Sep 17 00:00:00 2001 From: leapar Date: Tue, 1 Sep 2026 00:24:35 +0800 Subject: [PATCH 022/106] =?UTF-8?q?fix(i18n):=20address=20Bugbot=20review?= =?UTF-8?q?=20pass=206=20=E2=80=94=20S,=20T,=20U?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * registered-tool-helper.tsx — Bug S: route hint.label through t() so registry-driven kinds that store catalog keys (e.g. box-vent) render in the active locale. A literal label still passes through unchanged when t() can't resolve the key (i.e. when t(key) === key), so the existing English defaults are preserved for kinds that haven't been migrated. * level-display.ts — new shared helper extracted from floating-level-selector.tsx. Resolves a level's display name in the active locale via level.groundFloor / level.floor / level.basement, matching the in-component helper so all call sites agree. * command-palette/index.tsx — Bug T: replace getLevelDisplayName(level) with localizedLevelName(level, t) on the Go to Level sub-page. * view-toggles.tsx — Bug T: same swap for the reference-floor control (selectedLevelName and the lowerLevels loop). Also drop the now-unused getLevelDisplayName import. * floating-level-selector.tsx — import the shared helper instead of keeping a module-local copy; call sites (drag handle aria-label, row title/label, delete-confirmation dialog) all pass t() explicitly. * viewer-toolbar.tsx — Bug U: levelModeLabels.manual was pointing at viewer.stack, so manual mode rendered as 'Stack' / '堆叠'. Map it to the existing viewer.manual / viewer.manual key instead (already in both catalogs). No catalog changes required: viewer.manual, viewer.stack, viewer.exploded, and viewer.solo were already present in both en.json and zh.json, and the remaining labels are literal English sentences (not keys), so they fall through the existing useTranslations() lookup unchanged. Refs Bugbot review on PR #740. --- apps/editor/components/viewer-toolbar.tsx | 2 +- .../ui/action-menu/view-toggles.tsx | 6 ++-- .../components/ui/command-palette/index.tsx | 4 +-- .../components/ui/floating-level-selector.tsx | 15 +--------- .../ui/helpers/registered-tool-helper.tsx | 4 ++- packages/editor/src/lib/level-display.ts | 29 +++++++++++++++++++ 6 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 packages/editor/src/lib/level-display.ts diff --git a/apps/editor/components/viewer-toolbar.tsx b/apps/editor/components/viewer-toolbar.tsx index f043efbcf4..8f70d1be7e 100644 --- a/apps/editor/components/viewer-toolbar.tsx +++ b/apps/editor/components/viewer-toolbar.tsx @@ -121,7 +121,7 @@ const VIEW_MODES: { id: ViewMode; labelKey: string; icon: React.ReactNode }[] = const levelModeOrder = ['stacked', 'exploded', 'solo'] as const const levelModeLabels = { - manual: 'viewer.stack', + manual: 'viewer.manual', stacked: 'viewer.stack', exploded: 'viewer.exploded', solo: 'viewer.solo', diff --git a/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/packages/editor/src/components/ui/action-menu/view-toggles.tsx index 9021aa390c..7641455bb5 100644 --- a/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -12,7 +12,7 @@ import { useViewer } from '@pascal-app/viewer' import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2, Waypoints } from 'lucide-react' import { useCallback, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' -import { getLevelDisplayName } from '@pascal-app/core' +import { localizedLevelName } from '../../../lib/level-display' import { createLocalGuideImage } from '../../../lib/local-guide-image' import { useTranslations } from '../../../lib/i18n' import { cn } from '../../../lib/utils' @@ -793,7 +793,7 @@ function ReferenceFloorControl() { const lowerLevels = useLowerReferenceLevels() const hasLowerLevels = lowerLevels.length > 0 const selectedLevel = lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null - const selectedLevelName = selectedLevel ? getLevelDisplayName(selectedLevel) : null + const selectedLevelName = selectedLevel ? localizedLevelName(selectedLevel, t) : null return ( @@ -884,7 +884,7 @@ function ReferenceFloorControl() {
{lowerLevels.map((level, index) => { const isSelected = referenceFloorOffset === index + 1 - const levelName = getLevelDisplayName(level) + const levelName = localizedLevelName(level, t) return (
diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx index 82655bd110..128296bfa8 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx @@ -136,7 +136,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
- {t('loadBuild.errors', { count: errors.length })} + {t(errors.length === 1 ? 'loadBuild.errors' : 'loadBuild.errors_plural', { count: errors.length })}