-
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')}
- Export STL
+ {t('settings.exportedSTL')}
- Export OBJ
+ {t('settings.exportedOBJ')}
@@ -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')}
- Full floor plan
+ {t('settings.floorplanFull')}
- Structure only
+ {t('settings.floorplanStructure')}
@@ -513,7 +522,7 @@ export function SettingsPanel({
{/* Thumbnail Section (only for cloud projects) */}
{projectId && !isLocalProject && (
- Thumbnail
+ {t('settings.thumbnail')}
- {isGeneratingThumbnail ? 'Generating...' : 'Generate Thumbnail'}
+ {isGeneratingThumbnail ? t('settings.exporting') : t('settings.generateThumbnail')}
)}
{/* Save/Load Section */}
-
Save & Load
+
{t('settings.saveAndLoad')}
- Save Build
+ {t('settings.saveBuild')}
- Load Build
+ {t('settings.loadBuild')}
-
Audio
+
{t('settings.audio')}
{/* Keyboard Section */}
- Keyboard
+ {t('settings.keyboard')}
{/* Scene Graph */}
-
Scene Graph
+
{t('settings.sceneGraph')}
- Explore scene graph
+ {t('settings.exploreSceneGraph')}
- Scene Graph
+ {t('settings.sceneGraph')}
- Danger Zone
+ {t('settings.dangerZone')}
- Clear & Start New
+ {t('settings.clearAndStartNew')}
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')}
- 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..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
@@ -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(errors.length === 1 ? 'loadBuild.errors' : 'loadBuild.errors_plural', { 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(warnings.length === 1 ? 'loadBuild.warnings' : 'loadBuild.warnings_plural', { 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) {
- Cancel
+ {t('common.cancel')}
- Replace current scene
+ {t('loadBuild.replaceScene')}
diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx
index 764a50c128..78358dc644 100644
--- a/packages/editor/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx
+++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import { type AnyNodeId, type ChimneyNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import Image from 'next/image'
@@ -43,7 +45,7 @@ export const ChimneyTreeNode = memo(function ChimneyTreeNode({
[nodeId, setSelection],
)
- const defaultName = node?.name || 'Chimney'
+ const defaultName = node?.name || 'nodeTypes.chimney'
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 86f5a7beff..a7ec911190 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 3f4cae036a..e3bfa579bc 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
@@ -52,6 +52,7 @@ import {
} from './../../../../../lib/measurements'
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
+import { useTranslations } from './../../../../../lib/i18n'
import { cn } from './../../../../../lib/utils'
import useEditor from './../../../../../store/use-editor'
import { useUploadStore } from '../../../../../store/use-upload'
@@ -103,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)
@@ -171,7 +173,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
-
Property Line
+
{t('site.propertyLine')}
- Area:{' '}
+ {t('site.area')}:{' '}
{displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
- Perimeter:{' '}
+ {t('site.perimeter')}:{' '}
{displayPerimeter.toFixed(1)} {linearLabel}
@@ -209,7 +211,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
{points.map((point, index) => (
{index + 1}
-
X
+
{t('site.axisX')}
@@ -219,7 +221,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
type="number"
value={Number(toDisplayLinear(point[0]).toFixed(2))}
/>
-
Z
+
{t('site.axisZ')}
@@ -249,7 +251,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
onClick={handleAddPoint}
>
- Add point
+ {t('site.addPoint')}
)}
@@ -274,6 +276,7 @@ const CameraPopover = memo(function CameraPopover({
onOpenChange: (open: boolean) => void
buttonClassName?: string
}) {
+ const t = useTranslations()
const updateNode = useScene((state) => state.updateNode)
return (
@@ -284,7 +287,7 @@ const CameraPopover = memo(function CameraPopover({
buttonClassName,
)}
onClick={(e) => e.stopPropagation()}
- title="Camera snapshot"
+ title={t('site.cameraSnapshot')}
>
{hasCamera && (
@@ -309,7 +312,7 @@ const CameraPopover = memo(function CameraPopover({
}}
>
- View snapshot
+ {t('site.camera.viewSnapshot')}
)}
- {hasCamera ? 'Update snapshot' : 'Take snapshot'}
+ {hasCamera ? t('site.camera.updateSnapshot') : t('site.camera.takeSnapshot')}
{hasCamera && (
- Clear snapshot
+ {t('site.camera.clearSnapshot')}
)}
@@ -353,6 +356,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 [isExpanded, setIsExpanded] = useState(true)
const updateNode = useScene((state) => state.updateNode)
@@ -427,19 +431,19 @@ const ReferenceItem = memo(function ReferenceItem({
>
{isCapture ? (
) : (
)}
setIsEditing(true)}
@@ -454,7 +458,7 @@ const ReferenceItem = memo(function ReferenceItem({
event.stopPropagation()
updateNode(refNode.id, { visible: !isVisible })
}}
- title={isVisible ? 'Hide' : 'Show'}
+ title={isVisible ? t('site.hide') : t('site.show')}
type="button"
>
{isVisible ? : }
@@ -463,7 +467,7 @@ const ReferenceItem = memo(function ReferenceItem({
handleDelete(refNode.id, e)}
- title="Delete"
+ title={t('site.delete')}
type="button"
>
@@ -493,6 +497,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)
@@ -635,7 +640,9 @@ const LevelReferences = memo(function LevelReferences({
) : (
)}
- {uploading ? `Uploading ${uploadingType}... ${progress}%` : 'Upload scan/floorplan'}
+ {uploading
+ ? t('site.uploadingWithProgress', { type: uploadingType ?? '', progress })
+ : t('site.uploadScan')}
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)
@@ -807,7 +815,7 @@ const LevelItem = memo(function LevelItem({
setIsEditing(true)}
@@ -833,7 +841,7 @@ const LevelItem = memo(function LevelItem({
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
onClick={(e) => e.stopPropagation()}
- title="Camera snapshot"
+ title={t('site.cameraSnapshot')}
>
{level.camera && (
@@ -858,7 +866,7 @@ const LevelItem = memo(function LevelItem({
}}
>
- View snapshot
+ {t('site.camera.viewSnapshot')}
)}
- {level.camera ? 'Update snapshot' : 'Take snapshot'}
+ {level.camera ? t('site.camera.updateSnapshot') : t('site.camera.takeSnapshot')}
{level.camera && (
- Clear snapshot
+ {t('site.camera.clearSnapshot')}
)}
@@ -906,27 +914,27 @@ const LevelItem = memo(function LevelItem({
handleDuplicateLevel()}
- title="Duplicate level"
+ title={t('level.duplicateLevel')}
>
- Duplicate
+ {t('common.duplicate')}
setDuplicateDialogOpen(true)}
- title="Duplicate level with options"
+ title={t('site.duplicateLevelWithOptions')}
>
- Duplicate with options...
+ {t('level.duplicateOptions')}
deleteLevelWithFallbackSelection(level.id)}
- title={canDeleteLevel ? 'Delete level' : 'The ground level cannot be deleted'}
+ title={canDeleteLevel ? t('level.deleteLevel') : t('level.cannotDeleteGround')}
>
- Delete
+ {t('common.delete')}
@@ -943,7 +951,7 @@ const LevelItem = memo(function LevelItem({
updateNode(level.id, { baseElevation: value })}
precision={2}
step={0.05}
@@ -980,6 +988,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)
@@ -1029,7 +1038,7 @@ const LevelsSection = memo(function LevelsSection({
- Add level
+ {t('site.addLevel')}
{levels.length === 0 && (
@@ -1037,7 +1046,7 @@ const LevelsSection = memo(function LevelsSection({
{/* Horizontal branch line */}
- No levels yet
+ {t('site.noLevelsYet')}
)}
{[...levels].reverse().map((level, index) => (
@@ -1060,6 +1069,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)
@@ -1097,14 +1107,14 @@ const LayerToggle = memo(function LayerToggle() {
)}
- Structure
+ {t('site.structure')}
@@ -1133,14 +1143,14 @@ const LayerToggle = memo(function LayerToggle() {
)}
- Furnish
+ {t('site.furnish')}
@@ -1170,14 +1180,14 @@ const LayerToggle = memo(function LayerToggle() {
)}
- Zones
+ {t('site.zones')}
@@ -1190,6 +1200,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)
@@ -1286,7 +1297,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
e.stopPropagation()}
- title="Camera snapshot"
+ title={t('site.cameraSnapshot')}
>
{zone.camera && (
@@ -1311,7 +1322,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
}}
>
- View snapshot
+ {t('site.camera.viewSnapshot')}
)}
- {zone.camera ? 'Update snapshot' : 'Take snapshot'}
+ {zone.camera ? t('site.camera.updateSnapshot') : t('site.camera.takeSnapshot')}
{zone.camera && (
- Clear snapshot
+ {t('site.camera.clearSnapshot')}
)}
@@ -1353,6 +1364,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)
@@ -1361,11 +1373,11 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() {
return (
-
{selectedIds.length} objects selected
+
{t('site.objectsSelected', { count: selectedIds.length })}
setSelection({ selectedIds: [] })}
- title="Clear selection"
+ title={t('site.clearSelection')}
>
@@ -1375,6 +1387,7 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() {
})
const ContentSection = memo(function ContentSection() {
+ const t = useTranslations()
const selectedLevelId = useViewer((state) => state.selection.levelId)
const structureLayer = useEditor((state) => state.structureLayer)
const phase = useEditor((state) => state.phase)
@@ -1407,7 +1420,7 @@ const ContentSection = memo(function ContentSection() {
if (!level) {
return (
-
Select a level to view content
+
{t('level.selectLevelToView')}
)
}
@@ -1421,9 +1434,9 @@ const ContentSection = memo(function ContentSection() {
if (levelZones.length === 0) {
return (
- No zones on this level.{' '}
+ {t('level.noZonesOnLevel')}{' '}
- Add one
+ {t('common.addOne')}
)
@@ -1439,7 +1452,7 @@ const ContentSection = memo(function ContentSection() {
}
if (elementChildren.length === 0) {
- return
No elements on this level
+ return
{t('level.noElementsOnLevel')}
}
return (
@@ -1474,6 +1487,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)
@@ -1514,14 +1528,14 @@ const BuildingItem = memo(function BuildingItem({
>
-
{building.name || 'Building'}
+
{building.name || t('site.building')}
setBuildingCameraOpen(open ? building.id : null)}
@@ -1536,7 +1550,7 @@ const BuildingItem = memo(function BuildingItem({
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
onClick={(e) => e.stopPropagation()}
- title="Camera snapshot"
+ title={t('site.cameraSnapshot')}
>
{building.camera && (
@@ -1561,7 +1575,7 @@ const BuildingItem = memo(function BuildingItem({
}}
>
- View snapshot
+ {t('site.camera.viewSnapshot')}
)}
- {building.camera ? 'Update snapshot' : 'Take snapshot'}
+ {building.camera ? t('site.camera.updateSnapshot') : t('site.camera.takeSnapshot')}
{building.camera && (
- Clear snapshot
+ {t('site.camera.clearSnapshot')}
)}
@@ -1631,6 +1645,7 @@ export interface SitePanelProps {
}
export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanelProps = {}) {
+ const t = useTranslations()
const rootNodeIds = useScene((state) => state.rootNodeIds)
const updateNode = useScene((state) => state.updateNode)
const selectedBuildingId = useViewer((state) => state.selection.buildingId)
@@ -1670,14 +1685,14 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
>
-
{siteNode.name || 'Site'}
+
{siteNode.name || t('site.site')}
- No buildings yet
+ {t('site.noBuildingsYet')}
) : (
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 && (
e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
- placeholder={defaultName}
+ placeholder={resolvedDefault}
ref={inputRef}
size={inputSize}
type="text"
diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
index 168c39d7a6..5ce5b25610 100644
--- a/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
+++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
@@ -4,6 +4,7 @@ import { Layers } from 'lucide-react'
import { memo, useCallback, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { getDefaultLevelName } from '@pascal-app/core'
+import { useTranslations } from '../../../../../lib/i18n'
import { InlineRenameInput } from './inline-rename-input'
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
import { TreeNodeActions } from './tree-node-actions'
@@ -36,7 +37,8 @@ export const LevelTreeNode = memo(function LevelTreeNode({
const handleStartEditing = useCallback(() => setIsEditing(true), [])
const handleStopEditing = useCallback(() => setIsEditing(false), [])
- const defaultName = getDefaultLevelName(level)
+ const t = useTranslations()
+ const defaultName = getDefaultLevelName(level, t)
return (
)
const snapTarget = resolveNodeSnapTarget(node)
- const defaultName =
- node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
+ const t = useTranslations()
+ const catalogLabel = node ? t(`panel.nodeType.${camelType(node.type)}`) : ''
+ const presentationLabel =
+ node && !catalogLabel.startsWith('panel.nodeType.') ? catalogLabel : (presentation?.label ?? 'Node')
+ const defaultName = node
+ ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentationLabel || 'Node'
+ : 'Node'
const hasChildren = children.length > 0 || hasHostChildren
useEffect(() => {
diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx
index 02795c5830..b83433b213 100644
--- a/packages/editor/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx
+++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx
@@ -1,9 +1,12 @@
+'use client'
+
import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { AnimatePresence } from 'motion/react'
import Image from 'next/image'
import { memo, useCallback, useEffect, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
+import { messages, useLocale } from '../../../../../lib/i18n'
import useEditor from '../../../../../store/use-editor'
import { InlineRenameInput } from './inline-rename-input'
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
@@ -23,6 +26,8 @@ export const RoofTreeNode = memo(function RoofTreeNode({
}: RoofTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false)
const [expanded, setExpanded] = useState(false)
+ const { locale } = useLocale()
+ const t = (key: string) => (messages[locale] as Record)[key] || key
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
const isHovered = useViewer((state) => state.hoveredId === nodeId)
@@ -87,7 +92,8 @@ export const RoofTreeNode = memo(function RoofTreeNode({
}, [isDropTarget, expanded])
const segmentCount = segments.length
- const defaultName = `Roof (${segmentCount} segment${segmentCount !== 1 ? 's' : ''})`
+ const defaultName = 'nodeTypes.roofWithSegments'
+ const defaultNameParams = { count: segmentCount }
// Hide the dragged segment from every roof while dragging
const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
@@ -112,6 +118,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
label={
(messages[locale] as Record)[key] || key
const accessoryIds = useScene(
useShallow(
(s) => (s.nodes[node.id] as RoofSegmentNode | undefined)?.children ?? ([] as string[]),
@@ -218,7 +227,13 @@ function RoofSegmentTreeNode({
const handleStartEditing = useCallback(() => setIsEditing(true), [])
const handleStopEditing = useCallback(() => setIsEditing(false), [])
- const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
+ const defaultName = 'nodeTypes.roofSegmentWithDims'
+ const roofTypeKey = `nodeTypes.${node.roofType}` as keyof typeof messages.en
+ const defaultNameParams = {
+ type: (messages[locale] as Record)[roofTypeKey] || node.roofType,
+ width: node.width.toFixed(1),
+ depth: node.depth.toFixed(1),
+ }
const hasAccessories = accessoryIds.length > 0
@@ -246,6 +261,7 @@ function RoofSegmentTreeNode({
label={
setIsEditing(true)}
diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx
index 2f5114deb1..a4ba7f8b76 100644
--- a/packages/editor/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx
+++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx
@@ -1,9 +1,12 @@
+'use client'
+
import { type AnyNodeId, type StairNode, type StairSegmentNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { AnimatePresence } from 'motion/react'
import Image from 'next/image'
import { memo, useCallback, useEffect, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
+import { messages, useLocale } from '../../../../../lib/i18n'
import useEditor from '../../../../../store/use-editor'
import { InlineRenameInput } from './inline-rename-input'
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -23,6 +26,8 @@ export const StairTreeNode = memo(function StairTreeNode({
}: StairTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false)
const [expanded, setExpanded] = useState(false)
+ const { locale } = useLocale()
+ const t = (key: string) => (messages[locale] as Record)[key] || key
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
const isHovered = useViewer((state) => state.hoveredId === nodeId)
@@ -83,7 +88,8 @@ export const StairTreeNode = memo(function StairTreeNode({
}, [isDropTarget, expanded])
const segmentCount = segments.length
- const defaultName = `Staircase (${segmentCount} segment${segmentCount !== 1 ? 's' : ''})`
+ const defaultName = 'nodeTypes.staircaseWithSegments'
+ const defaultNameParams = { count: segmentCount }
// Hide the dragged segment from every stair while dragging
const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
@@ -108,6 +114,7 @@ export const StairTreeNode = memo(function StairTreeNode({
label={
state.selection.selectedIds.includes(node.id))
const isHovered = useViewer((state) => state.hoveredId === node.id)
const setSelection = useViewer((state) => state.setSelection)
@@ -191,8 +199,13 @@ function StairSegmentTreeNode({
const handleStartEditing = useCallback(() => setIsEditing(true), [])
const handleStopEditing = useCallback(() => setIsEditing(false), [])
- const typeLabel = node.segmentType === 'stair' ? 'Flight' : 'Landing'
- const defaultName = `${typeLabel} (${node.width.toFixed(1)}×${node.length.toFixed(1)}m)`
+ const typeLabel = node.segmentType === 'stair' ? 'nodeTypes.flight' : 'nodeTypes.landing'
+ const defaultName = 'nodeTypes.stairSegmentWithDims'
+ const defaultNameParams = {
+ type: (messages[locale] as Record)[typeLabel] || typeLabel,
+ width: node.width.toFixed(1),
+ depth: node.length.toFixed(1),
+ }
return (
@@ -218,6 +231,7 @@ function StairSegmentTreeNode({
label={
(messages[locale] as Record)[key] || key
+
const [open, setOpen] = useState(false)
const updateNode = useScene((state) => state.updateNode)
const updateNodes = useScene((state) => state.updateNodes)
@@ -57,7 +63,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
{isVisible ? : }
@@ -67,7 +73,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
e.stopPropagation()}
- title="Camera snapshot"
+ title={t('treeActions.cameraSnapshot')}
>
{hasCamera && (
@@ -88,7 +94,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
onClick={handleViewCamera}
>
- View snapshot
+ {t('treeActions.viewSnapshot')}
)}
- {hasCamera ? 'Update snapshot' : 'Take snapshot'}
+ {hasCamera ? t('treeActions.updateSnapshot') : t('treeActions.takeSnapshot')}
{hasCamera && (
- Clear snapshot
+ {t('treeActions.clearSnapshot')}
)}
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 2cdbe70ec4..f4b80d6d06 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={
state.deleteNode)
const updateNode = useScene((state) => state.updateNode)
@@ -67,7 +69,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
e.stopPropagation()}
- title="Camera snapshot"
+ title={t('editor.cameraSnapshot')}
>
{zone.camera && (
@@ -92,7 +94,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
}}
>
- View snapshot
+ {t('editor.viewSnapshot')}
)}
- {zone.camera ? 'Update snapshot' : 'Take snapshot'}
+ {zone.camera ? t('editor.updateSnapshot') : t('editor.takeSnapshot')}
{zone.camera && (
- Clear snapshot
+ {t('editor.clearSnapshot')}
)}
@@ -133,6 +135,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
}
export function ZonePanel() {
+ const t = useTranslations()
const nodes = useScene((state) => state.nodes)
const currentLevelId = useViewer((state) => state.selection.levelId)
const selectedZoneId = useViewer((state) => state.selection.zoneId)
@@ -169,7 +172,7 @@ export function ZonePanel() {
if (!currentLevelId) {
return (
- Select a level to view and create zones
+ {t('editor.selectLevelFirst')}
)
}
@@ -178,34 +181,34 @@ export function ZonePanel() {
{levelZones.length === 0 ? (
- No zones on this level.{' '}
+ {t('editor.noZonesOnLevel')}{' '}
- Add one
+ {t('editor.addOne')}
) : (
levelZones.map((zone) =>
)
)}
{selectedZone ? (
-
+
}
- label="Save to catalog"
+ label={t('editor.saveToCatalog')}
onClick={() => emitter.emit('room-preset:create', { zoneId: selectedZone.id })}
type="button"
/>
}
- label="Delete"
+ label={t('editor.delete')}
onClick={() => deleteSelectedZone(false)}
type="button"
/>
}
- label="Delete with contents"
+ label={t('editor.deleteWithContents')}
onClick={() => deleteSelectedZone(true)}
type="button"
/>
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/floorplan-compass-button.tsx b/packages/editor/src/components/viewer/floorplan-compass-button.tsx
index eef109049d..12b6de0a8b 100644
--- a/packages/editor/src/components/viewer/floorplan-compass-button.tsx
+++ b/packages/editor/src/components/viewer/floorplan-compass-button.tsx
@@ -1,6 +1,7 @@
'use client'
import type React from 'react'
+import { useTranslations } from '../../lib/i18n'
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
export type FloorplanCompassButtonProps = {
@@ -14,11 +15,12 @@ export function FloorplanCompassButton({
onAlignNorth,
needleRef,
}: FloorplanCompassButtonProps) {
+ const t = useTranslations()
return (
{
event.preventDefault()
@@ -44,7 +46,7 @@ export function FloorplanCompassButton({
- Align view to north
+ {t('editor.alignViewNorth')}
)
}
diff --git a/packages/editor/src/components/viewer/floorplan-preview.tsx b/packages/editor/src/components/viewer/floorplan-preview.tsx
index fc2d98e425..deec78450f 100644
--- a/packages/editor/src/components/viewer/floorplan-preview.tsx
+++ b/packages/editor/src/components/viewer/floorplan-preview.tsx
@@ -6,10 +6,12 @@ import {
type FloorplanGeometry,
type FloorplanPalette,
type GeometryContext,
+ getDefaultLevelName,
isNodeKindEnabled,
nodeRegistry,
useScene,
} from '@pascal-app/core'
+import type { Translator } from '@pascal-app/core'
import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema'
import { useViewer } from '@pascal-app/viewer'
import { Maximize2, Minus, Plus } from 'lucide-react'
@@ -33,6 +35,7 @@ import {
} from '../../lib/floorplan'
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
import { buildFloorplanContext, floorplanLayerRank } from '../../lib/floorplan/floorplan-readonly'
+import { useTranslations } from '../../lib/i18n'
import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
import useEditor, { type NavigationSyncPose } from '../../store/use-editor'
import {
@@ -141,13 +144,10 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox {
}
}
-function levelLabel(level: AnyNode): string {
+function levelLabel(level: AnyNode, t: Translator): string {
const named = (level as { name?: string }).name?.trim()
if (named) return named
- const ordinal = (level as { level?: number }).level ?? 0
- if (ordinal === 0) return 'Ground floor'
- if (ordinal < 0) return `Basement ${Math.abs(ordinal)}`
- return `Level ${ordinal}`
+ return getDefaultLevelName((level as { level?: number }).level ?? 0, t)
}
function collectLevelTree(root: AnyNode, nodes: Record): AnyNode[] {
@@ -321,6 +321,7 @@ export function FloorplanPreview({
showLevelSelector = true,
synchronizeNavigation = false,
}: FloorplanPreviewProps) {
+ const t = useTranslations()
const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes))
const storeInstalledPlugins = useScene((state) =>
scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins,
@@ -894,7 +895,7 @@ export function FloorplanPreview({
{synchronizeNavigation ? : null}
- Floor
+ {t('editor.floor')}
chooseLevel(event.target.value)}
style={{
border: '1px solid rgba(148,163,184,.55)',
@@ -997,7 +998,7 @@ export function FloorplanPreview({
>
{levels.map((level) => (
- {levelLabel(level)}
+ {levelLabel(level, t)}
))}
@@ -1025,28 +1026,28 @@ export function FloorplanPreview({
}}
>
zoom(1.2)}
style={controlStyle}
- title="Zoom out"
+ title={t('editor.zoomOut')}
type="button"
>
updateLocalViewBox(fittedViewBox)}
style={controlStyle}
- title="Fit floor plan"
+ title={t('editor.fitFloorPlan')}
type="button"
>
zoom(0.8)}
style={controlStyle}
- title="Zoom in"
+ title={t('editor.zoomIn')}
type="button"
>
diff --git a/packages/editor/src/components/viewer/viewer-controls-bar.tsx b/packages/editor/src/components/viewer/viewer-controls-bar.tsx
index 929dbd4a99..2be85b7905 100644
--- a/packages/editor/src/components/viewer/viewer-controls-bar.tsx
+++ b/packages/editor/src/components/viewer/viewer-controls-bar.tsx
@@ -26,6 +26,7 @@ import {
Square,
SwatchBook,
} from 'lucide-react'
+import { useTranslations } from '../../lib/i18n'
import { cn } from '../../lib/utils'
import { ActionButton } from '../ui/action-menu/action-button'
import {
@@ -40,43 +41,11 @@ import {
} from '../ui/primitives/dropdown-menu'
import { TooltipProvider } from '../ui/primitives/tooltip'
-const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
- stacked: 'Stacked',
- exploded: 'Exploded',
- solo: 'Solo',
-}
-
-const wallModeConfig = {
- up: {
- icon: (props: any) => (
-
- ),
- label: 'Full height',
- },
- cutaway: {
- icon: (props: any) => (
-
- ),
- label: 'Cutaway',
- },
- down: {
- icon: (props: any) => (
-
- ),
- label: '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 },
-] as const
+type LevelModeKey = 'stacked' | 'exploded' | 'solo'
+type WallModeKey = 'up' | 'cutaway' | 'down'
-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 }[]
+const LEVEL_MODE_KEYS: ReadonlyArray = ['stacked', 'exploded', 'solo']
+const WALL_MODE_KEYS: ReadonlyArray = ['cutaway', 'up', 'down']
// Keep the dropdown open when flipping an in-place toggle row.
const keepOpen = (event: Event, fn: () => void) => {
@@ -94,6 +63,7 @@ function VisibilityMenu({
canShowScans: boolean
canShowGuides: boolean
}) {
+ const t = useTranslations()
const showScans = useViewer((s) => s.showScans)
const showGuides = useViewer((s) => s.showGuides)
return (
@@ -101,7 +71,7 @@ function VisibilityMenu({
keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
>
- Scans
+ {t('editor.scans')}
{showScans ? (
) : (
@@ -128,7 +98,7 @@ function VisibilityMenu({
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowGuides(!showGuides))}
>
- Guides
+ {t('editor.guides')}
{showGuides ? (
) : (
@@ -141,24 +111,47 @@ function VisibilityMenu({
)
}
+function wallModeLabel(key: WallModeKey, t: ReturnType): string {
+ if (key === 'up') return t('editor.fullHeight')
+ if (key === 'down') return t('editor.low')
+ return t('editor.cutaway')
+}
+
+function wallModeAlt(key: WallModeKey, t: ReturnType): string {
+ return wallModeLabel(key, t)
+}
+
+function wallModeIconSrc(key: WallModeKey): string {
+ if (key === 'up') return '/icons/room.webp'
+ if (key === 'down') return '/icons/walllow.webp'
+ return '/icons/wallcut.webp'
+}
+
+function levelModeLabel(key: LevelModeKey, t: ReturnType): string {
+ if (key === 'stacked') return t('editor.stacked')
+ if (key === 'exploded') return t('editor.exploded')
+ return t('editor.solo')
+}
+
// One "Display" button gathering shadows, camera projection, colors, render
// mode, scene theme and edges.
function DisplayMenu() {
+ const t = useTranslations()
const cameraMode = useViewer((s) => s.cameraMode)
const shading = useViewer((s) => s.shading)
const textures = useViewer((s) => s.textures)
const shadows = useViewer((s) => s.shadows)
const sceneTheme = useViewer((s) => s.sceneTheme)
const edges = useViewer((s) => s.edges)
- const activeShading = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
- const activeTheme = getSceneTheme(sceneTheme)
- const activeEdges = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
+ const activeShadingName = shading === 'rendered' ? t('editor.rendered') : t('editor.solid')
+ const activeEdgesName =
+ edges === 'soft' ? t('editor.edgeSoft') : edges === 'strong' ? t('editor.edgeStrong') : t('editor.edgeOff')
return (
keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
>
- Shadows
- {shadows ? 'On' : 'Off'}
+ {t('editor.shadows')}
+
+ {shadows ? t('editor.on') : t('editor.off')}
+
@@ -184,18 +179,18 @@ function DisplayMenu() {
}
>
- Camera
+ {t('editor.camera')}
- {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
+ {cameraMode === 'perspective' ? t('editor.perspective') : t('editor.orthographic')}
keepOpen(e, () => useViewer.getState().setTextures(!textures))}
>
{textures ? : }
- Colors
+ {t('editor.colors')}
- {textures ? 'Colored' : 'Monochrome'}
+ {textures ? t('editor.colored') : t('editor.monochrome')}
@@ -203,43 +198,55 @@ function DisplayMenu() {
-
- Render
- {activeShading.name}
+ {shading === 'rendered' ? (
+
+ ) : (
+
+ )}
+ {t('editor.render')}
+ {activeShadingName}
- {SHADING_OPTIONS.map((option) => {
- const OptionIcon = option.icon
- return (
- useViewer.getState().setShading(option.id)}
- >
-
-
- {option.name}
- {option.detail}
-
- {shading === option.id ? (
-
- ) : null}
-
- )
- })}
+ useViewer.getState().setShading('solid')}
+ >
+
+
+ {t('editor.solid')}
+ {t('editor.solidDetail')}
+
+ {shading === 'solid' ? (
+
+ ) : null}
+
+ useViewer.getState().setShading('rendered')}
+ >
+
+
+ {t('editor.rendered')}
+
+ {t('editor.renderedDetail')}
+
+
+ {shading === 'rendered' ? (
+
+ ) : null}
+
- Theme
+ {t('editor.theme')}
{activeTheme.name}
{SCENE_THEMES.map((t) => {
- const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
+ const swatches = ((['wall', 'roof', 'floor', 'glazing'] as const)).map(
(role) => t.clayTints?.[role] ?? CLAY_PALETTE[role],
)
return (
@@ -267,22 +274,45 @@ function DisplayMenu() {
- Edges
- {activeEdges.name}
+ {t('editor.edges')}
+ {activeEdgesName}
- {EDGE_OPTIONS.map((option) => (
- useViewer.getState().setEdges(option.id)}
- >
-
- {option.name}
- {option.detail}
-
- {edges === option.id ? : null}
-
- ))}
+ useViewer.getState().setEdges('off')}
+ >
+
+ {t('editor.edgeOff')}
+ {t('editor.edgeOffDetail')}
+
+ {edges === 'off' ? (
+
+ ) : null}
+
+ useViewer.getState().setEdges('soft')}
+ >
+
+ {t('editor.edgeSoft')}
+ {t('editor.edgeSoftDetail')}
+
+ {edges === 'soft' ? (
+
+ ) : null}
+
+ useViewer.getState().setEdges('strong')}
+ >
+
+ {t('editor.edgeStrong')}
+
+ {t('editor.edgeStrongDetail')}
+
+
+ {edges === 'strong' ? (
+
+ ) : null}
+
@@ -316,14 +346,20 @@ export const ViewerControlsBar = ({
onWalkthroughToggle,
className,
}: ViewerControlsBarProps) => {
+ const t = useTranslations()
const levelMode = useViewer((s) => s.levelMode)
const wallMode = useViewer((s) => s.wallMode)
// Sessions may carry a stale mode outside the cycle (e.g. the retired
// 'translucent'); render and cycle it as cutaway instead of crashing.
- const safeWallMode = (
- wallMode in wallModeConfig ? wallMode : 'cutaway'
- ) as keyof typeof wallModeConfig
- const WallModeIcon = wallModeConfig[safeWallMode].icon
+ const safeWallMode = (WALL_MODE_KEYS.includes(wallMode as WallModeKey)
+ ? wallMode
+ : 'cutaway') as WallModeKey
+ const wallAlt = wallModeAlt(safeWallMode, t)
+ const wallSrc = wallModeIconSrc(safeWallMode)
+ const levelLabel =
+ levelMode === 'manual'
+ ? t('editor.manual')
+ : levelModeLabel(levelMode as LevelModeKey, t)
return (
{
if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
- const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
- const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
- useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
+ const nextIndex =
+ (LEVEL_MODE_KEYS.indexOf(levelMode as LevelModeKey) + 1) % LEVEL_MODE_KEYS.length
+ useViewer.getState().setLevelMode(LEVEL_MODE_KEYS[nextIndex] ?? 'stacked')
}}
size="icon"
tooltipSide="top"
@@ -376,17 +412,17 @@ export const ViewerControlsBar = ({
? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
: 'bg-white/10'
}
- label={`Walls: ${wallModeConfig[safeWallMode].label}`}
+ label={t('editor.wallsLabel', { mode: wallModeLabel(safeWallMode, t) })}
onClick={() => {
- const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
- const nextIndex = (modes.indexOf(safeWallMode) + 1) % modes.length
- useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
+ const nextIndex =
+ (WALL_MODE_KEYS.indexOf(safeWallMode) + 1) % WALL_MODE_KEYS.length
+ useViewer.getState().setWallMode(WALL_MODE_KEYS[nextIndex] ?? 'cutaway')
}}
size="icon"
tooltipSide="top"
variant="ghost"
>
-
+
)}
@@ -403,7 +439,7 @@ export const ViewerControlsBar = ({
? 'bg-emerald-500/20 text-emerald-400'
: 'hover:bg-white/5 hover:text-emerald-400'
}
- label={`Walkthrough: ${walkthroughActive ? 'On' : 'Off'}`}
+ label={`${t('editor.walkthrough')}: ${walkthroughActive ? t('editor.on') : t('editor.off')}`}
onClick={onWalkthroughToggle}
size="icon"
tooltipSide="top"
@@ -417,14 +453,14 @@ export const ViewerControlsBar = ({
{/* Camera actions */}
emitter.emit('camera-controls:orbit-ccw')}
size="icon"
tooltipSide="top"
variant="ghost"
>
@@ -432,14 +468,14 @@ export const ViewerControlsBar = ({
emitter.emit('camera-controls:orbit-cw')}
size="icon"
tooltipSide="top"
variant="ghost"
>
@@ -447,14 +483,14 @@ export const ViewerControlsBar = ({
emitter.emit('camera-controls:top-view')}
size="icon"
tooltipSide="top"
variant="ghost"
>
@@ -463,4 +499,4 @@ export const ViewerControlsBar = ({
)
-}
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/viewer/viewer-scene-header.tsx b/packages/editor/src/components/viewer/viewer-scene-header.tsx
index 1d44a9be00..b83a650762 100644
--- a/packages/editor/src/components/viewer/viewer-scene-header.tsx
+++ b/packages/editor/src/components/viewer/viewer-scene-header.tsx
@@ -14,17 +14,19 @@ import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
import Link from 'next/link'
import type { ReactNode } from 'react'
import { useShallow } from 'zustand/react/shallow'
+import { useTranslations } from '../../lib/i18n'
import { cn } from '../../lib/utils'
-const getNodeName = (node: AnyNode): string => {
+const getNodeName = (node: AnyNode, t: ReturnType): 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('site.nodeType.wall')
+ if (node.type === 'fence') return t('site.nodeType.fence')
+ if (node.type === 'item')
+ return (node as { asset: { name: string } }).asset?.name || t('site.nodeType.item')
+ if (node.type === 'slab') return t('site.nodeType.slab')
+ if (node.type === 'ceiling') return t('site.nodeType.ceiling')
+ if (node.type === 'roof') return t('site.nodeType.roof')
+ if (node.type === 'roof-segment') return t('site.nodeType.roofSegment')
return node.type
}
@@ -46,6 +48,7 @@ export const ViewerSceneHeader = ({
backHref = '/',
stats,
}: ViewerSceneHeaderProps) => {
+ const t = useTranslations()
const selection = useViewer((s) => s.selection)
// Subscribe only to the specific nodes we read so that creating an unrelated
@@ -100,7 +103,7 @@ export const ViewerSceneHeader = ({
{onBack ? (
) : (
- {projectName || 'Untitled'}
+ {projectName || t('site.untitled')}
{owner?.username && (
handleBreadcrumbClick('root')}
>
- Site
+ {t('site.site')}
@@ -152,7 +155,7 @@ export const ViewerSceneHeader = ({
className={`truncate transition-colors ${level ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
onClick={() => handleBreadcrumbClick('building')}
>
- {building.name || 'Building'}
+ {building.name || t('site.building')}
{level && (
@@ -162,7 +165,7 @@ export const ViewerSceneHeader = ({
className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
onClick={() => handleBreadcrumbClick('level')}
>
- {getLevelDisplayName(level)}
+ {getLevelDisplayName(level, t)}
>
)}
@@ -182,7 +185,7 @@ export const ViewerSceneHeader = ({
<>
- {getNodeName(selectedNode)}
+ {getNodeName(selectedNode, t)}
>
)}
@@ -195,7 +198,7 @@ export const ViewerSceneHeader = ({
{building && levels.length > 0 && (
- Levels
+ {t('site.levels')}
{levels.map((lvl) => {
@@ -221,7 +224,7 @@ export const ViewerSceneHeader = ({
- {getLevelDisplayName(lvl)}
+ {getLevelDisplayName(lvl, t)}
diff --git a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx
index ea137c6556..5d6705c909 100644
--- a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx
+++ b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx
@@ -2,6 +2,7 @@
import { Box, Columns2, Map as MapIcon } from 'lucide-react'
import type { ReactNode } from 'react'
+import { useTranslations } from '../../lib/i18n'
import { cn } from '../../lib/utils'
import { normalizeViewerStageModes, type ViewerStageMode } from './viewer-stage-modes'
@@ -22,11 +23,12 @@ export function ViewerStageSwitcher({
modes,
onChange,
}: ViewerStageSwitcherProps) {
+ const t = useTranslations()
const enabledModes = normalizeViewerStageModes(modes)
return (
}
- label="3D"
+ label={t('editor.viewer3d')}
onClick={() => onChange('3d')}
/>
) : null}
@@ -45,7 +47,7 @@ export function ViewerStageSwitcher({
}
- label="2D"
+ label={t('editor.viewer2d')}
onClick={() => onChange('2d')}
/>
) : null}
@@ -54,7 +56,7 @@ export function ViewerStageSwitcher({
active={mode === 'split'}
className={hideSplitOnMobile && enabledModes.length > 1 ? 'hidden md:flex' : undefined}
icon={
}
- label="Split"
+ label={t('editor.viewerSplit')}
onClick={() => onChange('split')}
/>
) : null}
diff --git a/packages/editor/src/hooks/use-grid-events.ts b/packages/editor/src/hooks/use-grid-events.ts
index a93533d076..d3d2cca9bf 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-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 ceebd342bd..0e26833af0 100644
--- a/packages/editor/src/index.tsx
+++ b/packages/editor/src/index.tsx
@@ -467,7 +467,7 @@ export {
buildRoofSurfaceMaterialPatch,
buildSingleSurfaceMaterialPatch,
buildStairSurfaceMaterialPatch,
- getActivePaintMaterialLabel,
+ getActivePaintMaterialLabelKey,
hasActivePaintMaterial,
} from './lib/material-paint'
export {
@@ -695,3 +695,12 @@ export {
type WallSnapKind,
type WallSnapPoint,
} from './store/use-wall-snap-indicator'
+export {
+ I18nProvider,
+ useLocale,
+ useTranslations,
+ defaultLocale,
+ type Locale,
+ type Translator,
+ messages,
+} from './lib/i18n'
diff --git a/packages/editor/src/lib/contextual-help.ts b/packages/editor/src/lib/contextual-help.ts
index cf0bf4efc4..ac6e553849 100644
--- a/packages/editor/src/lib/contextual-help.ts
+++ b/packages/editor/src/lib/contextual-help.ts
@@ -3,10 +3,22 @@ export type ContextualShortcutHint = {
// itself be an array of alternatives (rendered joined by "/"), e.g.
// [['Cmd/Ctrl', 'Shift'], 'Left click'] → "⌘ / ⇧ + click".
keys: Array
+ /**
+ * Pre-resolved label (English by convention). The contextual helper prefers
+ * `labelKey` when supplied and falls back to this verbatim — convenient for
+ * embedder-provided hints that already come pre-localised.
+ */
label: string
+ /**
+ * Optional i18n key for `label`. When set, the contextual helper resolves
+ * through the active locale at render time; otherwise `label` is used
+ * verbatim. Supply one of these (or both, with `label` as the fallback).
+ */
+ labelKey?: string
// Optional secondary line under the label for a terser qualifier
// (e.g. "disable 15° snap"). The HUD wraps both lines rather than truncating.
subtitle?: string
+ subtitleKey?: string
active?: boolean
}
@@ -37,6 +49,7 @@ export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualS
{
keys: [SHIFT_KEY],
label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
+ labelKey: shiftPressed ? 'contextualHelp.rotateHandle.active' : 'contextualHelp.rotateHandle.idle',
active: shiftPressed,
},
]
@@ -79,6 +92,7 @@ export function resolveSelectModeHelpHints({
hints.push({
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
label: 'Add or remove objects from the selection',
+ labelKey: 'contextualHelp.select.addOrRemove',
active: true,
})
return hints
@@ -91,22 +105,26 @@ export function resolveSelectModeHelpHints({
hints.push({
keys: [LEFT_CLICK],
label: 'Click or drag the selection to move it as one',
+ labelKey: 'contextualHelp.select.moveAsOne',
})
- hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°', labelKey: 'contextualHelp.select.rotateSelection' })
hints.push({
keys: [COMMAND_KEY, 'G'],
label: 'Group selection (session only)',
+ labelKey: 'contextualHelp.select.groupSelection',
})
hints.push({
keys: [COMMAND_KEY, SHIFT_KEY, 'G'],
label: 'Ungroup session selection',
+ labelKey: 'contextualHelp.select.ungroupSelection',
})
hints.push({
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
label: 'Add or remove objects from the selection',
+ labelKey: 'contextualHelp.select.addOrRemove',
active: commandPressed || shiftPressed,
})
- hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)' })
+ hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)', labelKey: 'contextualHelp.select.clearSelection' })
return hints
}
@@ -116,12 +134,28 @@ export function resolveSelectModeHelpHints({
// detaches the joint mid-drag; a fitting's cluster adds rotate arcs, with
// R / T (and Alt to switch axis) for keyboard rotation.
if (mepSelection === 'run') {
- hints.push({ keys: [CLICK], label: 'Click a handle dot to show move arrows' })
- hints.push({ keys: [ALT_KEY], label: 'Detach the joint while dragging an arrow' })
+ hints.push({
+ keys: [CLICK],
+ label: 'Click a handle dot to show move arrows',
+ labelKey: 'contextualHelp.mep.run.showArrows',
+ })
+ hints.push({
+ keys: [ALT_KEY],
+ label: 'Detach the joint while dragging an arrow',
+ labelKey: 'contextualHelp.mep.run.detachJoint',
+ })
} else if (mepSelection === 'fitting') {
- hints.push({ keys: [CLICK], label: 'Click the handle dot to show move + rotate handles' })
- hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°' })
- hints.push({ keys: [ALT_KEY], label: 'Switch the rotation axis (Y → X → Z)' })
+ hints.push({
+ keys: [CLICK],
+ label: 'Click the handle dot to show move + rotate handles',
+ labelKey: 'contextualHelp.mep.fitting.showHandles',
+ })
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°', labelKey: 'contextualHelp.mep.fitting.rotate45' })
+ hints.push({
+ keys: [ALT_KEY],
+ label: 'Switch the rotation axis (Y → X → Z)',
+ labelKey: 'contextualHelp.mep.fitting.switchAxis',
+ })
}
// The rows are the same whatever modifier is held — guides/snapping are
@@ -132,6 +166,7 @@ export function resolveSelectModeHelpHints({
hints.push({
keys: [LEFT_CLICK],
label: 'Drag selected movable object',
+ labelKey: 'contextualHelp.single.movableDrag',
})
}
@@ -139,12 +174,14 @@ export function resolveSelectModeHelpHints({
hints.push({
keys: [COMMAND_KEY, RIGHT_CLICK],
label: 'Drag left or right to rotate selected object',
+ labelKey: 'contextualHelp.single.rotatableDrag',
})
}
hints.push({
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
label: 'Add or remove objects from the selection',
+ labelKey: 'contextualHelp.select.addOrRemove',
active: commandPressed || shiftPressed,
})
diff --git a/packages/editor/src/lib/continuation.ts b/packages/editor/src/lib/continuation.ts
index 420aef590c..2b1a1578b7 100644
--- a/packages/editor/src/lib/continuation.ts
+++ b/packages/editor/src/lib/continuation.ts
@@ -6,6 +6,9 @@ export const CONTINUATION_PROFILES: Record<
{
options: ContinuationMode[]
default: ContinuationMode
+ // i18n keys — resolved by `useTranslations()` in the helper panel. Keeping
+ // the data i18n-key-based (rather than pre-localised strings) lets the
+ // active locale switch without re-rendering the parent.
labels: Record
icons: Record
}
@@ -13,16 +16,16 @@ export const CONTINUATION_PROFILES: Record<
wall: {
options: ['room', 'single'],
default: 'room',
- labels: { room: 'Room (auto-close)', single: 'Single wall' },
+ labels: { room: 'continuation.wall.room', single: 'continuation.wall.single' },
icons: { room: 'lucide:square', single: 'lucide:minus' },
},
fence: {
options: ['single', 'continuous', 'curved'],
default: 'continuous',
labels: {
- continuous: 'Continuous',
- single: 'Single fence',
- curved: 'Curved fence',
+ continuous: 'continuation.fence.continuous',
+ single: 'continuation.fence.single',
+ curved: 'continuation.fence.curved',
},
icons: {
continuous: 'lucide:waypoints',
@@ -33,19 +36,19 @@ export const CONTINUATION_PROFILES: Record<
point: {
options: ['once', 'repeat'],
default: 'once',
- labels: { once: 'Place once', repeat: 'Place multiple' },
+ labels: { once: 'continuation.point.once', repeat: 'continuation.point.repeat' },
icons: { once: 'lucide:target', repeat: 'lucide:copy-plus' },
},
cabinet: {
options: ['single', 'continuous'],
default: 'single',
- labels: { single: 'Single cabinet', continuous: 'Continuous run' },
+ labels: { single: 'continuation.cabinet.single', continuous: 'continuation.cabinet.continuous' },
icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
},
canopy: {
options: ['single', 'continuous'],
default: 'single',
- labels: { single: 'Single canopy', continuous: 'Continuous canopy' },
+ labels: { single: 'continuation.canopy.single', continuous: 'continuation.canopy.continuous' },
icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
},
}
@@ -68,4 +71,4 @@ export function continuationContextOf(kind: string): ContinuationContext | null
if (kind === 'cabinet') return 'cabinet'
if (kind === 'lean-to-extension') return 'canopy'
return POINT_KINDS.has(kind) ? 'point' : null
-}
+}
\ No newline at end of file
diff --git a/packages/editor/src/lib/i18n.tsx b/packages/editor/src/lib/i18n.tsx
new file mode 100644
index 0000000000..431cd29b4b
--- /dev/null
+++ b/packages/editor/src/lib/i18n.tsx
@@ -0,0 +1,66 @@
+'use client'
+
+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'
+
+export type Locale = 'en' | 'zh'
+
+function getDefaultLocale(): Locale {
+ const browserLang = navigator.language.toLowerCase()
+ return browserLang.startsWith('zh') ? 'zh' : 'en'
+}
+
+export const defaultLocale: Locale = getDefaultLocale()
+
+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 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 type Translator = (
+ key: string,
+ params?: Record,
+) => string
+
+export function I18nProvider({ children }: { children: ReactNode }) {
+ const [locale, setLocale] = useState(getDefaultLocale())
+
+ 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..b642de5046
--- /dev/null
+++ b/packages/editor/src/lib/i18n/en.json
@@ -0,0 +1,2304 @@
+{
+ "buildTab.tile.wall": "Wall",
+ "buildTab.tile.fence": "Fence",
+ "buildTab.tile.slab": "Slab",
+ "buildTab.tile.ceiling": "Ceiling",
+ "buildTab.tile.roof": "Roof",
+ "buildTab.tile.stairs": "Stairs",
+ "buildTab.tile.elevator": "Elevator",
+ "buildTab.tile.door": "Door",
+ "buildTab.tile.window": "Window",
+ "buildTab.tile.column": "Column",
+ "buildTab.tile.shelf": "Shelf",
+ "buildTab.tile.spawn": "Spawn Point",
+ "buildTab.tile.kitchen": "Kitchen",
+ "buildTab.tile.mep": "MEP",
+ "buildTab.tile.painting": "Painting",
+ "buildTab.tile.terrain": "Terrain",
+ "buildTab.mep.duct": "Duct",
+ "buildTab.mep.register": "Register",
+ "buildTab.mep.hvacUnit": "HVAC Unit",
+ "buildTab.mep.lineset": "Lineset",
+ "buildTab.mep.liquidLine": "Liquid Line",
+ "buildTab.mep.dwvPipe": "DWV Pipe",
+ "buildTab.mep.modularCabinet": "Modular Cabinet",
+ "buildTab.section.roofType": "Roof type",
+ "buildTab.section.createFrom": "Create from",
+ "buildTab.section.featuresAndExtensions": "Features & extensions",
+ "buildTab.roofType.hip": "Hip",
+ "buildTab.roofType.gable": "Gable",
+ "buildTab.roofType.shed": "Shed",
+ "buildTab.roofType.flat": "Flat",
+ "buildTab.roofType.gambrel": "Gambrel",
+ "buildTab.roofType.dutch": "Dutch",
+ "buildTab.roofType.mansard": "Mansard",
+ "buildTab.roofType.conical": "Conical",
+ "buildTab.roofSource.room": "Room",
+ "buildTab.roofSource.walls": "Wall",
+ "buildTab.roofSource.draw": "Draw",
+ "buildTab.roofSource.roomHint": "Hover a room to preview its boundary, then click to place.",
+ "buildTab.roofSource.wallsHint": "Select a curved wall to match its radius and arc.",
+ "buildTab.roofSource.drawHint": "Draw the roof footprint with two corner clicks.",
+ "buildTab.action.addFitting": "Add Fitting",
+ "buildTab.action.addTrap": "Add Trap",
+ "buildTab.liquidLine.followLineset": "Follow lineset",
+ "buildTab.liquidLine.followHintOn": "Click a lineset to lay the line beside it.",
+ "buildTab.liquidLine.followHintOff": "Trace a line alongside an existing lineset (F).",
+ "buildTab.toggle.on": "On",
+ "buildTab.toggle.off": "Off",
+ "catalog.intensity": "Intensity",
+ "materials.flooring": "Flooring",
+ "materials.other": "Other",
+ "materials.roof": "Roofing",
+ "materials.wood": "Wood",
+ "measurement.angle": "Angle",
+ "measurement.angularDimension": "Angular dimension",
+ "measurement.arcLength": "Arc length",
+ "measurement.area": "Area",
+ "measurement.centerMark": "Center mark",
+ "measurement.chordDimension": "Chord dimension",
+ "measurement.continuousDimension": "Continuous dimension",
+ "measurement.coordinateDimensions": "Coordinate dimensions",
+ "measurement.diameterDimension": "Diameter dimension",
+ "measurement.distance": "Distance",
+ "measurement.floorplanSection": "Floor plan",
+ "measurement.linearDimension": "Linear dimension",
+ "measurement.measurePrefix": "Measure: {label}",
+ "measurement.options": "Measurement options",
+ "measurement.perimeter": "Perimeter",
+ "measurement.radiusDimension": "Radius dimension",
+ "measurement.smart": "Smart",
+ "measurement.type": "Measurement type",
+ "measurement.volume": "Volume",
+ "commands.addLevel": "Add Level",
+ "commands.back": "back",
+ "commands.cameraSwitchTo": "Camera: Switch to {mode}",
+ "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.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",
+ "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.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",
+ "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.translucent": "Translucent",
+ "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.diameter": "Diameter",
+ "common.dimensions": "Dimensions",
+ "common.area": "Area",
+ "common.directions.down": "Down",
+ "common.directions.left": "Left",
+ "common.directions.right": "Right",
+ "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.minus45": "-45°",
+ "common.move": "Move",
+ "common.next": "Next",
+ "common.noResults": "No results found",
+ "common.objectsSelected": "objects selected",
+ "common.open": "Open",
+ "common.overhang": "Overhang",
+ "common.paste": "Paste",
+ "common.plus45": "+45°",
+ "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.style": "Style",
+ "common.success": "Success",
+ "common.thickness": "Thickness",
+ "common.undo": "Undo",
+ "common.untitled": "Untitled",
+ "common.width": "Width",
+ "common.x": "X",
+ "common.y": "Y",
+ "common.z": "Z",
+ "common.yaw": "Yaw",
+ "common.rotation": "Rotation",
+ "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",
+ "contextualHelp.mep.fitting.rotate45": "Rotate ±45°",
+ "contextualHelp.mep.fitting.showHandles": "Click the handle dot to show move + rotate handles",
+ "contextualHelp.mep.fitting.switchAxis": "Switch the rotation axis (Y → X → Z)",
+ "contextualHelp.mep.run.detachJoint": "Detach the joint while dragging an arrow",
+ "contextualHelp.mep.run.showArrows": "Click a handle dot to show move arrows",
+ "contextualHelp.measurement.exit": "Exit smart measure",
+ "contextualHelp.measurement.inspect": "Inspect surface dimensions",
+ "contextualHelp.measurement.pin": "Pin measurement lens",
+ "contextualHelp.reshape.controlPoint": "Move control point",
+ "contextualHelp.reshape.corner": "Move corner",
+ "contextualHelp.reshape.curve": "Curve",
+ "contextualHelp.reshape.endpoint": "Move endpoint",
+ "contextualHelp.reshape.tangent": "Move tangent",
+ "contextualHelp.resize": "Resize",
+ "contextualHelp.rotateHandle.active": "Rotating freely (no angle step)",
+ "contextualHelp.rotateHandle.idle": "Hold to rotate freely",
+ "contextualHelp.sculpt.brushSize": "Brush size",
+ "contextualHelp.sculpt.cancelPick": "Cancel picking",
+ "contextualHelp.sculpt.cancelStroke": "Cancel stroke",
+ "contextualHelp.sculpt.flatten": "Level ground",
+ "contextualHelp.sculpt.lower": "Lower ground",
+ "contextualHelp.sculpt.pickTarget": "Pick target height",
+ "contextualHelp.sculpt.raise": "Raise ground",
+ "contextualHelp.sculpt.smooth": "Smooth ground",
+ "contextualHelp.select.addOrRemove": "Add or remove objects from the selection",
+ "contextualHelp.select.clearSelection": "Clear the selection (or click outside)",
+ "contextualHelp.select.groupSelection": "Group selection (session only)",
+ "contextualHelp.select.moveAsOne": "Click or drag the selection to move it as one",
+ "contextualHelp.select.rotateSelection": "Rotate the selection ±45°",
+ "contextualHelp.select.ungroupSelection": "Ungroup session selection",
+ "contextualHelp.single.movableDrag": "Drag selected movable object",
+ "contextualHelp.single.rotatableDrag": "Drag left or right to rotate selected object",
+ "dialog.deleteElements.title": "Delete {count} elements?",
+ "dialog.deleteElements.description": "This removes every selected element. You can undo the deletion while it remains in the editor history.",
+ "editor.cameraPan": "Pan",
+ "editor.cameraRotate": "Rotate",
+ "editor.cameraZoom": "Zoom",
+ "editor.create": "Create",
+ "editor.createNew": "Create new",
+ "editor.recent": "Recent",
+ "editor.delete": "Delete",
+ "editor.cancel": "Cancel",
+ "editor.clearScale": "Clear Scale",
+ "editor.center": "Center",
+ "editor.resetRotation": "Reset Rotation",
+ "editor.resetImageScale": "Reset Image Scale",
+ "editor.opacity": "Opacity",
+ "editor.move": "Move",
+ "editor.toggleSidebar": "Toggle Sidebar",
+ "editor.close": "Close",
+ "editor.duplicate": "Duplicate",
+ "editor.editProperties": "Edit properties",
+ "editor.cameraControlsHint": "Camera controls hint",
+ "editor.dismissCameraControlsHint": "Dismiss camera controls hint",
+ "editor.jump": "Jump",
+ "editor.sprint": "Sprint",
+ "editor.interact": "Interact",
+ "editor.clickToLookAround": "Click to look around",
+ "editor.exitStreetView": "Exit Street View",
+ "editor.referenceFloor": "Reference floor",
+ "editor.setCorner": "Set corner",
+ "editor.forcePlace": "Force place",
+ "editor.guidedConstraintsBypassed": "Guided constraints bypassed",
+ "editor.openSavedScenes": "Scenes",
+ "editor.place": "Place",
+ "editor.placeBuilding": "Place building",
+ "editor.rotate": "Rotate",
+ "editor.rotateCounterclockwise": "Rotate counterclockwise",
+ "editor.rotateClockwise": "Rotate clockwise",
+ "editor.sceneFailedToRender": "The editor scene failed to render",
+ "editor.setOverlayScale": "Set overlay scale",
+ "editor.somethingWentWrong": "Something went wrong",
+ "editor.addNewLevel": "Add new level",
+ "editor.displaysMobileSidebar": "Displays the mobile sidebar.",
+ "editor.material": "Material",
+ "editor.customMaterial": "Custom Material",
+ "editor.image": "Image",
+ "editor.3dScan": "3D Scan",
+ "editor.guideImage": "Guide Image",
+ "editor.referenceScale": "Reference Scale",
+ "editor.quickActions": "Quick Actions",
+ "editor.position": "Position",
+ "editor.rotation": "Rotation",
+ "editor.scaleAndOpacity": "Scale & Opacity",
+ "editor.actions": "Actions",
+ "editor.expandSidebar": "Expand sidebar",
+ "editor.custom": "Custom",
+ "editor.addPoint": "Add point",
+ "editor.viewSnapshot": "View snapshot",
+ "editor.reloadEditor": "Reload editor",
+ "editor.drawnLine": "Drawn line",
+ "editor.realLength": "Real length",
+ "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",
+ "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",
+ "functionTree.bathroom": "Bathroom",
+ "functionTree.bedroom": "Bedroom",
+ "functionTree.dining": "Dining",
+ "functionTree.entry": "Entry",
+ "functionTree.hvac": "HVAC",
+ "functionTree.kitchen": "Kitchen",
+ "functionTree.laundry": "Laundry",
+ "functionTree.lighting": "Lighting",
+ "functionTree.living": "Living",
+ "functionTree.office": "Office",
+ "functionTree.outdoor": "Outdoor",
+ "functionTree.plumbing": "Plumbing",
+ "functionTree.storage": "Storage",
+ "itemHelper.freePlace": "Free place",
+ "itemHelper.placeItem": "Place item",
+ "itemHelper.rotateClockwise": "Rotate clockwise",
+ "itemHelper.rotateCounterclockwise": "Rotate counterclockwise",
+ "helper.continuation.line": "Continuation: {label}",
+ "helper.continuation.tooltip": "Continuation — click or press C to cycle",
+ "helper.fence.continuationCurved": "Straight continuation is unavailable while curved fence type is active",
+ "helper.fence.continuationLine": "Fence continuation: {label}",
+ "helper.fence.continuationStraight": "Straight fence continuation — click or press C to toggle",
+ "helper.fence.curved": "Curved",
+ "helper.fence.finishCurve": "Finish curve (or double-click)",
+ "helper.fence.straight": "Straight",
+ "helper.fence.straightContinuous": "Straight: Continuous",
+ "helper.fence.straightSingle": "Straight: Single",
+ "helper.fence.typeCurved": "Type: Curved",
+ "helper.fence.typeLine": "Fence type: {kind}",
+ "helper.fence.typeStraight": "Type: Straight",
+ "helper.fence.typeTooltip": "Fence type — click or press T to switch between straight and curved",
+ "helper.gridStep.label": "Grid: {step} m",
+ "helper.gridStep.line": "Grid step: {step} m",
+ "helper.gridStep.tooltip": "Grid step — click or tap Ctrl to cycle",
+ "helper.paint.allMatching": "All matching",
+ "helper.paint.hoverSurface": "Hover a surface to paint",
+ "helper.paint.line": "Paint: {scope}",
+ "helper.paint.pickMaterial": "Select a material to paint",
+ "helper.paint.room": "Room",
+ "helper.paint.scopeLine": "Paint scope: {scope}",
+ "helper.paint.scopeTooltip": "Paint scope — click or press Shift to cycle",
+ "helper.paint.thisSurface": "This surface",
+ "helper.paint.wholeNoun": "Whole {noun}",
+ "helper.snapping.angles": "Angles",
+ "helper.snapping.grid": "Grid",
+ "helper.snapping.lines": "Lines",
+ "helper.snapping.line": "Snapping: {mode}",
+ "helper.snapping.off": "Off",
+ "helper.snapping.tooltip": "Snapping mode — click or press Shift to cycle",
+ "items.all": "All",
+ "items.community": "Community",
+ "keys.commandMac": "Command",
+ "keys.control": "Control",
+ "keys.leftClick": "Left click",
+ "keys.middleClick": "Middle click",
+ "keys.rightClick": "Right click",
+ "keys.shift": "Shift",
+ "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.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}",
+ "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.errors_plural": "{count} errors",
+ "loadBuild.floorArea": "Floor area",
+ "loadBuild.hide": "Hide",
+ "loadBuild.invalidJson": "File could not be parsed as JSON.",
+ "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",
+ "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.warnings_plural": "{count} warnings",
+ "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.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",
+ "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.boxVent": "Box Vent",
+ "nodes.roofSegment.hip": "Hip",
+ "nodes.roofSegment.gable": "Gable",
+ "nodes.roofSegment.shed": "Shed",
+ "nodes.roofSegment.flat": "Flat",
+ "nodes.roofSegment.gambrel": "Gambrel",
+ "nodes.roofSegment.dutch": "Dutch",
+ "nodes.roofSegment.mansard": "Mansard",
+ "nodes.roofSegment.conical": "Conical",
+ "nodes.roofSegment.conicalShape": "Conical Shape",
+ "nodes.roofSegment.clippedVersion": "Clipped version",
+ "nodes.roofSegment.startAngle": "Start Angle",
+ "nodes.roofSegment.arc": "Arc",
+ "nodes.roofSegment.angle": "Angle",
+ "nodes.roofSegment.editFootprint": "Edit footprint",
+ "nodes.roofSegment.autoRidgeVent": "Auto ridge vent",
+ "nodes.roofSegment.autoGutters": "Auto gutters",
+ "nodes.roofSegment.kinkDepth": "Kink Depth",
+ "nodes.roofSegment.kinkHeight": "Kink Height",
+ "nodes.roofSegment.waistWidth": "Waist Width",
+ "nodes.roofSegment.waistHeight": "Waist Height",
+ "nodes.roofSegment.waistLength": "Waist Length",
+ "nodes.roofSegment.topRakeThickness": "Top Rake Thick.",
+ "nodes.roofSegment.topRakeLength": "Top Rake Length",
+ "nodes.roofSegment.wallThickness": "Wall Thick.",
+ "nodes.roofSegment.deckThickness": "Deck Thick.",
+ "nodes.roofSegment.shingleThickness": "Shingle Thick.",
+ "nodes.stairSegment.flight": "Flight",
+ "nodes.stairSegment.landing": "Landing",
+ "nodes.stairSegment.front": "Front",
+ "nodes.stairSegment.left": "Left",
+ "nodes.stairSegment.right": "Right",
+ "nodes.elevator.glass": "Glass",
+ "nodes.ridgeVent.standard": "Standard",
+ "nodes.ridgeVent.shingled": "Shingled",
+ "nodes.ridgeVent.flanged": "Flanged",
+ "nodes.ridgeVent.endCaps": "End Caps",
+ "nodes.ridgeVent.open": "Open",
+ "nodes.skylight.top": "Top",
+ "nodes.skylight.bottom": "Bottom",
+ "nodes.skylight.left": "Left",
+ "nodes.skylight.right": "Right",
+ "nodes.skylight.motor": "Motor",
+ "nodes.skylight.skylight": "Skylight",
+ "nodes.skylight.noMotor": "No Motor",
+ "nodes.skylight.alongZ": "Along Z",
+ "nodes.skylight.alongX": "Along X",
+ "nodes.skylight.yes": "Yes",
+ "nodes.skylight.no": "No",
+ "common.yes": "Yes",
+ "common.no": "No",
+ "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.defaultName": "Box Vent {count}",
+ "nodes.boxVent.topTaper": "Top Taper",
+ "nodes.boxVent.style": "Style",
+ "nodes.boxVent.toolHints.cancel": "Cancel",
+ "nodes.boxVent.toolHints.place": "Place box vent",
+ "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.customHeight": "Custom height",
+ "nodes.ceiling.currently": "Currently {value}",
+ "nodes.ceiling.editing": "Editing",
+ "nodes.ceiling.followsLevel": "Follows level",
+ "nodes.ceiling.height": "Height",
+ "nodes.ceiling.heightPresets.high": "High (3.0m)",
+ "nodes.ceiling.heightPresets.highImperial": "High (9'0\")",
+ "nodes.ceiling.heightPresets.low": "Low (2.4m)",
+ "nodes.ceiling.heightPresets.lowImperial": "Low (8'0\")",
+ "nodes.ceiling.heightPresets.standard": "Standard (2.5m)",
+ "nodes.ceiling.heightPresets.standardImperial": "Standard (8'6\")",
+ "nodes.ceiling.holeLabel": "Hole {index}",
+ "nodes.ceiling.holes": "Holes",
+ "nodes.ceiling.info": "Info",
+ "nodes.ceiling.limitedBy": "Limited by the level to {available} — raise the level height for a taller ceiling.",
+ "nodes.ceiling.manual": "Manual",
+ "nodes.ceiling.noHoles": "No holes",
+ "nodes.ceiling.tooTall": "Taller than this level ({available} available). Raise the level height first.",
+ "nodes.chimney.aboveRidge": "Above Ridge",
+ "nodes.chimney.back": "Back",
+ "nodes.chimney.bandExtent": "Extent",
+ "nodes.chimney.bandHeight": "Height",
+ "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.width": "Width",
+ "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.flueCount": "Count",
+ "nodes.chimney.flueDiameter": "Diameter",
+ "nodes.chimney.flueHeight": "Height",
+ "nodes.chimney.flueShape": "Shape",
+ "nodes.chimney.flueSpacing": "Spacing",
+ "nodes.chimney.flueWallThickness": "Wall Thickness",
+ "nodes.chimney.wallThickness": "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.chimney.chimney": "Chimney",
+ "nodes.chimney.chimneyType": "Chimney Type",
+ "nodes.chimney.offset": "Offset",
+ "nodes.chimney.footprint": "Footprint",
+ "nodes.chimney.style": "Style",
+ "nodes.column.aFrame": "A-Frame",
+ "nodes.column.applyPreset": "Apply preset...",
+ "nodes.column.applyProportion": "Apply proportion...",
+ "nodes.column.preset": "Preset",
+ "nodes.column.shape": "Shape",
+ "nodes.column.transform": "Transform",
+ "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.column.dimensions": "Dimensions",
+ "nodes.column.shaft": "Shaft",
+ "nodes.column.ends": "Ends",
+ "nodes.column.slender": "Slender",
+ "nodes.column.standard": "Standard",
+ "nodes.column.heavy": "Heavy",
+ "nodes.column.shortStout": "Short / Stout",
+ "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.defaultName": "Window {count}",
+ "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.pitchDirection": "Pitch Direction",
+ "nodes.dormer.pitchRise": "Pitch Rise",
+ "nodes.dormer.rect": "Rect",
+ "nodes.dormer.riseBack": "Rise Back",
+ "nodes.dormer.riseFront": "Rise Front",
+ "nodes.dormer.roofHeight": "Roof Height",
+ "nodes.dormer.rounded": "Rounded",
+ "nodes.dormer.roofType": "Roof Type",
+ "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.cabDepth": "Depth",
+ "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.solid": "Solid",
+ "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.elevator.yaw": "Yaw",
+ "nodes.elevator.access": "Access",
+ "nodes.elevator.cab": "Cab",
+ "nodes.elevator.destination": "Destination",
+ "nodes.elevator.doors": "Doors",
+ "nodes.elevator.copy": "{name} Copy",
+ "nodes.elevator.defaultCopy": "Elevator Copy",
+ "nodes.elevator.motion": "Motion",
+ "nodes.elevator.offset": "Offset",
+ "nodes.elevator.rotation": "Rotation",
+ "nodes.elevator.rotationPresets.negative": "-45°",
+ "nodes.elevator.rotationPresets.positive": "+45°",
+ "nodes.elevator.shaft": "Shaft",
+ "nodes.elevator.stopLabel": "Stop {n}",
+ "nodes.elevator.serviceButton": "Service",
+ "nodes.elevator.disabledButton": "Disabled",
+ "nodes.fence.description": "A straight or curved fence segment with configurable posts and infill.",
+ "nodes.fence.label": "Fence",
+ "nodes.fence.toolHints.allowAngles": "Allow non-45° angles",
+ "nodes.fence.toolHints.cancel": "Cancel",
+ "nodes.fence.toolHints.setStartEnd": "Set fence start / end",
+ "nodes.measurement.toolHints.finish": "Finish measurement",
+ "nodes.measurement.toolHints.finishContinue": "Finish and continue",
+ "nodes.measurement.toolHints.placePoint": "Place measurement point",
+ "nodes.measurement.toolHints.removeLast": "Remove last point",
+ "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.addCupola": "Add Cupola",
+ "nodes.roof.addDormer": "Add Dormer",
+ "nodes.roof.addEyebrowVent": "Add Eyebrow Vent",
+ "nodes.roof.addGutter": "Add Gutter",
+ "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.defaultName.boxVent": "Box Vent {count}",
+ "nodes.roof.defaultName.chimney": "Chimney {count}",
+ "nodes.roof.defaultName.dormer": "Dormer {count}",
+ "nodes.roof.defaultName.gutter": "Gutter {count}",
+ "nodes.roof.defaultName.ridgeVent": "Ridge Vent {count}",
+ "nodes.roof.defaultName.segment": "Segment {count}",
+ "nodes.roof.defaultName.skylight": "Skylight {count}",
+ "nodes.roof.defaultName.solarPanel": "Solar Panel {count}",
+ "nodes.roof.defaultName.turbineVent": "Turbine Vent {count}",
+ "nodes.roof.elements": "Elements",
+ "nodes.roof.fallbackTitle": "Roof",
+ "nodes.roof.kindLabel.boxVent": "box vent",
+ "nodes.roof.kindLabel.chimney": "chimney",
+ "nodes.roof.kindLabel.dormer": "dormer",
+ "nodes.roof.kindLabel.gutter": "gutter",
+ "nodes.roof.kindLabel.ridgeVent": "ridge vent",
+ "nodes.roof.kindLabel.skylight": "skylight",
+ "nodes.roof.kindLabel.solarPanel": "solar panel",
+ "nodes.roof.kindLabel.turbineVent": "turbine vent",
+ "nodes.roofSegment.fallbackTitle": "Roof Segment",
+ "nodes.roofSegment.roofType": "Roof Type",
+ "nodes.roofSegment.footprint": "Footprint",
+ "nodes.roofSegment.wallHeight": "Wall Height",
+ "nodes.roofSegment.pitch": "Pitch",
+ "nodes.roofSegment.shape": "Shape",
+ "nodes.roofSegment.structure": "Structure",
+ "nodes.roofSegment.trim": "Trim",
+ "nodes.roofSegment.drainage": "Drainage",
+ "nodes.roof.ridge": "Ridge",
+ "nodes.roof.segments": "Segments",
+ "nodes.roof.turbine": "Turbine",
+ "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.base": "Base",
+ "nodes.slab.depth": "Depth",
+ "nodes.slab.description": "A polygon-bounded floor surface that hosts items on top.",
+ "nodes.slab.editing": "(Editing)",
+ "nodes.slab.elevationPresets.ground": "Ground (0m)",
+ "nodes.slab.elevationPresets.raised": "Raised (+5cm)",
+ "nodes.slab.elevationPresets.standard": "Standard (5cm)",
+ "nodes.slab.elevationPresets.step": "Step (+15cm)",
+ "nodes.slab.elevationPresets.sunken": "Sunken (-15cm)",
+ "nodes.slab.elevationPresets.thick": "Thick (15cm)",
+ "nodes.slab.elevationPresets.thin": "Thin (2cm)",
+ "nodes.slab.fallbackTitle": "Slab",
+ "nodes.slab.fixed": "Fixed",
+ "nodes.slab.floor": "Floor",
+ "nodes.slab.followsTerrain": "Follows terrain",
+ "nodes.slab.foundation": "Foundation",
+ "nodes.slab.holeLabel": "Hole {index}",
+ "nodes.slab.label": "Slab",
+ "nodes.slab.manual": "Manual",
+ "nodes.slab.noHoles": "No holes",
+ "nodes.slab.pts": "pts",
+ "nodes.slab.rim": "Rim",
+ "nodes.slab.surface": "Surface",
+ "nodes.slab.terrainDescription": "Extends the perimeter down to terrain. The flat surface, base, and thickness stay unchanged.",
+ "nodes.slab.toolHints.cancel": "Cancel",
+ "nodes.slab.toolHints.finish": "Finish slab",
+ "nodes.slab.toolHints.trace": "Trace slab outline",
+ "nodes.slab.elevation": "Elevation",
+ "nodes.slab.info": "Info",
+ "nodes.slab.holes": "Holes",
+ "nodes.solarPanel.columns": "Columns",
+ "nodes.solarPanel.customNotice": "Custom — dimensions don't match any preset",
+ "nodes.solarPanel.fallbackTitle": "Solar Panel",
+ "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.setbacksTooLarge": "Setbacks too large to fit a panel.",
+ "nodes.solarPanel.standoff": "Standoff",
+ "nodes.solarPanel.tiltAngle": "Tilt angle",
+ "nodes.solarPanel.tilted": "Tilted",
+ "nodes.solarPanel.preset": "Preset",
+ "nodes.solarPanel.presetCompact": "Compact",
+ "nodes.solarPanel.presetFrameless": "Frameless",
+ "nodes.solarPanel.presetResidential": "Residential",
+ "nodes.solarPanel.presetResidentialLarge": "Residential Large",
+ "nodes.solarPanel.array": "Array",
+ "nodes.solarPanel.panel": "Panel",
+ "nodes.solarPanel.mounting": "Mounting",
+ "nodes.solarPanel.autoFitToRoof": "Auto-fit to roof",
+ "nodes.solarPanel.toolHints.cancel": "Cancel",
+ "nodes.solarPanel.toolHints.place": "Place solar panel",
+ "nodes.skyLight.type": "Type",
+ "nodes.skyLight.dimensions": "Dimensions",
+ "nodes.skyLight.frame": "Frame",
+ "nodes.skyLight.curb": "Curb",
+ "nodes.skyLight.fallbackTitle": "Skylight",
+ "nodes.ridgeVent.style": "Style",
+ "nodes.ridgeVent.dimensions": "Dimensions",
+ "nodes.ridgeVent.rotation": "Rotation",
+ "nodes.ridgeVent.fallbackTitle": "Ridge Vent",
+ "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.fallbackTitle": "Staircase",
+ "nodes.stair.fromLevel": "From Level",
+ "nodes.stair.followsDeck": "Follows deck",
+ "nodes.stair.customRise": "Custom rise",
+ "nodes.stair.currently": "Currently {height} m",
+ "nodes.stair.deckName": "Deck",
+ "nodes.stair.geometry": "Geometry",
+ "nodes.stair.landing": "Landing",
+ "nodes.stair.levelName": "Level {n}",
+ "nodes.stair.rise": "Rise",
+ "nodes.stair.segmentName": "Segment {n}",
+ "nodes.stair.steps": "Steps",
+ "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.stairSegment.type": "Type",
+ "nodes.stairSegment.attachment": "Attachment",
+ "nodes.stairSegment.dimensions": "Dimensions",
+ "nodes.stairSegment.structure": "Structure",
+ "nodes.stairSegment.fallbackTitle": "Stair Segment",
+ "nodes.stairSegment.width": "Width",
+ "nodes.stairSegment.length": "Length",
+ "nodes.stairSegment.height": "Height",
+ "nodes.stairSegment.steps": "Steps",
+ "nodes.stairSegment.fillToFloor": "Fill to floor",
+ "nodes.stairSegment.thickness": "Thickness",
+ "nodes.stairSegment.move": "Move",
+ "nodes.stairSegment.duplicate": "Duplicate",
+ "nodes.wall.bands": "Wall bands",
+ "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.followsLevel": "Follows level",
+ "nodes.wall.customHeight": "Custom height",
+ "nodes.wall.currently": "Currently {measurement}",
+ "nodes.wall.label": "Wall",
+ "nodes.wall.length": "Length",
+ "nodes.wall.top": "Top",
+ "nodes.wall.bottom": "Bottom",
+ "nodes.wall.auto": "Auto",
+ "nodes.wall.fillToTerrain": "Fill to terrain",
+ "nodes.wall.fillToTerrainDescription": "Extends downward to meet the terrain. Height and top stay unchanged.",
+ "nodes.wall.thickness": "Thickness",
+ "nodes.wall.bandsCount": "Bands",
+ "nodes.wall.bandsLower": "Lower",
+ "nodes.wall.bandsMiddle": "Middle",
+ "nodes.wall.bandsUpper": "Upper",
+ "nodes.wall.skirting": "Skirting",
+ "nodes.wall.crown": "Crown molding",
+ "nodes.wall.chairRail": "Chair rail",
+ "nodes.wall.trimHide": "Hide {trim}",
+ "nodes.wall.trimShow": "Show {trim}",
+ "nodes.wall.interior": "Interior",
+ "nodes.wall.exterior": "Exterior",
+ "nodes.wall.both": "Both",
+ "nodes.wall.proud": "Proud",
+ "nodes.wall.offset": "Offset",
+ "nodes.wall.trimProfile.flat": "Flat",
+ "nodes.wall.trimProfile.modern": "Modern",
+ "nodes.wall.trimProfile.colonial": "Colonial",
+ "nodes.wall.trimProfile.shoe": "Shoe",
+ "nodes.wall.trimProfile.ogee": "Ogee",
+ "nodes.wall.trimProfile.cove": "Cove",
+ "nodes.wall.trimProfile.craft": "Craft",
+ "nodes.wall.trimProfile.layered": "Layered",
+ "nodes.wall.trimProfile.round": "Round",
+ "nodes.wall.trimProfile.picture": "Picture",
+ "nodes.wall.trimProfile.step": "Step",
+ "nodes.wall.dimensions": "Dimensions",
+ "nodes.spawn.spawnPoint": "Spawn Point",
+ "nodes.spawn.position": "Position",
+ "nodes.spawn.facing": "Facing",
+ "nodes.spawn.yaw": "Yaw",
+ "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.operationLabels.raise": "Raise",
+ "nodes.window.operationLabels.slats": "Slats",
+ "nodes.window.operationLabels.swing": "Swing",
+ "nodes.window.operationLabels.tilt": "Tilt",
+ "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.nodeType.block": "Block",
+ "panel.nodeType.block_plural": "Blocks",
+ "panel.nodeType.boxVent": "Box Vent",
+ "panel.nodeType.boxVent_plural": "Box Vents",
+ "panel.nodeType.building": "Building",
+ "panel.nodeType.building_plural": "Buildings",
+ "panel.nodeType.ceiling": "Ceiling",
+ "panel.nodeType.ceiling_plural": "Ceilings",
+ "panel.nodeType.chimney": "Chimney",
+ "panel.nodeType.chimney_plural": "Chimneys",
+ "panel.nodeType.constructionDimension": "Construction Dimension",
+ "panel.nodeType.constructionDimension_plural": "Construction Dimensions",
+ "panel.nodeType.cupola": "Cupola",
+ "panel.nodeType.cupola_plural": "Cupolas",
+ "panel.nodeType.dormer": "Dormer",
+ "panel.nodeType.dormer_plural": "Dormers",
+ "panel.nodeType.downspout": "Downspout",
+ "panel.nodeType.downspout_plural": "Downspouts",
+ "panel.nodeType.ductFitting": "Duct Fitting",
+ "panel.nodeType.ductFitting_plural": "Duct Fittings",
+ "panel.nodeType.ductSegment": "Duct",
+ "panel.nodeType.ductSegment_plural": "Ducts",
+ "panel.nodeType.ductTerminal": "Register",
+ "panel.nodeType.ductTerminal_plural": "Registers",
+ "panel.nodeType.eyebrowVent": "Eyebrow Vent",
+ "panel.nodeType.eyebrowVent_plural": "Eyebrow Vents",
+ "panel.nodeType.gutter": "Gutter",
+ "panel.nodeType.gutter_plural": "Gutters",
+ "panel.nodeType.hvacEquipment": "HVAC Unit",
+ "panel.nodeType.hvacEquipment_plural": "HVAC Units",
+ "panel.nodeType.leanToExtension": "Canopy",
+ "panel.nodeType.leanToExtension_plural": "Canopies",
+ "panel.nodeType.level": "Level",
+ "panel.nodeType.level_plural": "Levels",
+ "panel.nodeType.lineset": "Lineset",
+ "panel.nodeType.lineset_plural": "Linesets",
+ "panel.nodeType.liquidLine": "Liquid Line",
+ "panel.nodeType.liquidLine_plural": "Liquid Lines",
+ "panel.nodeType.pipeFitting": "Pipe Fitting",
+ "panel.nodeType.pipeFitting_plural": "Pipe Fittings",
+ "panel.nodeType.pipeSegment": "DWV Pipe",
+ "panel.nodeType.pipeSegment_plural": "DWV Pipes",
+ "panel.nodeType.pipeTrap": "Trap",
+ "panel.nodeType.pipeTrap_plural": "Traps",
+ "panel.nodeType.ridgeVent": "Ridge Vent",
+ "panel.nodeType.ridgeVent_plural": "Ridge Vents",
+ "panel.nodeType.shelf": "Shelf",
+ "panel.nodeType.shelf_plural": "Shelves",
+ "panel.nodeType.site": "Site",
+ "panel.nodeType.site_plural": "Sites",
+ "panel.nodeType.skylight": "Skylight",
+ "panel.nodeType.skylight_plural": "Skylights",
+ "panel.nodeType.solarPanel": "Solar Panel",
+ "panel.nodeType.solarPanel_plural": "Solar Panels",
+ "panel.nodeType.structuralGrid": "Structural Grid",
+ "panel.nodeType.structuralGrid_plural": "Structural Grids",
+ "panel.nodeType.turbineVent": "Turbine Vent",
+ "panel.nodeType.turbineVent_plural": "Turbine Vents",
+ "panel.nodeType.zone": "Zone",
+ "panel.nodeType.zone_plural": "Zones",
+ "panel.nodeType.column": "Column",
+ "panel.nodeType.column_plural": "Columns",
+ "panel.nodeType.door": "Door",
+ "panel.nodeType.door_plural": "Doors",
+ "panel.nodeType.elevator": "Elevator",
+ "panel.nodeType.elevator_plural": "Elevators",
+ "panel.nodeType.fence": "Fence",
+ "panel.nodeType.fence_plural": "Fences",
+ "panel.nodeType.guide": "Guide image",
+ "panel.nodeType.guide_plural": "Guide images",
+ "panel.nodeType.item": "Item",
+ "panel.nodeType.item_plural": "Items",
+ "panel.nodeType.roof": "Roof",
+ "panel.nodeType.roof_plural": "Roofs",
+ "panel.nodeType.roofSegment": "Roof segment",
+ "panel.nodeType.roofSegment_plural": "Roof segments",
+ "panel.nodeType.scan": "3D Scan",
+ "panel.nodeType.scan_plural": "3D Scans",
+ "panel.nodeType.slab": "Slab",
+ "panel.nodeType.slab_plural": "Slabs",
+ "panel.nodeType.stair": "Stair",
+ "panel.nodeType.stair_plural": "Stairs",
+ "panel.nodeType.stairSegment": "Stair segment",
+ "panel.nodeType.stairSegment_plural": "Stair segments",
+ "panel.nodeType.wall": "Wall",
+ "panel.nodeType.wall_plural": "Walls",
+ "panel.nodeType.window": "Window",
+ "panel.nodeType.window_plural": "Windows",
+ "panel.nodeType.measurement": "Measurement",
+ "panel.nodeType.measurement_plural": "Measurements",
+ "panel.nodeType.spawn": "Spawn",
+ "panel.nodeType.spawn_plural": "Spawns",
+ "panel.nodeType.cabinet": "Cabinet",
+ "panel.nodeType.cabinet_plural": "Cabinets",
+ "panel.nodeType.cabinetModule": "Cabinet module",
+ "panel.nodeType.cabinetModule_plural": "Cabinet modules",
+ "panel.action.group": "Group",
+ "panel.action.ungroup": "Ungroup",
+ "panel.action.duplicate": "Duplicate",
+ "panel.action.delete": "Delete",
+ "panel.multiSelection.selected": "{count} selected",
+ "panel.multiSelection.withGroup": "{label} · {count}",
+ "panel.multiSelection.sessionOnlyFooter": "{label} (session only). Plain click reselects all members. Not saved with the project.",
+ "panel.noEdgeLines": "No edge lines",
+ "panel.scaleAndOpacity": "Scale & Opacity",
+ "panel.selection": "Selection",
+ "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} 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.",
+ "save.createNewScene": "Create new scene",
+ "save.creating": "Creating…",
+ "save.failedToCreateScene": "Failed to create scene",
+ "save.newSceneName": "New scene name",
+ "save.noSceneToSave": "No scene to save",
+ "save.save": "Save",
+ "save.saveAs": "Save As",
+ "save.saveAsFailed": "Save as failed",
+ "save.saveFailed": "Save failed",
+ "save.saved": "Saved",
+ "save.saving": "Saving…",
+ "save.untitledScene": "Untitled Scene",
+ "settings.adjustVolume": "Adjust volume levels and mute settings",
+ "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",
+ "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",
+ "settings.visibleNodesOnly": "Visible nodes only",
+ "settings.visibleNodesOnlyDesc": "Only export nodes that are currently visible in the editor.",
+ "shortcuts.activateMeasurementTool": "Activate the measurement tool",
+ "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.deleteSelected": "Delete selected objects",
+ "shortcuts.directManipulation": "Direct Manipulation",
+ "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.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.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",
+ "shortcuts.switchToSitePhase": "Switch to Site phase",
+ "shortcuts.switchToStructurePhase": "Switch to Structure phase",
+ "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.",
+ "sidebar.build": "Build",
+ "sidebar.file": "File",
+ "sidebar.items": "Items",
+ "sidebar.scene": "Scene",
+ "sidebar.settings": "Settings",
+ "sidebar.site": "Site",
+ "site.addLevel": "Add level",
+ "site.area": "Area",
+ "site.baseElevation": "Base elevation",
+ "site.building": "Building",
+ "site.cameraSnapshot": "Camera snapshot",
+ "site.capture": "Capture",
+ "site.clearSelection": "Clear selection",
+ "site.delete": "Delete",
+ "site.duplicateLevelWithOptions": "Duplicate level with options",
+ "site.furnish": "Furnish",
+ "site.guide": "Guide",
+ "site.hide": "Hide",
+ "site.level": "Level",
+ "site.noBuildingsYet": "No buildings yet",
+ "site.noLevelsYet": "No levels yet",
+ "site.perimeter": "Perimeter",
+ "site.propertyLine": "Property Line",
+ "site.show": "Show",
+ "site.site": "Site",
+ "site.structure": "Structure",
+ "site.uploadScan": "Upload scan/floorplan",
+ "site.uploadingWithProgress": "Uploading {type}... {progress}%",
+ "site.untitled": "Untitled",
+ "site.levels": "Levels",
+ "site.addPoint": "Add point",
+ "site.axisX": "X",
+ "site.axisZ": "Z",
+ "site.camera.viewSnapshot": "View snapshot",
+ "site.camera.updateSnapshot": "Update snapshot",
+ "site.camera.takeSnapshot": "Take snapshot",
+ "site.camera.clearSnapshot": "Clear snapshot",
+ "site.objectsSelected": "{count} objects selected",
+ "site.reference.capture": "Capture",
+ "site.reference.guide": "Guide Image",
+ "site.nodeType.wall": "Wall",
+ "site.nodeType.fence": "Fence",
+ "site.nodeType.item": "Item",
+ "site.nodeType.slab": "Slab",
+ "site.nodeType.ceiling": "Ceiling",
+ "site.nodeType.roof": "Roof",
+ "site.nodeType.roofSegment": "Roof Segment",
+ "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",
+ "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",
+ "toolbar.up": "Up",
+ "toolbar.walls": "Walls",
+ "continuation.cabinet.continuous": "Continuous run",
+ "continuation.cabinet.single": "Single cabinet",
+ "continuation.canopy.continuous": "Continuous canopy",
+ "continuation.canopy.single": "Single canopy",
+ "continuation.fence.continuous": "Continuous",
+ "continuation.fence.curved": "Curved fence",
+ "continuation.fence.single": "Single fence",
+ "continuation.point.once": "Place once",
+ "continuation.point.repeat": "Place multiple",
+ "continuation.wall.room": "Room (auto-close)",
+ "continuation.wall.single": "Single wall",
+ "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.belowCount": "{count} below",
+ "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.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.opacity": "Opacity",
+ "viewer.hideReferenceRow": "Hide {subject}",
+ "viewer.showReferenceRow": "Show {subject}",
+ "viewer.deleteReferenceRow": "Delete {subject}",
+ "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.referenceSettings": "Reference settings",
+ "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",
+ "viewer.soft": "Soft",
+ "viewer.solid": "Solid",
+ "viewer.solo": "Solo",
+ "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",
+ "viewer.viewMode2D": "2D",
+ "viewer.viewMode3D": "3D",
+ "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.",
+ "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.item.position": "Position",
+ "nodes.item.rotation": "Rotation",
+ "nodes.item.scale": "Scale",
+ "nodes.item.uniformScale": "Uniform Scale",
+ "nodes.item.info": "Info",
+ "nodes.item.collections": "Collections",
+ "nodes.item.manageCollections": "Manage collections…",
+ "nodes.site.label": "Site",
+ "nodes.site.description": "A site containing buildings.",
+ "nodes.dormer.toolHints.place": "Place dormer on roof",
+ "nodes.dormer.toolHints.rotateGhost": "Rotate ghost",
+ "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",
+ "nodes.block.actions": "Actions",
+ "nodes.block.addSlot": "Add slot",
+ "nodes.block.blockAccent": "Block Accent",
+ "nodes.block.defaultMaterial": "Default material",
+ "nodes.block.editModeSlotActions": "Enter Edit Mode to use slot actions",
+ "nodes.block.editModeAssignFaces": "Enter Edit Mode to assign faces",
+ "nodes.block.faceSelectSwitch": "Switch to Face Select (3)",
+ "nodes.block.fallbackTitle": "Block",
+ "nodes.block.noFacesSelected": "No faces selected",
+ "nodes.block.readOnly": "Scene is read-only",
+ "nodes.block.mixedSlots": "{count} faces · Mixed slots",
+ "nodes.block.singleSlot": "{count} {faceLabel} · {slotLabel}",
+ "nodes.block.face": "face",
+ "nodes.block.faces": "faces",
+ "nodes.block.position": "Position",
+ "nodes.block.selectFacesFirst": "Select one or more faces first",
+ "nodes.block.slots": "Slots",
+ "nodes.block.slotApplied": "{slot} applied to {count} {faceLabel} with an accent material. Use Paint (P) to replace it.",
+ "nodes.block.slotApplyAria": "Apply {slot} to selected faces",
+ "nodes.block.slotDeleteAria": "Delete {slot} slot",
+ "nodes.block.slotRenameAria": "Rename {slot} slot",
+ "nodes.block.slotDeleteTitle": "Delete material slot and use Body on its faces",
+ "nodes.block.unpainted": "Unpainted",
+ "nodes.block.toolHints.place": "Place block",
+ "nodes.column.toolHints.place": "Place column",
+ "nodes.cupola.cupola": "Cupola",
+ "nodes.cupola.dome": "Dome",
+ "nodes.cupola.finial": "Finial",
+ "nodes.cupola.noFinial": "No Finial",
+ "nodes.cupola.pyramid": "Pyramid",
+ "nodes.cupola.toolHints.place": "Place cupola on roof",
+ "nodes.door.toolHints.flipSide": "Flip side",
+ "nodes.downspout.toolHints.highlightOutlet": "Highlight outlet",
+ "nodes.downspout.toolHints.drop": "Drop downspout from outlet",
+ "nodes.elevator.toolHints.place": "Place elevator",
+ "nodes.eyebrowVent.toolHints.place": "Place eyebrow vent on roof",
+ "nodes.eyebrowVent.fallbackTitle": "Eyebrow Vent",
+ "nodes.eyebrowVent.style": "Style",
+ "nodes.eyebrowVent.scoop": "Scoop",
+ "nodes.eyebrowVent.halfRound": "Half-round",
+ "nodes.eyebrowVent.slantBox": "Slant-box",
+ "nodes.eyebrowVent.louvers": "Louvers",
+ "nodes.eyebrowVent.backHeight": "Back height",
+ "nodes.eyebrowVent.dimensions": "Dimensions",
+ "nodes.gutter.toolHints.place": "Place gutter on roof eave",
+ "nodes.item.toolHints.place": "Place item",
+ "nodes.item.toolHints.cycleSnap": "Cycle snapping mode",
+ "nodes.roof.toolHints.setFootprint": "Set roof footprint",
+ "nodes.roof.toolHints.placement": "Placement",
+ "nodes.roof.toolHints.placementAuto": "Placement: Auto",
+ "nodes.roof.toolHints.placementGround": "Placement: Ground",
+ "nodes.roof.toolHints.placementRoof": "Placement: Roof",
+ "nodes.roof.toolHints.rotateDirection": "Rotate roof direction 90°",
+ "nodes.spawn.toolHints.rotate": "Rotate spawn point",
+ "nodes.stair.toolHints.place": "Place stairs",
+ "nodes.structuralGrid.toolHints.start": "Start grid axis",
+ "nodes.structuralGrid.toolHints.finish": "Finish grid axis",
+ "nodes.structuralGrid.toolHints.bypassSnap": "Bypass snapping",
+ "nodes.turbineVent.toolHints.place": "Place turbine vent on roof",
+ "nodes.turbineVent.fallbackTitle": "Turbine Vent",
+ "nodes.turbineVent.style": "Style",
+ "nodes.turbineVent.globe": "Globe",
+ "nodes.turbineVent.cylinder": "Cylinder",
+ "nodes.turbineVent.dimensions": "Dimensions",
+ "nodes.turbineVent.neckHeight": "Neck Height",
+ "nodes.turbineVent.vanes": "Vanes",
+ "nodes.turbineVent.motion": "Motion",
+ "nodes.turbineVent.pause": "Pause",
+ "nodes.turbineVent.play": "Play",
+ "nodes.turbineVent.spinSpeed": "Spin Speed",
+ "nodes.window.toolHints.flipSide": "Flip side",
+ "nodes.cabinet.addChimney": "Add chimney",
+ "nodes.cabinet.addCompartment": "Add compartment",
+ "nodes.cabinet.addWallCabinet": "Add wall cabinet",
+ "nodes.cabinet.cabinetType": "Cabinet Type",
+ "nodes.cabinet.carcassHeight": "Carcass height",
+ "nodes.cabinet.close": "Close",
+ "nodes.cabinet.closeCabinet": "Close cabinet",
+ "nodes.cabinet.compartments": "Compartments",
+ "nodes.cabinet.dimensions": "Dimensions",
+ "nodes.cabinet.fallbackTitle": "Modular Cabinet",
+ "nodes.cabinet.fillToCeiling": "Fill to ceiling",
+ "nodes.cabinet.finish": "Finish",
+ "nodes.cabinet.frontOverlay.full": "Overlay",
+ "nodes.cabinet.frontOverlay.inset": "Inset",
+ "nodes.cabinet.frontStyle.raisedArch": "Raised Arch",
+ "nodes.cabinet.frontStyle.shaker": "Shaker",
+ "nodes.cabinet.frontStyle.slab": "Slab",
+ "nodes.cabinet.fronts": "Fronts",
+ "nodes.cabinet.fronts.mounting": "Mounting",
+ "nodes.cabinet.fronts.revealGap": "Reveal gap",
+ "nodes.cabinet.fronts.style": "Style",
+ "nodes.cabinet.handle.bar": "Bar",
+ "nodes.cabinet.handle.cutout": "Cutout",
+ "nodes.cabinet.handle.hole": "Hole",
+ "nodes.cabinet.handle.knob": "Knob",
+ "nodes.cabinet.handle.none": "None",
+ "nodes.cabinet.handlePosition.auto": "Auto",
+ "nodes.cabinet.handlePosition.center": "Center",
+ "nodes.cabinet.handlePosition.top": "Top",
+ "nodes.cabinet.handles": "Handles",
+ "nodes.cabinet.handles.position": "Position",
+ "nodes.cabinet.handles.style": "Style",
+ "nodes.cabinet.open": "Open",
+ "nodes.cabinet.openAnimation": "Open Animation",
+ "nodes.cabinet.openCabinet": "Open cabinet",
+ "nodes.cabinet.play": "Play",
+ "nodes.cabinet.playAnimation": "Play animation",
+ "nodes.cabinet.planningChecks": "Planning checks",
+ "nodes.cabinet.presets": "Presets",
+ "nodes.cabinet.reflowRejected": "No space in this run. No base cabinet can shrink enough to fit this item.",
+ "nodes.cabinet.removeWallCabinet": "Remove wall cabinet",
+ "nodes.cabinet.standardWidth": "Standard width",
+ "nodes.cabinet.stop": "Stop",
+ "nodes.cabinet.stopAnimation": "Stop animation",
+ "nodes.cabinet.tier.base": "Base Cabinet",
+ "nodes.cabinet.tier.tall": "Tall Cabinet",
+ "nodes.cabinet.topCeiling": "Top / Ceiling",
+ "nodes.cabinet.topDepth": "Top depth",
+ "nodes.cabinet.topFinish.none": "None",
+ "nodes.cabinet.topFinish.topCabinet": "Top Cabinet",
+ "nodes.cabinet.topFinish.trim": "Trim / Soffit",
+ "nodes.cabinet.topHeight": "Top height",
+ "nodes.cabinet.toolHints.place": "Place cabinet",
+ "nodes.cabinet.toolHints.placementType": "Placement type",
+ "nodes.cabinet.toolHints.typeCabinet": "Type: Cabinet",
+ "nodes.cabinet.toolHints.typeIsland": "Type: Island",
+ "nodes.constructionDimension.actions": "Actions",
+ "nodes.constructionDimension.centerMark": "Center mark",
+ "nodes.constructionDimension.datumPolicy": "Datum policy",
+ "nodes.constructionDimension.datumPolicyOptions.centerline": "Centerline",
+ "nodes.constructionDimension.datumPolicyOptions.finishFace": "Finish face",
+ "nodes.constructionDimension.datumPolicyOptions.structuralFace": "Structural face",
+ "nodes.constructionDimension.datumPolicyOptions.wallFace": "Wall face",
+ "nodes.constructionDimension.defaultFoundationDimensionName": "Foundation dimension",
+ "nodes.constructionDimension.dimension": "Dimension",
+ "nodes.constructionDimension.drawingCoordination": "Drawing coordination",
+ "nodes.constructionDimension.extensionGap": "Extension gap",
+ "nodes.constructionDimension.extensionOvershoot": "Extension overshoot",
+ "nodes.constructionDimension.fallbackTitle": "Construction Dimension",
+ "nodes.constructionDimension.featureCount": "Feature count",
+ "nodes.constructionDimension.foundationController": "Foundation controller",
+ "nodes.constructionDimension.imperialPrecision": "Imperial precision",
+ "nodes.constructionDimension.imperialPrecisionOptions.1": "Nearest inch",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/2": "Nearest 1/2 inch",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/4": "Nearest 1/4 inch",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/8": "Nearest 1/8 inch",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/16": "Nearest 1/16 inch",
+ "nodes.constructionDimension.linkedDimensionsNote": "Linked dimensions reuse the controller's associative anchors and update with it.",
+ "nodes.constructionDimension.metricNotation": "Metric notation",
+ "nodes.constructionDimension.metricNotationOptions.meters": "Meters",
+ "nodes.constructionDimension.metricNotationOptions.millimeters": "Millimeters",
+ "nodes.constructionDimension.mode": "Mode",
+ "nodes.constructionDimension.modeOptions.angular": "Angular",
+ "nodes.constructionDimension.modeOptions.arc-length": "Arc length",
+ "nodes.constructionDimension.modeOptions.center-mark": "Center mark",
+ "nodes.constructionDimension.modeOptions.chord": "Chord",
+ "nodes.constructionDimension.modeOptions.coordinate": "Coordinate",
+ "nodes.constructionDimension.modeOptions.diameter": "Diameter",
+ "nodes.constructionDimension.modeOptions.linear": "Linear",
+ "nodes.constructionDimension.modeOptions.radius": "Radius",
+ "nodes.constructionDimension.notation": "Notation",
+ "nodes.constructionDimension.noFoundationDimensions": "No foundation dimensions",
+ "nodes.constructionDimension.presentation": "{drawing} presentation",
+ "nodes.constructionDimension.presentationOptions.controlled": "Controlled by foundation",
+ "nodes.constructionDimension.presentationOptions.omit": "Omitted",
+ "nodes.constructionDimension.presentationOptions.shown": "Shown",
+ "nodes.constructionDimension.prefix": "Prefix",
+ "nodes.constructionDimension.primaryDrawing": "Primary drawing",
+ "nodes.constructionDimension.standards": "Standards",
+ "nodes.constructionDimension.suffix": "Suffix",
+ "nodes.constructionDimension.suppressedSegments": "{drawing} suppressed segments",
+ "nodes.constructionDimension.suppressedSegmentsNote": "Segment numbers are one-based and apply only in this drawing view.",
+ "nodes.constructionDimension.suppressedSegmentsPlaceholder": "e.g. 2, 4",
+ "nodes.constructionDimension.terminator": "Terminator",
+ "nodes.constructionDimension.terminatorOptions.architectural-tick": "Architectural tick",
+ "nodes.constructionDimension.terminatorOptions.dot": "Dot",
+ "nodes.constructionDimension.terminatorOptions.filled-arrow": "Filled arrow",
+ "nodes.constructionDimension.terminatorOptions.open-arrow": "Open arrow",
+ "nodes.constructionDimension.textOverride": "Text override",
+ "nodes.constructionDimension.textOverridePlaceholder": "Use measured value",
+ "nodes.constructionDimension.textPosition": "Text position",
+ "nodes.constructionDimension.textPositionOptions.above": "Above line",
+ "nodes.constructionDimension.textPositionOptions.centered": "Centered on line",
+ "nodes.constructionDimension.toolHints.pickWitness": "Pick witness point",
+ "nodes.constructionDimension.toolHints.finishWitnesses": "Finish multi-point witnesses",
+ "nodes.constructionDimension.toolHints.placeLine": "Place dimension line when needed",
+ "nodes.constructionDimension.toolHints.removeLast": "Remove last witness",
+ "nodes.ductFitting.toolHints.place": "Place fitting",
+ "nodes.ductFitting.toolHints.snap": "Snap onto the run",
+ "nodes.ductSegment.toolHints.start": "Start segment",
+ "nodes.ductSegment.toolHints.placeContinue": "Place and continue",
+ "nodes.ductSegment.toolHints.vertical": "Go vertical ↕, click to place",
+ "nodes.ductSegment.toolHints.diameter": "Duct diameter down / up",
+ "nodes.ductSegment.toolHints.trunk": "Round / rect trunk",
+ "nodes.ductSegment.toolHints.height": "Ceiling / floor height",
+ "nodes.ductTerminal.toolHints.place": "Place register",
+ "nodes.ductTerminal.toolHints.mount": "Mount: floor / ceiling / wall",
+ "nodes.ductTerminal.toolHints.rotate": "Rotate ±45° (floor / ceiling)",
+ "nodes.hvacEquipment.toolHints.place": "Place unit",
+ "nodes.leanToExtension.toolHints.place": "Place canopy or set the next run point",
+ "nodes.leanToExtension.toolHints.rotateFlip": "Rotate or flip the run side",
+ "nodes.leanToExtension.toolHints.cycleStyle": "Cycle mono / gable / butterfly",
+ "nodes.lineset.toolHints.start": "Start lineset",
+ "nodes.lineset.toolHints.place": "Place it (locked to 45°)",
+ "nodes.lineset.toolHints.vertical": "Go vertical ↕, click to place",
+ "nodes.liquidLine.toolHints.start": "Start liquid line",
+ "nodes.liquidLine.toolHints.place": "Place it (locked to 45°)",
+ "nodes.liquidLine.toolHints.vertical": "Go vertical ↕, click to place",
+ "nodes.liquidLine.toolHints.follow": "Follow: trace a lineset",
+ "nodes.pipeFitting.toolHints.place": "Place fitting",
+ "nodes.pipeFitting.toolHints.snap": "Snap onto the run",
+ "nodes.pipeSegment.toolHints.start": "Start run",
+ "nodes.pipeSegment.toolHints.place": "Place it (waste falls ¼″/ft)",
+ "nodes.pipeSegment.toolHints.wasteVent": "Waste / vent",
+ "nodes.pipeSegment.toolHints.size": "Pipe size down / up",
+ "nodes.pipeSegment.toolHints.vertical": "Vertical stack ↕, click to place",
+ "nodes.pipeTrap.toolHints.place": "Place trap",
+ "paint.material.defaultName": "Material {n}",
+ "paint.material.copySuffix": "{name} copy",
+ "paint.material.usedByOne": "Used by 1 part",
+ "paint.material.usedByOther": "Used by {count} parts",
+ "paint.material.paintWith": "Paint with",
+ "paint.erase": "Erase",
+ "paint.resetAll": "Reset all",
+ "paint.sceneMaterials": "Scene materials",
+ "paint.addMaterial": "Add material",
+ "paint.noCustomMaterials": "No custom materials yet — add one with +.",
+ "materialPicker.source.pascal": "Pascal",
+ "materialPicker.source.workspace": "Workspace",
+ "materialPicker.newMaterial": "New material",
+ "material.color": "Color",
+ "material.roughness": "Roughness",
+ "material.metalness": "Metalness",
+ "material.opacity": "Opacity",
+ "material.side": "Side",
+ "material.side.front": "Front",
+ "material.side.back": "Back",
+ "material.side.double": "Double",
+ "materials.colors": "Colors",
+ "materials.stone": "Stone",
+ "materials.brick": "Brick",
+ "materials.tile": "Tile",
+ "materials.wallpaper": "Wallpaper",
+ "materials.concrete": "Concrete",
+ "materials.metal": "Metal",
+ "materials.plastic": "Plastic",
+ "materials.fabric": "Fabric",
+ "materials.carpet": "Carpet",
+ "materials.leather": "Leather",
+ "materials.glass": "Glass",
+ "common.mixed": "Mixed",
+ "terrain.verb.raise": "Raise",
+ "terrain.verb.lower": "Lower",
+ "terrain.verb.flatten": "Flatten",
+ "terrain.verb.smooth": "Smooth",
+ "terrain.hint.raise": "Drag to raise the ground. One pass moves it up to {metres} m — release and drag again to go further.",
+ "terrain.hint.lower": "Drag to lower the ground. One pass moves it down to {metres} m.",
+ "terrain.hint.flatten": "Drag to level the ground toward the target height. It never overshoots.",
+ "terrain.hint.smooth": "Drag to soften slopes and remove ridges. Flat ground stays flat.",
+ "terrain.brush.size": "Size",
+ "terrain.brush.strength": "Strength",
+ "terrain.brush.softness": "Softness",
+ "terrain.brush.round": "Round",
+ "terrain.brush.square": "Square",
+ "terrain.flatten.target": "Target",
+ "terrain.flatten.pickTarget": "Pick target height from the ground",
+ "terrain.flatten.samplingHint": "Click the ground to pick its height as the target.",
+ "terrain.flatten.noTargetHint": "No target yet — the first click samples the ground under it.",
+ "terrain.flatten.targetHint": "Every flatten stroke levels toward this height.",
+ "terrain.levelLot": "Level lot",
+ "terrain.clearTerrain": "Clear terrain",
+ "materials.concrete-drywall.label": "Prepared Drywall",
+ "materials.concrete-plaster.label": "Painted Plaster",
+ "materials.concrete-plate.label": "Concrete Plate",
+ "materials.concrete-polished.label": "Polished Concrete",
+ "materials.concrete-raw.label": "Raw Concrete",
+ "materials.concrete-stucco.label": "White Stucco",
+ "materials.fabric-boucle.label": "Bouclé",
+ "materials.fabric-cotton.label": "Cotton",
+ "materials.fabric-linen.label": "Linen",
+ "materials.fabric-suede.label": "Suede",
+ "materials.fabric-velvet.label": "Velvet",
+ "materials.fabric-wool.label": "Wool",
+ "materials.flooring-agedbrick.label": "Aged Brick",
+ "materials.flooring-ceramic53.label": "Ceramic Mosaic",
+ "materials.flooring-darkceramic22.label": "Dark Ceramic Grunge",
+ "materials.flooring-garagedoor.label": "Garage Panel",
+ "materials.flooring-greenlabradorite.label": "Green Labradorite",
+ "materials.flooring-greenquartzitea.label": "Green Quartzite A",
+ "materials.flooring-ground13.label": "Earth Ground",
+ "materials.flooring-lightceramic24.label": "Light Ceramic Grunge",
+ "materials.flooring-pooltiles.label": "Pool Tiles",
+ "materials.flooring-rusticbrick.label": "Rustic Brick",
+ "materials.flooring-statuarettowhite.label": "Statuaretto White",
+ "materials.flooring-terrazzo19.label": "Terrazzo",
+ "materials.flooring-tile20.label": "Mosaic Tile",
+ "materials.flooring-tile68.label": "Pattern Tile",
+ "materials.flooring-tile79.label": "Stone Tile",
+ "materials.flooring-tile85a.label": "Quarry Tile",
+ "materials.flooring-tile86.label": "Terracotta Tile",
+ "materials.flooring-tiles3.label": "Checker Tiles",
+ "materials.flooring-tiles4.label": "Grid Tiles",
+ "materials.flooring-wallstone1.label": "Stone Wall",
+ "materials.flooring-weatheredbrick.label": "Weathered Brick",
+ "materials.flooring-woodenceramic2.label": "Wooden Ceramic 2",
+ "materials.flooring-woodenceramic3.label": "Wooden Ceramic 3",
+ "materials.flooring-woodparquet76.label": "Wood Parquet",
+ "materials.leather-black.label": "Black Leather",
+ "materials.leather-calf.label": "Calf Leather",
+ "materials.metal-brass.label": "Brass",
+ "materials.metal-chrome.label": "Chrome",
+ "materials.metal-copper.label": "Copper",
+ "materials.metal-polished.label": "Polished Metal",
+ "materials.metal-steel.label": "Brushed Steel",
+ "materials.preset-aubergine.label": "Aubergine",
+ "materials.preset-beige.label": "Beige",
+ "materials.preset-berry.label": "Berry",
+ "materials.preset-blush.label": "Blush",
+ "materials.preset-brickred.label": "Brick red",
+ "materials.preset-burntorange.label": "Burnt orange",
+ "materials.preset-charcoal.label": "Charcoal",
+ "materials.preset-clay.label": "Clay",
+ "materials.preset-cream.label": "Cream",
+ "materials.preset-deepteal.label": "Deep teal",
+ "materials.preset-dustyrose.label": "Dusty Rose",
+ "materials.preset-espresso.label": "Espresso",
+ "materials.preset-forest.label": "Forest",
+ "materials.preset-glass.label": "Glass",
+ "materials.preset-gold.label": "Gold",
+ "materials.preset-greige.label": "Greige",
+ "materials.preset-lavender.label": "Lavender",
+ "materials.preset-lightgrey.label": "Light grey",
+ "materials.preset-metal.label": "Metal",
+ "materials.preset-midgrey.label": "Mid grey",
+ "materials.preset-mint.label": "Mint",
+ "materials.preset-mustard.label": "Mustard",
+ "materials.preset-navy.label": "Navy",
+ "materials.preset-nearblack.label": "Near-black",
+ "materials.preset-ochre.label": "Ochre",
+ "materials.preset-olive.label": "Olive",
+ "materials.preset-oxblood.label": "Oxblood",
+ "materials.preset-paleteal.label": "Pale teal",
+ "materials.preset-paleyellow.label": "Pale yellow",
+ "materials.preset-peach.label": "Peach",
+ "materials.preset-petal.label": "Petal",
+ "materials.preset-plum.label": "Plum",
+ "materials.preset-powderblue.label": "Powder blue",
+ "materials.preset-rose.label": "Rose",
+ "materials.preset-royalblue.label": "Royal blue",
+ "materials.preset-sage.label": "Sage",
+ "materials.preset-sand.label": "Sand",
+ "materials.preset-sky.label": "Sky",
+ "materials.preset-slateblue.label": "Slate Blue",
+ "materials.preset-softblue.label": "Soft Blue",
+ "materials.preset-softwhite.label": "Soft White",
+ "materials.preset-tan.label": "Tan",
+ "materials.preset-taupe.label": "Taupe",
+ "materials.preset-teal.label": "Teal",
+ "materials.preset-terracotta.label": "Terracotta",
+ "materials.preset-tomato.label": "Tomato",
+ "materials.preset-white.label": "White",
+ "materials.roof-classicshingles.label": "Classic Shingles",
+ "materials.roof-claytiles.label": "Clay Tiles",
+ "materials.roof-terracottatiles.label": "Terracotta Tiles",
+ "materials.roof-weatheredshingles.label": "Weathered Shingles",
+ "materials.wood-finewood27.label": "Finewood 27",
+ "materials.wood-floorplank1.label": "Floor Plank 1",
+ "materials.wood-hungarianparquet10.label": "Hungarian Parquet 10",
+ "materials.wood-hungarianparquet2.label": "Hungarian Parquet 2",
+ "materials.wood-squareparquet21.label": "Square Parquet 21",
+ "materials.wood-squareparquet23.label": "Square Parquet 23",
+ "materials.wood-woodenparquet11.label": "Wooden Parquet 11",
+ "materials.wood-woodfine1.label": "Wood Fine 1",
+ "materials.wood-woodfine11.label": "Wood Fine 11",
+ "materials.wood-woodfine13.label": "Wood Fine 13",
+ "materials.wood-woodfine2.label": "Wood Fine 2",
+ "materials.wood-woodfine22.label": "Wood Fine 22",
+ "materials.wood-woodfine24.label": "Wood Fine 24",
+ "materials.wood-woodparquet121.label": "Wood Parquet 121",
+ "materials.wood-woodparquet14.label": "Wood Parquet 14",
+ "materials.wood-woodparquet56.label": "Wood Parquet 56",
+ "materials.wood-woodparquet65.label": "Wood Parquet 65",
+ "materials.wood-woodparquet99.label": "Wood Parquet 99",
+ "materials.wood-woodplank19.label": "Wood Plank 19",
+ "materials.wood-woodplank48.label": "Wood Plank 48",
+ "common.placement": "Placement",
+ "common.construction": "Construction",
+ "common.drainage": "Drainage",
+ "common.transform": "Transform",
+ "common.connections": "Connections",
+ "common.fitting": "Fitting",
+ "common.advanced": "Advanced",
+ "common.appearance": "Appearance",
+ "common.mounting": "Mounting",
+ "nodes.chimney.body": "Body",
+ "nodes.door.frame": "Frame",
+ "nodes.dormer.dormerRoof": "Dormer Roof",
+ "nodes.downspout.hardware": "Hardware",
+ "nodes.ductSegment.air": "Air",
+ "nodes.ductTerminal.terminal": "Terminal",
+ "nodes.ductTerminal.face": "Face",
+ "nodes.ductTerminal.collar": "Collar",
+ "nodes.fence.structure": "Structure",
+ "nodes.gutter.profile": "Profile",
+ "nodes.gutter.endCaps": "End Caps",
+ "nodes.gutter.hangers": "Hangers",
+ "nodes.hvacEquipment.equipment": "Equipment",
+ "nodes.hvacEquipment.cabinet": "Cabinet",
+ "nodes.hvacEquipment.supply": "Supply",
+ "nodes.hvacEquipment.return": "Return",
+ "nodes.leanToExtension.size": "Size",
+ "nodes.leanToExtension.connection": "Connection",
+ "nodes.leanToExtension.structure": "Structure",
+ "nodes.lineset.lines": "Lines",
+ "nodes.lineset.insulation": "Insulation",
+ "nodes.liquidLine.line": "Line",
+ "nodes.pipeTrap.trap": "Trap",
+ "nodes.shelf.topology": "Topology",
+ "nodes.skylight.type": "Type",
+ "nodes.skylight.curb": "Curb",
+ "nodes.skylight.opening": "Opening",
+ "nodes.skylight.lantern": "Lantern",
+ "nodes.solarPanel.grid": "Grid",
+ "nodes.solarPanel.panelDimensions": "Panel Dimensions",
+ "nodes.solarPanel.frame": "Frame",
+ "editor.deleteWithContents": "Delete with contents",
+ "editor.saveToCatalog": "Save to catalog",
+ "editor.replace": "Replace",
+ "editor.replacing": "Replacing...",
+ "editor.lock": "Lock",
+ "editor.unlock": "Unlock",
+ "editor.setScale": "Set Scale",
+ "editor.editScale": "Edit Scale",
+ "editor.showScale": "Show Scale",
+ "editor.hideScale": "Hide Scale",
+ "editor.uncalibrated": "Uncalibrated",
+ "editor.scaled": "Scaled",
+ "editor.scaledHidden": "Scaled (hidden)",
+ "editor.chooseImage": "Choose a PNG, JPEG, or WebP image.",
+ "editor.couldNotReplaceImage": "Could not replace that image.",
+ "editor.overlayImageUnavailable": "Overlay image unavailable. Replace the image to restore it.",
+ "editor.clickEndsOfKnownDistance": "Click both ends of a known distance on the plan, then type its real length.",
+ "editor.drawLineOverKnownDimension": "Draw a line over a known dimension on the plan, then type its real length to scale the image exactly.",
+ "editor.zoomOut": "Zoom out",
+ "editor.zoomIn": "Zoom in",
+ "editor.fitFloorPlan": "Fit floor plan",
+ "editor.floor": "Floor",
+ "editor.alignViewNorth": "Align view to north",
+ "editor.topView": "Top view",
+ "editor.orbitLeft": "Orbit left",
+ "editor.orbitRight": "Orbit right",
+ "editor.camera": "Camera",
+ "editor.visibility": "Visibility",
+ "editor.displaySettings": "Display settings",
+ "editor.walkthrough": "Walkthrough",
+ "editor.levelsLabel": "Levels: {mode}",
+ "editor.wallsLabel": "Walls: {mode}",
+ "editor.scans": "Scans",
+ "editor.guides": "Guides",
+ "editor.shadows": "Shadows",
+ "editor.render": "Render",
+ "editor.colors": "Colors",
+ "editor.theme": "Theme",
+ "editor.edges": "Edges",
+ "editor.stacked": "Stacked",
+ "editor.exploded": "Exploded",
+ "editor.solo": "Solo",
+ "editor.manual": "Manual",
+ "editor.fullHeight": "Full height",
+ "editor.cutaway": "Cutaway",
+ "editor.low": "Low",
+ "editor.viewer3d": "3D",
+ "editor.viewer2d": "2D",
+ "editor.viewerSplit": "Split",
+ "editor.perspective": "Perspective",
+ "editor.orthographic": "Orthographic",
+ "editor.solid": "Solid",
+ "editor.rendered": "Rendered",
+ "editor.solidDetail": "Flat and fast — no ambient occlusion",
+ "editor.renderedDetail": "Full ambient occlusion",
+ "editor.on": "On",
+ "editor.off": "Off",
+ "editor.monochrome": "Monochrome",
+ "editor.colored": "Colored",
+ "editor.cameraSnapshot": "Camera snapshot",
+ "editor.takeSnapshot": "Take snapshot",
+ "editor.updateSnapshot": "Update snapshot",
+ "editor.clearSnapshot": "Clear snapshot",
+ "editor.closeCaptureMode": "Close capture mode",
+ "editor.dwvRiserDiagram": "DWV riser diagram",
+ "editor.extrusionHeight": "Extrusion height",
+ "editor.viewerLayout": "Viewer layout",
+ "editor.noZonesOnLevel": "No zones on this level.",
+ "editor.addOne": "Add one",
+ "editor.selectLevelFirst": "Select a level to view and create zones",
+ "nodes.block.toolbar.transform": "Transform selected components (G / R / S)",
+ "nodes.block.toolbar.vertexSelect": "Vertex select (1)",
+ "nodes.block.toolbar.edgeSelect": "Edge select (2)",
+ "nodes.block.toolbar.faceSelect": "Face select (3)",
+ "nodes.block.toolbar.meshOperations": "Mesh operations",
+ "nodes.block.toolbar.operations": "Operations",
+ "nodes.block.toolbar.loopCut": "LOOP CUT",
+ "nodes.block.toolbar.bevel": "BEVEL",
+ "nodes.block.toolbar.moveSelection": "Move selection",
+ "nodes.block.toolbar.rotateSelection": "Rotate selection",
+ "nodes.block.toolbar.extrudeFaces": "Extrude selected faces",
+ "nodes.block.toolbar.insetFaces": "Inset selected faces",
+ "nodes.block.toolbar.loopCutAndSlide": "Loop Cut and Slide",
+ "nodes.block.toolbar.mergeVertices": "Merge vertices",
+ "nodes.block.toolbar.dissolve": "Dissolve selection",
+ "nodes.block.toolbar.bevelEdges": "Bevel selected edges",
+ "nodes.block.toolbar.finishEdit": "Finish edit mode (Tab)",
+ "nodes.block.toolbar.selectionAndMore": "Selection and more",
+ "nodes.block.toolbar.selectionActions": "Selection actions",
+ "nodes.block.toolbar.selectAll": "Select all",
+ "nodes.block.toolbar.invertSelection": "Invert selection",
+ "nodes.block.toolbar.clearSelection": "Clear selection",
+ "nodes.block.toolbar.xraySelection": "X-ray selection",
+ "nodes.block.toolbar.deleteComponents": "Delete components",
+ "nodes.block.loopCutCount": "Loop cut count",
+ "nodes.block.closeLastOperationPanel": "Close last operation panel",
+ "nodes.block.adjustLastOp": "Adjust {label} (F9)",
+ "nodes.skylight.glassThickness": "Glass Thickness",
+ "nodes.skylight.lanternHeight": "Lantern Height",
+ "nodes.skylight.topScale": "Top Scale",
+ "nodes.skylight.open": "Open",
+ "nodes.skylight.openingAngle": "Opening Angle",
+ "nodes.skylight.motorHousing": "Motor Housing",
+ "nodes.skylight.trackWidth": "Track Width",
+ "nodes.skylight.cutoutOffset": "Cutout Offset",
+ "nodes.cabinet.baseFlange": "Base Flange",
+ "nodes.cabinet.shelvesInside": "Shelves inside",
+ "nodes.cabinet.burnersOn": "Burners on",
+ "nodes.cabinet.topGrate": "Top grate",
+ "nodes.cabinet.baskets": "Baskets",
+ "nodes.cabinet.addLeft": "Add left",
+ "nodes.cabinet.addRight": "Add right",
+ "nodes.cabinet.showPlinth": "Show plinth",
+ "nodes.cabinet.plinthHeight": "Plinth height",
+ "nodes.cabinet.showCountertop": "Show countertop",
+ "nodes.cabinet.countertopHeight": "Countertop height",
+ "nodes.cabinet.countertopDepth": "Countertop depth",
+ "nodes.cabinet.seatingOverhang": "Seating overhang",
+ "nodes.cabinet.finishedBack": "Finished back",
+ "nodes.cabinet.waterfallEnds": "Waterfall ends",
+ "nodes.cabinet.barCounter": "Bar counter",
+ "nodes.cabinet.barHeight": "Bar height",
+ "nodes.cabinet.barDepth": "Bar depth",
+ "nodes.cabinet.sharedPlinthCountertop": "Shared Plinth & Countertop",
+ "nodes.cabinet.islandAndBar": "Island & Bar",
+ "nodes.cabinet.standardDimensions": "Standard dimensions",
+ "nodes.cabinet.appliesStandardDimensions": "Applies depth, carcass, plinth, and countertop thickness to this run.",
+ "nodes.dormer.addWindow": "Add Window",
+ "nodes.dormer.editWindow": "Edit window",
+ "nodes.dormer.moveWindow": "Move window",
+ "nodes.dormer.windowsTitle": "Windows ({count})",
+ "nodes.dormer.noWindows": "No windows",
+ "nodes.dormer.increaseWidth": "Increase the dormer width to add another window.",
+ "nodes.zone.architecturalRoom": "Architectural room",
+ "nodes.zone.roomName": "Room name",
+ "nodes.zone.roomNumber": "Room number",
+ "nodes.zone.enclosure": "Enclosure",
+ "nodes.zone.autoDetect": "Auto-detect",
+ "nodes.zone.enclosed": "Enclosed",
+ "nodes.zone.open": "Open",
+ "nodes.zone.occupancy": "Occupancy / use",
+ "nodes.zone.floorFinish": "Floor finish",
+ "nodes.zone.wallFinish": "Wall finish",
+ "nodes.zone.ceilingFinish": "Ceiling finish",
+ "nodes.zone.ceilingHeight": "Ceiling height",
+ "nodes.zone.clearDimensions": "Clear dimensions",
+ "nodes.zone.none": "None",
+ "nodes.zone.insideFaces": "Inside faces",
+ "nodes.zone.finishFaces": "Finish faces",
+ "nodes.zone.wallSurface": "Wall surface",
+ "nodes.zone.floorSurface": "Floor surface",
+ "nodes.zone.volume": "Volume",
+ "nodes.zone.roomDocumentation": "Room documentation",
+ "nodes.zone.roomQuantities": "Room quantities",
+ "nodes.zone.zoneQuantities": "Zone quantities",
+ "nodes.zone.enclosedRoom": "Enclosed room",
+ "nodes.zone.footprintOnly": "Footprint only",
+ "nodes.zone.boundaryUnavailable": "Zone boundary unavailable",
+ "nodes.zone.notProven": "Not proven",
+ "nodes.gutter.downspouts": "Downspouts",
+ "nodes.gutter.addDownspout": "Add Downspout",
+ "nodes.gutter.removeDownspout": "Remove downspout",
+ "nodes.door.documentation": "Documentation",
+ "nodes.window.documentation": "Documentation",
+ "nodes.spawn.actions": "Actions",
+ "nodes.ductFitting.swapWH": "Swap W/H",
+ "nodes.ductFitting.swapWidthHeight": "Swap width and height",
+ "nodes.boxVent.baseFlange": "Base Flange",
+ "nodes.shared.mark": "Mark",
+ "nodes.shared.construction": "Construction",
+ "nodes.shared.framed": "Framed",
+ "nodes.shared.masonry": "Masonry",
+ "nodes.shared.dimensionTo": "Dimension to",
+ "nodes.shared.nominal": "Nominal",
+ "nodes.shared.roughOpening": "Rough opening",
+ "nodes.shared.masonryOpening": "Masonry opening",
+ "nodes.shared.finishOpening": "Finish opening",
+ "nodes.shared.roWidth": "RO Width",
+ "nodes.shared.roHeight": "RO Height",
+ "nodes.shared.moWidth": "MO Width",
+ "nodes.shared.moHeight": "MO Height",
+ "nodes.shared.foWidth": "FO Width",
+ "nodes.shared.foHeight": "FO Height",
+ "nodes.shared.leaveBlankHint": "Leave RO, MO, and FO values blank until verified by the applicable manufacturer or trade.",
+ "nodes.shared.autoAssigned": "Auto-assigned",
+ "nodes.shared.verify": "Verify",
+ "panel.section.actions": "Actions",
+ "panel.section.position": "Position",
+ "panel.section.rotation": "Rotation",
+ "panel.section.scaleOpacity": "Scale & Opacity",
+ "editor.edgeOff": "Off",
+ "editor.edgeSoft": "Soft",
+ "editor.edgeStrong": "Strong",
+ "editor.edgeOffDetail": "No edge lines",
+ "editor.edgeSoftDetail": "Faint outline of major creases",
+ "editor.edgeStrongDetail": "Crisp, opaque edge lines",
+ "nodes.skylight.frame": "Frame",
+ "nodes.cabinet.modules": "Modules",
+ "nodes.cabinet.style": "Style",
+ "nodes.cabinet.back": "Back",
+ "nodes.cabinet.left": "Left",
+ "nodes.cabinet.right": "Right",
+ "common.shelves": "Shelves",
+ "common.drawers": "Drawers",
+ "nodes.zone.occupancyUse": "Occupancy / use",
+ "nodes.zone.topViewAriaLabel": "Top view with zone edge dimensions",
+ "nodes.gutter.downspoutDefaultName": "Downspout {index}",
+ "nodes.elevator.levelFallback": "Level {n}",
+ "nodes.measurement.extrusionHeight": "Extrusion height",
+ "common.dismiss": "Dismiss",
+ "editor.riserDiagram": "DWV riser diagram",
+ "editor.followsLevel": "Follows level",
+ "editor.customHeight": "Custom height",
+ "editor.currentlyLabel": "Currently {value}",
+ "editor.capture": "Capture",
+ "editor.top": "Top",
+ "buildTab.roofSource.conicalHint": "Select a curved wall to match its radius and arc.",
+ "nodes.roof.toolHints.placementTooltip": "Placement surface — click or press P to cycle",
+ "nodes.roof.defaultName": "Roof {count}",
+ "nodes.roof.preview": "Roof preview"
+}
diff --git a/packages/editor/src/lib/i18n/zh.json b/packages/editor/src/lib/i18n/zh.json
new file mode 100644
index 0000000000..f8face6fdb
--- /dev/null
+++ b/packages/editor/src/lib/i18n/zh.json
@@ -0,0 +1,2304 @@
+{
+ "buildTab.tile.wall": "墙体",
+ "buildTab.tile.fence": "围栏",
+ "buildTab.tile.slab": "楼板",
+ "buildTab.tile.ceiling": "天花板",
+ "buildTab.tile.roof": "屋顶",
+ "buildTab.tile.stairs": "楼梯",
+ "buildTab.tile.elevator": "电梯",
+ "buildTab.tile.door": "门",
+ "buildTab.tile.window": "窗户",
+ "buildTab.tile.column": "柱子",
+ "buildTab.tile.shelf": "搁板",
+ "buildTab.tile.spawn": "生成点",
+ "buildTab.tile.kitchen": "厨房",
+ "buildTab.tile.mep": "机电",
+ "buildTab.tile.painting": "涂装",
+ "buildTab.tile.terrain": "地形",
+ "buildTab.mep.duct": "风管",
+ "buildTab.mep.register": "送风口",
+ "buildTab.mep.hvacUnit": "空调机组",
+ "buildTab.mep.lineset": "冷媒管",
+ "buildTab.mep.liquidLine": "液管",
+ "buildTab.mep.dwvPipe": "排水管",
+ "buildTab.mep.modularCabinet": "模块化橱柜",
+ "buildTab.section.roofType": "屋顶类型",
+ "buildTab.section.createFrom": "基于",
+ "buildTab.section.featuresAndExtensions": "特征与延伸",
+ "buildTab.roofType.hip": "四坡",
+ "buildTab.roofType.gable": "双坡",
+ "buildTab.roofType.shed": "单坡",
+ "buildTab.roofType.flat": "平顶",
+ "buildTab.roofType.gambrel": "复折",
+ "buildTab.roofType.dutch": "荷兰式",
+ "buildTab.roofType.mansard": "孟莎式",
+ "buildTab.roofType.conical": "锥形",
+ "buildTab.roofSource.room": "房间",
+ "buildTab.roofSource.walls": "墙体",
+ "buildTab.roofSource.draw": "绘制",
+ "buildTab.roofSource.roomHint": "悬停房间以预览其轮廓,然后点击放置。",
+ "buildTab.roofSource.wallsHint": "选择一段弧形墙以匹配其半径和弧度。",
+ "buildTab.roofSource.drawHint": "用两次点击绘制屋顶轮廓。",
+ "buildTab.action.addFitting": "添加管件",
+ "buildTab.action.addTrap": "添加存水弯",
+ "buildTab.liquidLine.followLineset": "跟随冷媒管",
+ "buildTab.liquidLine.followHintOn": "点击冷媒管以在其旁边铺设线路。",
+ "buildTab.liquidLine.followHintOff": "在已有冷媒管旁边绘制线路(F)。",
+ "buildTab.toggle.on": "开",
+ "buildTab.toggle.off": "关",
+ "catalog.intensity": "强度",
+ "materials.flooring": "地板",
+ "materials.other": "其他",
+ "materials.roof": "屋顶",
+ "materials.wood": "木材",
+ "measurement.angle": "角度",
+ "measurement.angularDimension": "角度尺寸",
+ "measurement.arcLength": "弧长",
+ "measurement.area": "面积",
+ "measurement.centerMark": "中心标记",
+ "measurement.chordDimension": "弦长尺寸",
+ "measurement.continuousDimension": "连续尺寸",
+ "measurement.coordinateDimensions": "坐标尺寸",
+ "measurement.diameterDimension": "直径尺寸",
+ "measurement.distance": "距离",
+ "measurement.floorplanSection": "平面图",
+ "measurement.linearDimension": "线性尺寸",
+ "measurement.measurePrefix": "测量:{label}",
+ "measurement.options": "测量选项",
+ "measurement.perimeter": "周长",
+ "measurement.radiusDimension": "半径尺寸",
+ "measurement.smart": "智能",
+ "measurement.type": "测量类型",
+ "measurement.volume": "体积",
+ "commands.addLevel": "添加楼层",
+ "commands.back": "返回",
+ "commands.cameraSwitchTo": "相机:切换到 {mode}",
+ "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.export3DModel": "导出3D模型 (GLB)",
+ "commands.exportJSON": "导出场景 (JSON)",
+ "commands.exportSceneJson": "导出场景 (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.orthographic": "正交",
+ "commands.perspective": "透视",
+ "commands.redo": "重做",
+ "commands.renameLevel": "重命名楼层",
+ "commands.renameTo": "重命名为 {name}",
+ "commands.sculptTerrain": "雕刻地形",
+ "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.translucent": "半透明",
+ "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.diameter": "直径",
+ "common.dimensions": "尺寸",
+ "common.area": "面积",
+ "common.directions.down": "下",
+ "common.directions.left": "左",
+ "common.directions.right": "右",
+ "common.directions.up": "上",
+ "common.done": "完成",
+ "common.duplicate": "复制",
+ "common.edit": "编辑",
+ "common.error": "错误",
+ "common.height": "高度",
+ "common.hide": "隐藏",
+ "common.home": "首页",
+ "common.length": "长度",
+ "common.loading": "加载中...",
+ "common.minus45": "-45°",
+ "common.move": "移动",
+ "common.next": "下一步",
+ "common.noResults": "未找到结果",
+ "common.objectsSelected": "个对象已选择",
+ "common.open": "打开",
+ "common.overhang": "挑檐",
+ "common.paste": "粘贴",
+ "common.plus45": "+45°",
+ "common.position": "位置",
+ "common.redo": "重做",
+ "common.remove": "移除",
+ "common.reset": "重置",
+ "common.rotate": "旋转",
+ "common.save": "保存",
+ "common.search": "搜索",
+ "common.select": "选择",
+ "common.settings": "设置",
+ "common.show": "显示",
+ "common.style": "样式",
+ "common.success": "成功",
+ "common.thickness": "厚度",
+ "common.undo": "撤销",
+ "common.untitled": "未命名",
+ "common.width": "宽度",
+ "common.x": "X",
+ "common.y": "Y",
+ "common.z": "Z",
+ "common.yaw": "偏航",
+ "common.rotation": "旋转",
+ "controlModes.boxSelect": "框选",
+ "controlModes.build": "构建",
+ "controlModes.editSite": "编辑场地",
+ "controlModes.exitSiteEditing": "退出场地编辑",
+ "controlModes.furnish": "家具",
+ "controlModes.materialPaint": "材质绘制",
+ "controlModes.preview": "预览",
+ "controlModes.select": "选择",
+ "controlModes.siteEditGroundOnly": "场地编辑仅在地面层可用",
+ "controlModes.zone": "区域",
+ "contextualHelp.mep.fitting.rotate45": "旋转 ±45°",
+ "contextualHelp.mep.fitting.showHandles": "点击手柄点以显示移动 + 旋转手柄",
+ "contextualHelp.mep.fitting.switchAxis": "切换旋转轴(Y → X → Z)",
+ "contextualHelp.mep.run.detachJoint": "拖动箭头时分离接缝",
+ "contextualHelp.mep.run.showArrows": "点击手柄点以显示移动箭头",
+ "contextualHelp.measurement.exit": "退出智能测量",
+ "contextualHelp.measurement.inspect": "查看表面尺寸",
+ "contextualHelp.measurement.pin": "钉住测量镜头",
+ "contextualHelp.reshape.controlPoint": "移动控制点",
+ "contextualHelp.reshape.corner": "移动角点",
+ "contextualHelp.reshape.curve": "调整曲线",
+ "contextualHelp.reshape.endpoint": "移动端点",
+ "contextualHelp.reshape.tangent": "移动切线",
+ "contextualHelp.resize": "调整大小",
+ "contextualHelp.rotateHandle.active": "自由旋转中(无角度步进)",
+ "contextualHelp.rotateHandle.idle": "按住以自由旋转",
+ "contextualHelp.sculpt.brushSize": "笔刷大小",
+ "contextualHelp.sculpt.cancelPick": "取消取样",
+ "contextualHelp.sculpt.cancelStroke": "取消笔刷",
+ "contextualHelp.sculpt.flatten": "平整地面",
+ "contextualHelp.sculpt.lower": "降低地面",
+ "contextualHelp.sculpt.pickTarget": "选择目标高度",
+ "contextualHelp.sculpt.raise": "抬升地面",
+ "contextualHelp.sculpt.smooth": "平滑地面",
+ "contextualHelp.select.addOrRemove": "从选中中添加或移除对象",
+ "contextualHelp.select.clearSelection": "清除选中(或点击空白处)",
+ "contextualHelp.select.groupSelection": "编组选中(仅本次会话)",
+ "contextualHelp.select.moveAsOne": "点击或拖动选区以整体移动",
+ "contextualHelp.select.rotateSelection": "将选中旋转 ±45°",
+ "contextualHelp.select.ungroupSelection": "取消编组本次会话的选中",
+ "contextualHelp.single.movableDrag": "拖动选中的可移动对象",
+ "contextualHelp.single.rotatableDrag": "向左或向右拖动以旋转选中对象",
+ "dialog.deleteElements.title": "删除 {count} 个元素?",
+ "dialog.deleteElements.description": "这将删除所有选中的元素。只要删除操作仍在编辑器历史记录中,你就可以撤销。",
+ "editor.cameraPan": "平移",
+ "editor.cameraRotate": "旋转",
+ "editor.cameraZoom": "缩放",
+ "editor.create": "创建",
+ "editor.createNew": "创建新场景",
+ "editor.recent": "最近",
+ "editor.delete": "删除",
+ "editor.cancel": "取消",
+ "editor.clearScale": "清除比例",
+ "editor.center": "居中",
+ "editor.resetRotation": "重置旋转",
+ "editor.resetImageScale": "重置图片比例",
+ "editor.opacity": "不透明度",
+ "editor.move": "移动",
+ "editor.toggleSidebar": "切换侧边栏",
+ "editor.close": "关闭",
+ "editor.duplicate": "复制",
+ "editor.editProperties": "编辑属性",
+ "editor.cameraControlsHint": "相机控制提示",
+ "editor.dismissCameraControlsHint": "关闭相机控制提示",
+ "editor.jump": "跳跃",
+ "editor.sprint": "冲刺",
+ "editor.interact": "交互",
+ "editor.clickToLookAround": "点击环顾四周",
+ "editor.exitStreetView": "退出街景",
+ "editor.referenceFloor": "参考楼层",
+ "editor.setCorner": "设置角落",
+ "editor.forcePlace": "强制放置",
+ "editor.guidedConstraintsBypassed": "已绕过引导约束",
+ "editor.openSavedScenes": "场景页",
+ "editor.place": "放置",
+ "editor.placeBuilding": "放置建筑",
+ "editor.rotate": "旋转",
+ "editor.rotateCounterclockwise": "逆时针旋转",
+ "editor.rotateClockwise": "顺时针旋转",
+ "editor.sceneFailedToRender": "编辑器场景渲染失败",
+ "editor.setOverlayScale": "设置叠加层比例",
+ "editor.somethingWentWrong": "出现错误",
+ "editor.addNewLevel": "添加新楼层",
+ "editor.displaysMobileSidebar": "显示移动端侧边栏。",
+ "editor.material": "材质",
+ "editor.customMaterial": "自定义材质",
+ "editor.image": "图片",
+ "editor.3dScan": "3D扫描",
+ "editor.guideImage": "参考图",
+ "editor.referenceScale": "参考比例",
+ "editor.quickActions": "快捷操作",
+ "editor.position": "位置",
+ "editor.rotation": "旋转",
+ "editor.scaleAndOpacity": "比例与不透明度",
+ "editor.actions": "操作",
+ "editor.expandSidebar": "展开侧边栏",
+ "editor.custom": "自定义",
+ "editor.addPoint": "添加点",
+ "editor.viewSnapshot": "查看快照",
+ "editor.reloadEditor": "重新加载编辑器",
+ "editor.drawnLine": "绘制线",
+ "editor.realLength": "实际长度",
+ "editor.duplicateLevel": "复制楼层",
+ "editor.tryAgain": "重试",
+ "editor.dismiss": "关闭",
+ "editor.reload": "重新加载",
+ "editor.conflictTitle": "另一个会话已先保存 — 是否刷新?",
+ "editor.conflictBody": "您的更改尚未保存。请重新加载以获取最新版本。",
+ "editor.lightPreview": "轻量预览",
+ "editor.lightPreviewTitle": "跳过后期处理流程 — 减少 GPU 负载,无环境光遮蔽或选择高亮",
+ "editor.leftClick": "左键点击",
+ "editor.localWarning": "这里是空白画布 — 你保存的场景在",
+ "editor.middleClick": "中键点击",
+ "editor.openRecent": "打开最近场景",
+ "editor.rightClick": "右键点击",
+ "editor.scrollWheel": "滚轮",
+ "editor.space": "空格",
+ "furnishTools.appliance": "家电",
+ "furnishTools.bathroom": "浴室",
+ "furnishTools.furniture": "家具",
+ "furnishTools.kitchen": "厨房",
+ "furnishTools.outdoor": "户外",
+ "functionTree.bathroom": "浴室",
+ "functionTree.bedroom": "卧室",
+ "functionTree.dining": "餐厅",
+ "functionTree.entry": "玄关",
+ "functionTree.hvac": "暖通空调",
+ "functionTree.kitchen": "厨房",
+ "functionTree.laundry": "洗衣房",
+ "functionTree.lighting": "照明",
+ "functionTree.living": "客厅",
+ "functionTree.office": "书房",
+ "functionTree.outdoor": "户外",
+ "functionTree.plumbing": "水管",
+ "functionTree.storage": "储物",
+ "itemHelper.freePlace": "自由放置",
+ "itemHelper.placeItem": "放置物品",
+ "itemHelper.rotateClockwise": "顺时针旋转",
+ "itemHelper.rotateCounterclockwise": "逆时针旋转",
+ "helper.continuation.line": "连续:{label}",
+ "helper.continuation.tooltip": "连续 — 点击或按 C 切换",
+ "helper.fence.continuationCurved": "弯曲围栏模式下直线连续不可用",
+ "helper.fence.continuationLine": "围栏连续:{label}",
+ "helper.fence.continuationStraight": "直线围栏连续 — 点击或按 C 切换",
+ "helper.fence.curved": "弯曲",
+ "helper.fence.finishCurve": "完成曲线(或双击)",
+ "helper.fence.straight": "直线",
+ "helper.fence.straightContinuous": "直线:连续",
+ "helper.fence.straightSingle": "直线:单段",
+ "helper.fence.typeCurved": "类型:弯曲",
+ "helper.fence.typeLine": "围栏类型:{kind}",
+ "helper.fence.typeStraight": "类型:直线",
+ "helper.fence.typeTooltip": "围栏类型 — 点击或按 T 在直线与曲线之间切换",
+ "helper.gridStep.label": "网格:{step} m",
+ "helper.gridStep.line": "网格步长:{step} m",
+ "helper.gridStep.tooltip": "网格步长 — 点击或按 Ctrl 切换",
+ "helper.paint.allMatching": "全部匹配",
+ "helper.paint.hoverSurface": "悬停一个表面进行绘制",
+ "helper.paint.line": "绘制:{scope}",
+ "helper.paint.pickMaterial": "选择一种材料进行绘制",
+ "helper.paint.room": "房间",
+ "helper.paint.scopeLine": "绘制范围:{scope}",
+ "helper.paint.scopeTooltip": "绘制范围 — 点击或按 Shift 切换",
+ "helper.paint.thisSurface": "此表面",
+ "helper.paint.wholeNoun": "整个{noun}",
+ "helper.snapping.angles": "角度",
+ "helper.snapping.grid": "网格",
+ "helper.snapping.lines": "线",
+ "helper.snapping.line": "捕捉:{mode}",
+ "helper.snapping.off": "关闭",
+ "helper.snapping.tooltip": "捕捉模式 — 点击或按 Shift 切换",
+ "items.all": "全部",
+ "items.community": "社区",
+ "keys.commandMac": "Command",
+ "keys.control": "Control",
+ "keys.leftClick": "左键点击",
+ "keys.middleClick": "中键点击",
+ "keys.rightClick": "右键点击",
+ "keys.shift": "Shift",
+ "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.levelHeight": "楼层高度",
+ "level.noElementsOnLevel": "此楼层上没有元素",
+ "level.noZonesOnLevel": "此楼层上没有区域。",
+ "level.pasteCopied": "粘贴已复制内容",
+ "level.reorder": "重新排序",
+ "level.thisLevel": "此楼层",
+ "level.reorderName": "重新排序 {name}",
+ "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.errors_plural": "{count} 个错误",
+ "loadBuild.floorArea": "地板面积",
+ "loadBuild.hide": "隐藏",
+ "loadBuild.invalidJson": "无法将文件解析为 JSON。",
+ "loadBuild.items": "物品",
+ "loadBuild.levels": "楼层",
+ "loadBuild.nodes": "{count} 个节点",
+ "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.warnings_plural": "{count} 个警告",
+ "loadBuild.windows": "窗",
+ "loadBuild.zones": "区域",
+ "nav.home": "首页",
+ "nav.privacy": "隐私",
+ "nav.scenes": "场景",
+ "nav.terms": "条款",
+ "nodeActions.curve": "曲线",
+ "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": "天花板",
+ "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.boxVent": "屋顶通风口",
+ "nodes.roofSegment.hip": "四坡",
+ "nodes.roofSegment.gable": "山墙",
+ "nodes.roofSegment.shed": "棚式",
+ "nodes.roofSegment.flat": "平顶",
+ "nodes.roofSegment.gambrel": "双坡",
+ "nodes.roofSegment.dutch": "荷兰式",
+ "nodes.roofSegment.mansard": "曼萨尔式",
+ "nodes.roofSegment.conical": "锥形",
+ "nodes.roofSegment.conicalShape": "锥形形状",
+ "nodes.roofSegment.clippedVersion": "裁剪版",
+ "nodes.roofSegment.startAngle": "起始角",
+ "nodes.roofSegment.arc": "弧度",
+ "nodes.roofSegment.angle": "角度",
+ "nodes.roofSegment.editFootprint": "编辑底部投影",
+ "nodes.roofSegment.autoRidgeVent": "自动屋脊通风口",
+ "nodes.roofSegment.autoGutters": "自动排水沟",
+ "nodes.roofSegment.kinkDepth": "折角深度",
+ "nodes.roofSegment.kinkHeight": "折角高度",
+ "nodes.roofSegment.waistWidth": "腰部宽度",
+ "nodes.roofSegment.waistHeight": "腰部高度",
+ "nodes.roofSegment.waistLength": "腰部长度",
+ "nodes.roofSegment.topRakeThickness": "顶部斜面厚度",
+ "nodes.roofSegment.topRakeLength": "顶部斜面长度",
+ "nodes.roofSegment.wallThickness": "墙厚",
+ "nodes.roofSegment.deckThickness": "板厚",
+ "nodes.roofSegment.shingleThickness": "瓦片厚度",
+ "nodes.stairSegment.flight": "梯段",
+ "nodes.stairSegment.landing": "平台",
+ "nodes.stairSegment.front": "前",
+ "nodes.stairSegment.left": "左",
+ "nodes.stairSegment.right": "右",
+ "nodes.elevator.glass": "玻璃",
+ "nodes.ridgeVent.standard": "标准",
+ "nodes.ridgeVent.shingled": "瓦片",
+ "nodes.ridgeVent.flanged": "法兰",
+ "nodes.ridgeVent.endCaps": "端帽",
+ "nodes.ridgeVent.open": "开放",
+ "nodes.skylight.top": "上",
+ "nodes.skylight.bottom": "下",
+ "nodes.skylight.left": "左",
+ "nodes.skylight.right": "右",
+ "nodes.skylight.motor": "电机",
+ "nodes.skylight.skylight": "天窗",
+ "nodes.skylight.noMotor": "无电机",
+ "nodes.skylight.alongZ": "沿Z轴",
+ "nodes.skylight.alongX": "沿X轴",
+ "nodes.skylight.yes": "是",
+ "nodes.skylight.no": "否",
+ "common.yes": "是",
+ "common.no": "否",
+ "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.defaultName": "屋顶通风口 {count}",
+ "nodes.boxVent.topTaper": "顶部锥度",
+ "nodes.boxVent.style": "样式",
+ "nodes.boxVent.toolHints.cancel": "取消",
+ "nodes.boxVent.toolHints.place": "放置通风口",
+ "nodes.building.description": "包含一个或多个楼层的建筑容器。",
+ "nodes.building.label": "建筑",
+ "nodes.ceiling.addHole": "添加切口",
+ "nodes.ceiling.auto": "自动",
+ "nodes.ceiling.autoHoleLabel.elevator": "自动电梯切口",
+ "nodes.ceiling.autoHoleLabel.stair": "自动楼梯切口",
+ "nodes.ceiling.customHeight": "自定义高度",
+ "nodes.ceiling.currently": "当前 {value}",
+ "nodes.ceiling.editing": "编辑中",
+ "nodes.ceiling.followsLevel": "跟随楼层",
+ "nodes.ceiling.height": "高度",
+ "nodes.ceiling.heightPresets.high": "高 (3.0m)",
+ "nodes.ceiling.heightPresets.highImperial": "高 (9英尺)",
+ "nodes.ceiling.heightPresets.low": "低 (2.4m)",
+ "nodes.ceiling.heightPresets.lowImperial": "低 (8英尺)",
+ "nodes.ceiling.heightPresets.standard": "标准 (2.5m)",
+ "nodes.ceiling.heightPresets.standardImperial": "标准 (8.5英尺)",
+ "nodes.ceiling.holeLabel": "切口 {index}",
+ "nodes.ceiling.holes": "切口",
+ "nodes.ceiling.info": "信息",
+ "nodes.ceiling.limitedBy": "受限于楼层的 {available} — 若需要更高的天花板,请先调高楼层层高。",
+ "nodes.ceiling.manual": "手动",
+ "nodes.ceiling.noHoles": "无切口",
+ "nodes.ceiling.tooTall": "超过本楼层上限(可用 {available})。请先调高楼层层高。",
+ "nodes.chimney.aboveRidge": "高过屋脊",
+ "nodes.chimney.back": "后",
+ "nodes.chimney.bandExtent": "范围",
+ "nodes.chimney.bandHeight": "高度",
+ "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.width": "宽度",
+ "nodes.chimney.cricket": "斜撑",
+ "nodes.chimney.cricketHeight": "高度",
+ "nodes.chimney.cricketLength": "长度",
+ "nodes.chimney.cutoutOffset": "切口偏移",
+ "nodes.chimney.diameter": "直径",
+ "nodes.chimney.double": "双个",
+ "nodes.chimney.flat": "平顶",
+ "nodes.chimney.flueCount": "数量",
+ "nodes.chimney.flueDiameter": "直径",
+ "nodes.chimney.flueHeight": "高度",
+ "nodes.chimney.flueShape": "形状",
+ "nodes.chimney.flueSpacing": "间距",
+ "nodes.chimney.flueWallThickness": "壁厚",
+ "nodes.chimney.wallThickness": "壁厚",
+ "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.chimney.chimney": "烟囱",
+ "nodes.chimney.chimneyType": "烟囱类型",
+ "nodes.chimney.offset": "偏移",
+ "nodes.chimney.footprint": "底部投影",
+ "nodes.chimney.style": "样式",
+ "nodes.column.aFrame": "A型框架",
+ "nodes.column.applyPreset": "应用预设...",
+ "nodes.column.applyProportion": "应用比例...",
+ "nodes.column.preset": "预设",
+ "nodes.column.shape": "形状",
+ "nodes.column.transform": "变换",
+ "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.column.dimensions": "尺寸",
+ "nodes.column.shaft": "柱身",
+ "nodes.column.ends": "端部",
+ "nodes.column.slender": "纤细",
+ "nodes.column.standard": "标准",
+ "nodes.column.heavy": "粗壮",
+ "nodes.column.shortStout": "矮胖",
+ "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.defaultName": "窗户 {count}",
+ "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.pitchDirection": "坡向",
+ "nodes.dormer.pitchRise": "坡度增量",
+ "nodes.dormer.rect": "矩形",
+ "nodes.dormer.riseBack": "高在后侧",
+ "nodes.dormer.riseFront": "高在前侧",
+ "nodes.dormer.roofHeight": "屋顶高度",
+ "nodes.dormer.rounded": "圆角",
+ "nodes.dormer.roofType": "屋顶类型",
+ "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.cabDepth": "轿厢深度",
+ "nodes.elevator.cabHeight": "轿厢高度",
+ "nodes.elevator.cabWidth": "宽度",
+ "nodes.elevator.centerOpening": "中部开口",
+ "nodes.elevator.defaultFloor": "默认楼层",
+ "nodes.elevator.disabled": "禁用",
+ "nodes.elevator.doorHeight": "门高",
+ "nodes.elevator.solid": "实体",
+ "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.elevator.yaw": "偏航",
+ "nodes.elevator.access": "通道",
+ "nodes.elevator.cab": "轿厢",
+ "nodes.elevator.destination": "目标",
+ "nodes.elevator.copy": "{name}副本",
+ "nodes.elevator.defaultCopy": "电梯副本",
+ "nodes.elevator.doors": "门",
+ "nodes.elevator.motion": "运动",
+ "nodes.elevator.offset": "偏移",
+ "nodes.elevator.rotation": "旋转",
+ "nodes.elevator.rotationPresets.negative": "-45°",
+ "nodes.elevator.rotationPresets.positive": "+45°",
+ "nodes.elevator.shaft": "井道",
+ "nodes.elevator.stopLabel": "停靠 {n}",
+ "nodes.elevator.serviceButton": "服务",
+ "nodes.elevator.disabledButton": "禁用",
+ "nodes.fence.description": "具有可配置柱和填充的直栏杆或曲线栏杆段。",
+ "nodes.fence.label": "栏杆",
+ "nodes.fence.toolHints.allowAngles": "允许非45°角",
+ "nodes.fence.toolHints.cancel": "取消",
+ "nodes.fence.toolHints.setStartEnd": "设置栏杆起点/终点",
+ "nodes.measurement.toolHints.finish": "完成测量",
+ "nodes.measurement.toolHints.finishContinue": "完成并继续",
+ "nodes.measurement.toolHints.placePoint": "放置测量点",
+ "nodes.measurement.toolHints.removeLast": "移除上一个点",
+ "nodes.level.description": "建筑的单个楼层,包含墙体/楼板/天花板/物品。",
+ "nodes.level.label": "楼层",
+ "nodes.roof.addChimney": "添加烟囱",
+ "nodes.roof.addCupola": "添加穹顶天窗",
+ "nodes.roof.addDormer": "添加老虎窗",
+ "nodes.roof.addEyebrowVent": "添加弧形通风口",
+ "nodes.roof.addGutter": "添加檐沟",
+ "nodes.roof.addSegment": "添加分段",
+ "nodes.roof.addSkylight": "添加天窗",
+ "nodes.roof.addSolarPanel": "添加太阳能板",
+ "nodes.roof.addVent": "添加通风口",
+ "nodes.roof.box": "箱形",
+ "nodes.roof.defaultName.boxVent": "屋顶通风口 {count}",
+ "nodes.roof.defaultName.chimney": "烟囱 {count}",
+ "nodes.roof.defaultName.dormer": "老虎窗 {count}",
+ "nodes.roof.defaultName.gutter": "檐沟 {count}",
+ "nodes.roof.defaultName.ridgeVent": "屋脊通风口 {count}",
+ "nodes.roof.defaultName.segment": "屋顶段 {count}",
+ "nodes.roof.defaultName.skylight": "天窗 {count}",
+ "nodes.roof.defaultName.solarPanel": "太阳能板 {count}",
+ "nodes.roof.defaultName.turbineVent": "涡轮通风口 {count}",
+ "nodes.roof.elements": "附属构件",
+ "nodes.roof.fallbackTitle": "屋顶",
+ "nodes.roof.kindLabel.boxVent": "屋顶通风口",
+ "nodes.roof.kindLabel.chimney": "烟囱",
+ "nodes.roof.kindLabel.dormer": "老虎窗",
+ "nodes.roof.kindLabel.gutter": "檐沟",
+ "nodes.roof.kindLabel.ridgeVent": "屋脊通风口",
+ "nodes.roof.kindLabel.skylight": "天窗",
+ "nodes.roof.kindLabel.solarPanel": "太阳能板",
+ "nodes.roof.kindLabel.turbineVent": "涡轮通风口",
+ "nodes.roof.ridge": "屋脊",
+ "nodes.roof.segments": "屋顶段",
+ "nodes.roof.turbine": "涡轮",
+ "nodes.roofSegment.fallbackTitle": "屋顶段",
+ "nodes.roofSegment.roofType": "屋顶类型",
+ "nodes.roofSegment.footprint": "底部投影",
+ "nodes.roofSegment.wallHeight": "墙高",
+ "nodes.roofSegment.pitch": "坡度",
+ "nodes.roofSegment.shape": "形状",
+ "nodes.roofSegment.structure": "结构",
+ "nodes.roofSegment.trim": "修剪",
+ "nodes.roofSegment.drainage": "排水",
+ "nodes.slab.addHole": "添加切口",
+ "nodes.slab.area": "面积",
+ "nodes.slab.auto": "自动",
+ "nodes.slab.autoHoleLabel.elevator": "自动电梯切口",
+ "nodes.slab.autoHoleLabel.stair": "自动楼梯切口",
+ "nodes.slab.base": "底部",
+ "nodes.slab.depth": "深度",
+ "nodes.slab.description": "承载物品的多边形地板表面。",
+ "nodes.slab.editing": "(编辑中)",
+ "nodes.slab.elevationPresets.ground": "地面 (0m)",
+ "nodes.slab.elevationPresets.raised": "抬起 (+5cm)",
+ "nodes.slab.elevationPresets.standard": "标准 (5cm)",
+ "nodes.slab.elevationPresets.step": "台阶 (+15cm)",
+ "nodes.slab.elevationPresets.sunken": "下沉 (-15cm)",
+ "nodes.slab.elevationPresets.thick": "加厚 (15cm)",
+ "nodes.slab.elevationPresets.thin": "薄型 (2cm)",
+ "nodes.slab.fallbackTitle": "楼板",
+ "nodes.slab.fixed": "固定",
+ "nodes.slab.floor": "楼面",
+ "nodes.slab.followsTerrain": "跟随地形",
+ "nodes.slab.foundation": "基础",
+ "nodes.slab.holeLabel": "切口 {index}",
+ "nodes.slab.label": "楼板",
+ "nodes.slab.manual": "手动",
+ "nodes.slab.noHoles": "无切口",
+ "nodes.slab.pts": "点",
+ "nodes.slab.rim": "边缘",
+ "nodes.slab.surface": "表面",
+ "nodes.slab.terrainDescription": "将楼板轮廓沿垂直方向延伸至地形。表面、底部和厚度保持不变。",
+ "nodes.slab.toolHints.cancel": "取消",
+ "nodes.slab.toolHints.finish": "完成楼板",
+ "nodes.slab.toolHints.trace": "绘制楼板轮廓",
+ "nodes.slab.elevation": "标高",
+ "nodes.slab.info": "信息",
+ "nodes.slab.holes": "切口",
+ "nodes.solarPanel.columns": "列数",
+ "nodes.solarPanel.customNotice": "自定义 — 尺寸与任何预设都不匹配",
+ "nodes.solarPanel.fallbackTitle": "太阳能板",
+ "nodes.solarPanel.flipOrientation": "翻转方向",
+ "nodes.solarPanel.flush": "平齐",
+ "nodes.solarPanel.frameDepth": "框架深度",
+ "nodes.solarPanel.frameThickness": "框架厚度",
+ "nodes.solarPanel.gapX": "水平间距",
+ "nodes.solarPanel.gapY": "垂直间距",
+ "nodes.solarPanel.rows": "行数",
+ "nodes.solarPanel.setbacksTooLarge": "退缩距离过大,无法放置面板。",
+ "nodes.solarPanel.standoff": "支架",
+ "nodes.solarPanel.tiltAngle": "倾斜角度",
+ "nodes.solarPanel.tilted": "倾斜",
+ "nodes.solarPanel.preset": "预设",
+ "nodes.solarPanel.presetCompact": "紧凑型",
+ "nodes.solarPanel.presetFrameless": "无边框",
+ "nodes.solarPanel.presetResidential": "住宅型",
+ "nodes.solarPanel.presetResidentialLarge": "住宅型大尺寸",
+ "nodes.solarPanel.array": "阵列",
+ "nodes.solarPanel.panel": "面板",
+ "nodes.solarPanel.mounting": "安装",
+ "nodes.solarPanel.autoFitToRoof": "自动适应屋顶",
+ "nodes.solarPanel.toolHints.cancel": "取消",
+ "nodes.solarPanel.toolHints.place": "放置太阳能板",
+ "nodes.skyLight.type": "类型",
+ "nodes.skyLight.dimensions": "尺寸",
+ "nodes.skyLight.frame": "框架",
+ "nodes.skyLight.curb": "基座",
+ "nodes.skyLight.fallbackTitle": "天窗",
+ "nodes.ridgeVent.style": "样式",
+ "nodes.ridgeVent.dimensions": "尺寸",
+ "nodes.ridgeVent.rotation": "旋转",
+ "nodes.ridgeVent.fallbackTitle": "屋脊通风口",
+ "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.fallbackTitle": "楼梯",
+ "nodes.stair.fromLevel": "起始楼层",
+ "nodes.stair.followsDeck": "跟随楼板",
+ "nodes.stair.customRise": "自定义提升",
+ "nodes.stair.currently": "当前 {height} 米",
+ "nodes.stair.deckName": "楼板",
+ "nodes.stair.geometry": "几何",
+ "nodes.stair.landing": "平台",
+ "nodes.stair.levelName": "第 {n} 层",
+ "nodes.stair.rise": "提升",
+ "nodes.stair.segmentName": "段 {n}",
+ "nodes.stair.steps": "梯段数",
+ "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.stairSegment.type": "类型",
+ "nodes.stairSegment.attachment": "连接",
+ "nodes.stairSegment.dimensions": "尺寸",
+ "nodes.stairSegment.structure": "结构",
+ "nodes.stairSegment.fallbackTitle": "楼梯段",
+ "nodes.stairSegment.width": "宽度",
+ "nodes.stairSegment.length": "长度",
+ "nodes.stairSegment.height": "高度",
+ "nodes.stairSegment.steps": "踏步数",
+ "nodes.stairSegment.fillToFloor": "填充到地板",
+ "nodes.stairSegment.thickness": "厚度",
+ "nodes.stairSegment.move": "移动",
+ "nodes.stairSegment.duplicate": "复制",
+ "nodes.wall.bands": "墙面分带",
+ "nodes.wall.curve": "弧度",
+ "nodes.wall.description": "直墙或弧墙。承载门、窗和壁挂物品。",
+ "nodes.wall.draftName": "草稿墙体",
+ "nodes.wall.fallbackTitle": "墙体",
+ "nodes.wall.followsLevel": "跟随楼层",
+ "nodes.wall.customHeight": "自定义高度",
+ "nodes.wall.currently": "当前 {measurement}",
+ "nodes.wall.label": "墙体",
+ "nodes.wall.length": "长度",
+ "nodes.wall.top": "顶部",
+ "nodes.wall.bottom": "底部",
+ "nodes.wall.auto": "自动",
+ "nodes.wall.fillToTerrain": "填充至地形",
+ "nodes.wall.fillToTerrainDescription": "向下延伸至地形。高度与顶部保持不变。",
+ "nodes.wall.thickness": "厚度",
+ "nodes.wall.bandsCount": "分带数",
+ "nodes.wall.bandsLower": "下部",
+ "nodes.wall.bandsMiddle": "中部",
+ "nodes.wall.bandsUpper": "上部",
+ "nodes.wall.skirting": "踢脚线",
+ "nodes.wall.crown": "顶角线",
+ "nodes.wall.chairRail": "护墙板腰线",
+ "nodes.wall.trimHide": "隐藏{trim}",
+ "nodes.wall.trimShow": "显示{trim}",
+ "nodes.wall.interior": "内侧",
+ "nodes.wall.exterior": "外侧",
+ "nodes.wall.both": "两侧",
+ "nodes.wall.proud": "凸出量",
+ "nodes.wall.offset": "偏移",
+ "nodes.wall.trimProfile.flat": "平直",
+ "nodes.wall.trimProfile.modern": "现代",
+ "nodes.wall.trimProfile.colonial": "殖民地",
+ "nodes.wall.trimProfile.shoe": "矮勒脚",
+ "nodes.wall.trimProfile.ogee": "葱形",
+ "nodes.wall.trimProfile.cove": "凹弧",
+ "nodes.wall.trimProfile.craft": "工匠",
+ "nodes.wall.trimProfile.layered": "分层",
+ "nodes.wall.trimProfile.round": "圆形",
+ "nodes.wall.trimProfile.picture": "挂镜",
+ "nodes.wall.trimProfile.step": "阶梯",
+ "nodes.wall.dimensions": "尺寸",
+ "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.operationLabels.raise": "升降",
+ "nodes.window.operationLabels.slats": "百叶",
+ "nodes.window.operationLabels.swing": "平开",
+ "nodes.window.operationLabels.tilt": "倾开",
+ "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.nodeType.block": "块",
+ "panel.nodeType.block_plural": "块",
+ "panel.nodeType.boxVent": "屋顶通风口",
+ "panel.nodeType.boxVent_plural": "屋顶通风口",
+ "panel.nodeType.building": "建筑",
+ "panel.nodeType.building_plural": "建筑",
+ "panel.nodeType.ceiling": "天花板",
+ "panel.nodeType.ceiling_plural": "天花板",
+ "panel.nodeType.chimney": "烟囱",
+ "panel.nodeType.chimney_plural": "烟囱",
+ "panel.nodeType.constructionDimension": "施工尺寸",
+ "panel.nodeType.constructionDimension_plural": "施工尺寸",
+ "panel.nodeType.cupola": "屋顶通风塔",
+ "panel.nodeType.cupola_plural": "屋顶通风塔",
+ "panel.nodeType.dormer": "老虎窗",
+ "panel.nodeType.dormer_plural": "老虎窗",
+ "panel.nodeType.downspout": "落水管",
+ "panel.nodeType.downspout_plural": "落水管",
+ "panel.nodeType.ductFitting": "风管配件",
+ "panel.nodeType.ductFitting_plural": "风管配件",
+ "panel.nodeType.ductSegment": "风管",
+ "panel.nodeType.ductSegment_plural": "风管",
+ "panel.nodeType.ductTerminal": "送风口",
+ "panel.nodeType.ductTerminal_plural": "送风口",
+ "panel.nodeType.eyebrowVent": "弧形通风口",
+ "panel.nodeType.eyebrowVent_plural": "弧形通风口",
+ "panel.nodeType.gutter": "檐沟",
+ "panel.nodeType.gutter_plural": "檐沟",
+ "panel.nodeType.hvacEquipment": "暖通空调设备",
+ "panel.nodeType.hvacEquipment_plural": "暖通空调设备",
+ "panel.nodeType.leanToExtension": "雨棚",
+ "panel.nodeType.leanToExtension_plural": "雨棚",
+ "panel.nodeType.level": "楼层",
+ "panel.nodeType.level_plural": "楼层",
+ "panel.nodeType.lineset": "管线",
+ "panel.nodeType.lineset_plural": "管线",
+ "panel.nodeType.liquidLine": "液管",
+ "panel.nodeType.liquidLine_plural": "液管",
+ "panel.nodeType.pipeFitting": "管道配件",
+ "panel.nodeType.pipeFitting_plural": "管道配件",
+ "panel.nodeType.pipeSegment": "排水管",
+ "panel.nodeType.pipeSegment_plural": "排水管",
+ "panel.nodeType.pipeTrap": "存水弯",
+ "panel.nodeType.pipeTrap_plural": "存水弯",
+ "panel.nodeType.ridgeVent": "屋脊通风口",
+ "panel.nodeType.ridgeVent_plural": "屋脊通风口",
+ "panel.nodeType.shelf": "架子",
+ "panel.nodeType.shelf_plural": "架子",
+ "panel.nodeType.site": "场地",
+ "panel.nodeType.site_plural": "场地",
+ "panel.nodeType.skylight": "天窗",
+ "panel.nodeType.skylight_plural": "天窗",
+ "panel.nodeType.solarPanel": "太阳能板",
+ "panel.nodeType.solarPanel_plural": "太阳能板",
+ "panel.nodeType.structuralGrid": "结构网格",
+ "panel.nodeType.structuralGrid_plural": "结构网格",
+ "panel.nodeType.turbineVent": "涡轮通风口",
+ "panel.nodeType.turbineVent_plural": "涡轮通风口",
+ "panel.nodeType.zone": "区域",
+ "panel.nodeType.zone_plural": "区域",
+ "panel.nodeType.column": "柱子",
+ "panel.nodeType.column_plural": "柱子",
+ "panel.nodeType.door": "门",
+ "panel.nodeType.door_plural": "门",
+ "panel.nodeType.elevator": "电梯",
+ "panel.nodeType.elevator_plural": "电梯",
+ "panel.nodeType.fence": "围栏",
+ "panel.nodeType.fence_plural": "围栏",
+ "panel.nodeType.guide": "参考图",
+ "panel.nodeType.guide_plural": "参考图",
+ "panel.nodeType.item": "物品",
+ "panel.nodeType.item_plural": "物品",
+ "panel.nodeType.roof": "屋顶",
+ "panel.nodeType.roof_plural": "屋顶",
+ "panel.nodeType.roofSegment": "屋顶段",
+ "panel.nodeType.roofSegment_plural": "屋顶段",
+ "panel.nodeType.scan": "3D 扫描",
+ "panel.nodeType.scan_plural": "3D 扫描",
+ "panel.nodeType.slab": "楼板",
+ "panel.nodeType.slab_plural": "楼板",
+ "panel.nodeType.stair": "楼梯",
+ "panel.nodeType.stair_plural": "楼梯",
+ "panel.nodeType.stairSegment": "楼梯段",
+ "panel.nodeType.stairSegment_plural": "楼梯段",
+ "panel.nodeType.wall": "墙体",
+ "panel.nodeType.wall_plural": "墙体",
+ "panel.nodeType.window": "窗户",
+ "panel.nodeType.window_plural": "窗户",
+ "panel.nodeType.measurement": "测量",
+ "panel.nodeType.measurement_plural": "测量",
+ "panel.nodeType.spawn": "生成点",
+ "panel.nodeType.spawn_plural": "生成点",
+ "panel.nodeType.cabinet": "橱柜",
+ "panel.nodeType.cabinet_plural": "橱柜",
+ "panel.nodeType.cabinetModule": "橱柜模块",
+ "panel.nodeType.cabinetModule_plural": "橱柜模块",
+ "panel.action.group": "编组",
+ "panel.action.ungroup": "取消编组",
+ "panel.action.duplicate": "复制",
+ "panel.action.delete": "删除",
+ "panel.multiSelection.selected": "已选择 {count} 个",
+ "panel.multiSelection.withGroup": "{label} · {count}",
+ "panel.multiSelection.sessionOnlyFooter": "{label}(仅本次会话)。普通点击将重新选中所有成员。不会随项目保存。",
+ "panel.noEdgeLines": "无边缘线",
+ "panel.scaleAndOpacity": "比例与不透明度",
+ "panel.selection": "选择",
+ "panel.setScale": "设置比例",
+ "panel.showScale": "显示比例",
+ "scenes.allScenes": "所有场景",
+ "scenes.noScenes": "还没有场景。创建一个开始吧。",
+ "scenes.noThumbnail": "无缩略图",
+ "scenes.nodes": "{count} 个节点",
+ "scenes.sceneCount": "{count} 个场景",
+ "scenes.sceneCount_plural": "{count} 个场景",
+ "scenes.title": "您的场景",
+ "scenes.noScenesSaved": "暂无保存的场景",
+ "save.conflictReload": "冲突 — 场景在其他地方被修改。请重新加载以继续。",
+ "save.createNewScene": "新建场景",
+ "save.creating": "创建中…",
+ "save.failedToCreateScene": "创建场景失败",
+ "save.newSceneName": "新场景名称",
+ "save.noSceneToSave": "没有可保存的场景",
+ "save.save": "保存",
+ "save.saveAs": "另存为",
+ "save.saveAsFailed": "另存为失败",
+ "save.saveFailed": "保存失败",
+ "save.saved": "已保存",
+ "save.saving": "保存中…",
+ "save.untitledScene": "未命名场景",
+ "settings.adjustVolume": "调整音量级别和静音设置",
+ "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": "保存与加载",
+ "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": "对公开查看者可见",
+ "settings.visibleNodesOnly": "仅可见节点",
+ "settings.visibleNodesOnlyDesc": "仅导出当前在编辑器中可见的节点。",
+ "shortcuts.activateMeasurementTool": "激活测量工具",
+ "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.deleteSelected": "删除选中对象",
+ "shortcuts.directManipulation": "直接操作",
+ "shortcuts.dragMiddleMouseOrHoldSpace": "按住中键拖动,或按住空格键同时左键拖动。",
+ "shortcuts.dragRightMouse": "右键拖动。",
+ "shortcuts.drawingTools": "绘制工具",
+ "shortcuts.editorNavigation": "编辑器导航",
+ "shortcuts.groupSelection": "编组选中",
+ "shortcuts.groupSelectionNote": "将所选对象合并为一个可变换的编组。",
+ "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.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": "切换到选择模式",
+ "shortcuts.switchToSitePhase": "切换到场地阶段",
+ "shortcuts.switchToStructurePhase": "切换到结构阶段",
+ "shortcuts.switchToZonesLayer": "切换到区域层",
+ "shortcuts.toggleSidebar": "切换侧边栏",
+ "shortcuts.undo": "撤销",
+ "shortcuts.ungroupSelection": "取消编组",
+ "shortcuts.ungroupSelectionNote": "将所选编组拆分为独立对象。",
+ "sidebar.build": "构建",
+ "sidebar.file": "文件",
+ "sidebar.items": "物品",
+ "sidebar.scene": "场景",
+ "sidebar.settings": "设置",
+ "sidebar.site": "场地",
+ "site.addLevel": "添加楼层",
+ "site.area": "面积",
+ "site.baseElevation": "基准标高",
+ "site.building": "建筑",
+ "site.cameraSnapshot": "相机快照",
+ "site.capture": "捕获",
+ "site.clearSelection": "清除选择",
+ "site.delete": "删除",
+ "site.duplicateLevelWithOptions": "带选项复制楼层",
+ "site.furnish": "家具",
+ "site.guide": "引导",
+ "site.hide": "隐藏",
+ "site.level": "楼层",
+ "site.noBuildingsYet": "暂无建筑",
+ "site.noLevelsYet": "暂无楼层",
+ "site.perimeter": "周长",
+ "site.propertyLine": "地界线",
+ "site.show": "显示",
+ "site.site": "场地",
+ "site.structure": "结构",
+ "site.uploadScan": "上传扫描/平面图",
+ "site.uploadingWithProgress": "正在上传 {type}... {progress}%",
+ "site.untitled": "未命名",
+ "site.levels": "楼层",
+ "site.addPoint": "添加点",
+ "site.axisX": "X",
+ "site.axisZ": "Z",
+ "site.camera.viewSnapshot": "查看快照",
+ "site.camera.updateSnapshot": "更新快照",
+ "site.camera.takeSnapshot": "拍摄快照",
+ "site.camera.clearSnapshot": "清除快照",
+ "site.objectsSelected": "已选择 {count} 个对象",
+ "site.reference.capture": "捕获",
+ "site.reference.guide": "引导图",
+ "site.nodeType.wall": "墙体",
+ "site.nodeType.fence": "围栏",
+ "site.nodeType.item": "物件",
+ "site.nodeType.slab": "楼板",
+ "site.nodeType.ceiling": "天花板",
+ "site.nodeType.roof": "屋顶",
+ "site.nodeType.roofSegment": "屋顶段",
+ "site.zones": "区域",
+ "snapshot.area": "区域",
+ "snapshot.capture": "捕获",
+ "snapshot.capturing": "捕获中",
+ "snapshot.closeCapture": "关闭捕获模式",
+ "snapshot.dragArea": "拖动选择要捕获的区域",
+ "snapshot.escToCancel": "Esc取消",
+ "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": "低",
+ "toolbar.up": "高",
+ "toolbar.walls": "墙体",
+ "continuation.cabinet.continuous": "连续排布",
+ "continuation.cabinet.single": "单个柜体",
+ "continuation.canopy.continuous": "连续雨棚",
+ "continuation.canopy.single": "单个雨棚",
+ "continuation.fence.continuous": "连续",
+ "continuation.fence.curved": "弯曲围栏",
+ "continuation.fence.single": "单段围栏",
+ "continuation.point.once": "放置一次",
+ "continuation.point.repeat": "放置多个",
+ "continuation.wall.room": "房间(自动闭合)",
+ "continuation.wall.single": "单段墙",
+ "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.belowCount": "下方 {count} 层",
+ "viewer.camera": "相机",
+ "viewer.collapseSidebar": "收起侧边栏",
+ "viewer.couldNotAddGuideImage": "无法添加该参考图。",
+ "viewer.crispEdges": "清晰不透明的边缘线",
+ "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.opacity": "不透明度",
+ "viewer.hideReferenceRow": "隐藏{subject}",
+ "viewer.showReferenceRow": "显示{subject}",
+ "viewer.deleteReferenceRow": "删除{subject}",
+ "viewer.openProjectBeforeUpload": "请先打开项目再上传扫描。",
+ "viewer.orbitLeft": "左旋转",
+ "viewer.orbitRight": "右旋转",
+ "viewer.orthographic": "正交",
+ "viewer.perspective": "透视",
+ "viewer.preview": "预览",
+ "viewer.previewMode": "预览模式",
+ "viewer.referenceFloorSettings": "参考楼层设置",
+ "viewer.referenceFloorWithLevel": "参考楼层:{level}",
+ "viewer.referenceSettings": "参考设置",
+ "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": "显示参考楼层",
+ "viewer.soft": "柔和",
+ "viewer.solid": "纯色",
+ "viewer.solo": "单独",
+ "viewer.stack": "堆叠",
+ "viewer.strong": "锐利",
+ "viewer.topView": "顶视图",
+ "viewer.translucent": "半透明",
+ "viewer.units": "单位",
+ "viewer.uploadGlbOrImage": "请上传 .glb/.gltf 扫描文件或图片。",
+ "viewer.uploadScanOrGuide": "上传扫描或参考图",
+ "viewer.uploading": "上传中",
+ "viewer.viewMode2D": "2D",
+ "viewer.viewMode3D": "3D",
+ "viewer.viewModeSplit": "分屏",
+ "viewer.visible": "可见",
+ "viewer.walkthrough": "导览",
+ "viewer.wallsWithMode": "墙体:{mode}",
+ "viewer.wallCenterline": "墙体中线",
+ "viewer.wallCenterlineDetail": "测量到墙体的结构中心。",
+ "viewer.wallDimensions": "墙体尺寸",
+ "viewer.spawnPointHint": "从构建选项卡放置出生点来控制导览开始位置。",
+ "zone.addOne": "添加",
+ "zone.noZonesOnLevel": "此楼层暂无区域。",
+ "zone.selectLevelToView": "选择楼层以查看和创建区域",
+ "nodes.ceiling.toolHints.trace": "绘制天花板轮廓",
+ "nodes.ceiling.toolHints.finish": "完成天花板",
+ "nodes.item.label": "物品",
+ "nodes.item.description": "放置在场景中的物品。",
+ "nodes.item.position": "位置",
+ "nodes.item.rotation": "旋转",
+ "nodes.item.scale": "缩放",
+ "nodes.item.uniformScale": "均匀缩放",
+ "nodes.item.info": "信息",
+ "nodes.item.collections": "集合",
+ "nodes.item.manageCollections": "管理集合…",
+ "nodes.site.label": "场地",
+ "nodes.site.description": "包含建筑的场地。",
+ "nodes.dormer.toolHints.place": "在屋顶上放置老虎窗",
+ "nodes.dormer.toolHints.rotateGhost": "旋转虚影",
+ "nodes.chimney.toolHints.place": "在屋顶上放置烟囱",
+ "nodes.ridgeVent.toolHints.place": "在屋顶上放置屋脊通风口",
+ "nodes.shelf.toolHints.place": "放置架子",
+ "nodes.skylight.toolHints.place": "在屋顶上放置天窗",
+ "nodes.spawn.toolHints.place": "放置出生点",
+ "nodes.spawn.spawnPoint": "出生点",
+ "nodes.spawn.position": "位置",
+ "nodes.spawn.facing": "朝向",
+ "nodes.spawn.yaw": "偏航",
+ "nodes.zone.toolHints.place": "放置区域",
+ "nodes.block.actions": "操作",
+ "nodes.block.addSlot": "添加槽",
+ "nodes.block.blockAccent": "块体强调色",
+ "nodes.block.defaultMaterial": "默认材质",
+ "nodes.block.editModeSlotActions": "进入编辑模式以使用槽操作",
+ "nodes.block.editModeAssignFaces": "进入编辑模式以分配面",
+ "nodes.block.faceSelectSwitch": "切换到面选择 (3)",
+ "nodes.block.fallbackTitle": "块",
+ "nodes.block.noFacesSelected": "未选中任何面",
+ "nodes.block.readOnly": "场景为只读",
+ "nodes.block.mixedSlots": "{count} 个面 · 混合槽",
+ "nodes.block.singleSlot": "{count} 个{faceLabel} · {slotLabel}",
+ "nodes.block.face": "面",
+ "nodes.block.faces": "面",
+ "nodes.block.position": "位置",
+ "nodes.block.selectFacesFirst": "请先选择一个或多个面",
+ "nodes.block.slots": "槽",
+ "nodes.block.slotApplied": "已使用强调色材质将 {slot} 应用到 {count} 个{faceLabel}。使用 Paint (P) 可替换。",
+ "nodes.block.slotApplyAria": "将 {slot} 应用到所选面",
+ "nodes.block.slotDeleteAria": "删除 {slot} 槽",
+ "nodes.block.slotRenameAria": "重命名 {slot} 槽",
+ "nodes.block.slotDeleteTitle": "删除材质槽并对其面使用主体",
+ "nodes.block.unpainted": "未涂装",
+ "nodes.block.toolHints.place": "放置块",
+ "nodes.column.toolHints.place": "放置柱子",
+ "nodes.cupola.cupola": "屋顶通风塔",
+ "nodes.cupola.dome": "圆顶",
+ "nodes.cupola.finial": "顶尖装饰",
+ "nodes.cupola.noFinial": "无顶尖装饰",
+ "nodes.cupola.pyramid": "金字塔顶",
+ "nodes.cupola.toolHints.place": "在屋顶上放置屋顶通风塔",
+ "nodes.door.toolHints.flipSide": "翻转侧面",
+ "nodes.downspout.toolHints.highlightOutlet": "高亮出水口",
+ "nodes.downspout.toolHints.drop": "从出水口放下水落管",
+ "nodes.elevator.toolHints.place": "放置电梯",
+ "nodes.eyebrowVent.toolHints.place": "在屋顶上放置老虎窗通风口",
+ "nodes.eyebrowVent.fallbackTitle": "老虎窗通风口",
+ "nodes.eyebrowVent.style": "样式",
+ "nodes.eyebrowVent.scoop": "勺形",
+ "nodes.eyebrowVent.halfRound": "半圆",
+ "nodes.eyebrowVent.slantBox": "斜箱",
+ "nodes.eyebrowVent.louvers": "百叶",
+ "nodes.eyebrowVent.backHeight": "背面高度",
+ "nodes.eyebrowVent.dimensions": "尺寸",
+ "nodes.gutter.toolHints.place": "在屋顶檐口放置檐沟",
+ "nodes.item.toolHints.place": "放置物品",
+ "nodes.item.toolHints.cycleSnap": "循环切换捕捉模式",
+ "nodes.roof.toolHints.setFootprint": "设置屋顶底部投影",
+ "nodes.roof.toolHints.placement": "放置方式",
+ "nodes.roof.toolHints.placementAuto": "放置方式:自动",
+ "nodes.roof.toolHints.placementGround": "放置方式:地面",
+ "nodes.roof.toolHints.placementRoof": "放置方式:屋顶",
+ "nodes.roof.toolHints.rotateDirection": "旋转屋顶方向 90°",
+ "nodes.spawn.toolHints.rotate": "旋转出生点",
+ "nodes.stair.toolHints.place": "放置楼梯",
+ "nodes.structuralGrid.toolHints.start": "开始网格轴",
+ "nodes.structuralGrid.toolHints.finish": "完成网格轴",
+ "nodes.structuralGrid.toolHints.bypassSnap": "绕过捕捉",
+ "nodes.turbineVent.toolHints.place": "在屋顶上放置涡轮通风口",
+ "nodes.turbineVent.fallbackTitle": "涡轮通风口",
+ "nodes.turbineVent.style": "样式",
+ "nodes.turbineVent.globe": "球形",
+ "nodes.turbineVent.cylinder": "圆柱",
+ "nodes.turbineVent.dimensions": "尺寸",
+ "nodes.turbineVent.neckHeight": "颈部高度",
+ "nodes.turbineVent.vanes": "叶片数",
+ "nodes.turbineVent.motion": "运动",
+ "nodes.turbineVent.pause": "暂停",
+ "nodes.turbineVent.play": "播放",
+ "nodes.turbineVent.spinSpeed": "旋转速度",
+ "nodes.window.toolHints.flipSide": "翻转侧面",
+ "nodes.cabinet.addChimney": "添加烟道罩",
+ "nodes.cabinet.addCompartment": "添加隔间",
+ "nodes.cabinet.addWallCabinet": "添加吊柜",
+ "nodes.cabinet.cabinetType": "柜体类型",
+ "nodes.cabinet.carcassHeight": "柜体高度",
+ "nodes.cabinet.close": "关闭",
+ "nodes.cabinet.closeCabinet": "关闭柜门",
+ "nodes.cabinet.compartments": "隔间",
+ "nodes.cabinet.dimensions": "尺寸",
+ "nodes.cabinet.fallbackTitle": "组合柜",
+ "nodes.cabinet.fillToCeiling": "填充至天花板",
+ "nodes.cabinet.finish": "饰面",
+ "nodes.cabinet.frontOverlay.full": "全覆盖",
+ "nodes.cabinet.frontOverlay.inset": "内嵌",
+ "nodes.cabinet.frontStyle.raisedArch": "拱形凸起",
+ "nodes.cabinet.frontStyle.shaker": "平板镶框",
+ "nodes.cabinet.frontStyle.slab": "平板",
+ "nodes.cabinet.fronts": "门板",
+ "nodes.cabinet.fronts.mounting": "安装方式",
+ "nodes.cabinet.fronts.revealGap": "门缝间隙",
+ "nodes.cabinet.fronts.style": "风格",
+ "nodes.cabinet.handle.bar": "条形",
+ "nodes.cabinet.handle.cutout": "挖手",
+ "nodes.cabinet.handle.hole": "孔",
+ "nodes.cabinet.handle.knob": "旋钮",
+ "nodes.cabinet.handle.none": "无",
+ "nodes.cabinet.handlePosition.auto": "自动",
+ "nodes.cabinet.handlePosition.center": "居中",
+ "nodes.cabinet.handlePosition.top": "顶部",
+ "nodes.cabinet.handles": "拉手",
+ "nodes.cabinet.handles.position": "位置",
+ "nodes.cabinet.handles.style": "风格",
+ "nodes.cabinet.open": "开",
+ "nodes.cabinet.openAnimation": "开关动画",
+ "nodes.cabinet.openCabinet": "打开柜门",
+ "nodes.cabinet.play": "播放",
+ "nodes.cabinet.playAnimation": "播放动画",
+ "nodes.cabinet.planningChecks": "规划检查",
+ "nodes.cabinet.presets": "预设",
+ "nodes.cabinet.reflowRejected": "本段内无可用空间。无法通过缩小地柜来容纳此项目。",
+ "nodes.cabinet.removeWallCabinet": "移除吊柜",
+ "nodes.cabinet.standardWidth": "标准宽度",
+ "nodes.cabinet.stop": "停止",
+ "nodes.cabinet.stopAnimation": "停止动画",
+ "nodes.cabinet.tier.base": "地柜",
+ "nodes.cabinet.tier.tall": "高柜",
+ "nodes.cabinet.topCeiling": "顶部 / 天花板",
+ "nodes.cabinet.topDepth": "顶部深度",
+ "nodes.cabinet.topFinish.none": "无",
+ "nodes.cabinet.topFinish.topCabinet": "顶柜",
+ "nodes.cabinet.topFinish.trim": "封边 / 吊顶",
+ "nodes.cabinet.topHeight": "顶部高度",
+ "nodes.cabinet.toolHints.place": "放置柜体",
+ "nodes.cabinet.toolHints.placementType": "放置类型",
+ "nodes.cabinet.toolHints.typeCabinet": "类型:柜体",
+ "nodes.cabinet.toolHints.typeIsland": "类型:岛台",
+ "nodes.constructionDimension.actions": "操作",
+ "nodes.constructionDimension.centerMark": "中心标记",
+ "nodes.constructionDimension.datumPolicy": "基准策略",
+ "nodes.constructionDimension.datumPolicyOptions.centerline": "中心线",
+ "nodes.constructionDimension.datumPolicyOptions.finishFace": "完成面",
+ "nodes.constructionDimension.datumPolicyOptions.structuralFace": "结构面",
+ "nodes.constructionDimension.datumPolicyOptions.wallFace": "墙面",
+ "nodes.constructionDimension.defaultFoundationDimensionName": "基础标注",
+ "nodes.constructionDimension.dimension": "标注",
+ "nodes.constructionDimension.drawingCoordination": "图纸协同",
+ "nodes.constructionDimension.extensionGap": "延伸间隙",
+ "nodes.constructionDimension.extensionOvershoot": "延伸超长",
+ "nodes.constructionDimension.fallbackTitle": "施工标注",
+ "nodes.constructionDimension.featureCount": "要素数量",
+ "nodes.constructionDimension.foundationController": "基础控制器",
+ "nodes.constructionDimension.imperialPrecision": "英制精度",
+ "nodes.constructionDimension.imperialPrecisionOptions.1": "整英寸",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/2": "1/2 英寸",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/4": "1/4 英寸",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/8": "1/8 英寸",
+ "nodes.constructionDimension.imperialPrecisionOptions.1/16": "1/16 英寸",
+ "nodes.constructionDimension.linkedDimensionsNote": "关联标注将复用控制器的关联锚点,并随其同步更新。",
+ "nodes.constructionDimension.metricNotation": "公制单位",
+ "nodes.constructionDimension.metricNotationOptions.meters": "米",
+ "nodes.constructionDimension.metricNotationOptions.millimeters": "毫米",
+ "nodes.constructionDimension.mode": "模式",
+ "nodes.constructionDimension.modeOptions.angular": "角度",
+ "nodes.constructionDimension.modeOptions.arc-length": "弧长",
+ "nodes.constructionDimension.modeOptions.center-mark": "中心标记",
+ "nodes.constructionDimension.modeOptions.chord": "弦长",
+ "nodes.constructionDimension.modeOptions.coordinate": "坐标",
+ "nodes.constructionDimension.modeOptions.diameter": "直径",
+ "nodes.constructionDimension.modeOptions.linear": "线性",
+ "nodes.constructionDimension.modeOptions.radius": "半径",
+ "nodes.constructionDimension.notation": "文本",
+ "nodes.constructionDimension.noFoundationDimensions": "无基础标注",
+ "nodes.constructionDimension.presentation": "{drawing} 显示方式",
+ "nodes.constructionDimension.presentationOptions.controlled": "由基础控制",
+ "nodes.constructionDimension.presentationOptions.omit": "省略",
+ "nodes.constructionDimension.presentationOptions.shown": "显示",
+ "nodes.constructionDimension.prefix": "前缀",
+ "nodes.constructionDimension.primaryDrawing": "主图纸",
+ "nodes.constructionDimension.standards": "标准",
+ "nodes.constructionDimension.suffix": "后缀",
+ "nodes.constructionDimension.suppressedSegments": "{drawing} 隐藏段",
+ "nodes.constructionDimension.suppressedSegmentsNote": "段号从 1 起算,仅作用于当前图纸视图。",
+ "nodes.constructionDimension.suppressedSegmentsPlaceholder": "例如 2, 4",
+ "nodes.constructionDimension.terminator": "端点符号",
+ "nodes.constructionDimension.terminatorOptions.architectural-tick": "建筑斜线",
+ "nodes.constructionDimension.terminatorOptions.dot": "圆点",
+ "nodes.constructionDimension.terminatorOptions.filled-arrow": "实心箭头",
+ "nodes.constructionDimension.terminatorOptions.open-arrow": "空心箭头",
+ "nodes.constructionDimension.textOverride": "文字覆盖",
+ "nodes.constructionDimension.textOverridePlaceholder": "使用实测值",
+ "nodes.constructionDimension.textPosition": "文字位置",
+ "nodes.constructionDimension.textPositionOptions.above": "线上方",
+ "nodes.constructionDimension.textPositionOptions.centered": "居中",
+ "nodes.constructionDimension.toolHints.pickWitness": "选择参考点",
+ "nodes.constructionDimension.toolHints.finishWitnesses": "完成多点参考",
+ "nodes.constructionDimension.toolHints.placeLine": "需要时放置尺寸线",
+ "nodes.constructionDimension.toolHints.removeLast": "移除上一个参考点",
+ "nodes.ductFitting.toolHints.place": "放置配件",
+ "nodes.ductFitting.toolHints.snap": "吸附到管线",
+ "nodes.ductSegment.toolHints.start": "开始管段",
+ "nodes.ductSegment.toolHints.placeContinue": "放置并继续",
+ "nodes.ductSegment.toolHints.vertical": "垂直 ↕,点击放置",
+ "nodes.ductSegment.toolHints.diameter": "风管直径 减小 / 增大",
+ "nodes.ductSegment.toolHints.trunk": "圆形 / 方形主管",
+ "nodes.ductSegment.toolHints.height": "天花板 / 地板高度",
+ "nodes.ductTerminal.toolHints.place": "放置出风口",
+ "nodes.ductTerminal.toolHints.mount": "安装:地板 / 天花板 / 墙",
+ "nodes.ductTerminal.toolHints.rotate": "旋转 ±45°(地板 / 天花板)",
+ "nodes.hvacEquipment.toolHints.place": "放置设备",
+ "nodes.leanToExtension.toolHints.place": "放置雨棚或设置下一个连续点",
+ "nodes.leanToExtension.toolHints.rotateFlip": "旋转或翻转连续侧",
+ "nodes.leanToExtension.toolHints.cycleStyle": "循环切换 单坡 / 山墙 / 蝶形",
+ "nodes.lineset.toolHints.start": "开始冷媒管",
+ "nodes.lineset.toolHints.place": "放置(锁定 45°)",
+ "nodes.lineset.toolHints.vertical": "垂直 ↕,点击放置",
+ "nodes.liquidLine.toolHints.start": "开始液管",
+ "nodes.liquidLine.toolHints.place": "放置(锁定 45°)",
+ "nodes.liquidLine.toolHints.vertical": "垂直 ↕,点击放置",
+ "nodes.liquidLine.toolHints.follow": "跟随:追踪冷媒管",
+ "nodes.pipeFitting.toolHints.place": "放置管件",
+ "nodes.pipeFitting.toolHints.snap": "吸附到管线",
+ "nodes.pipeSegment.toolHints.start": "开始管段",
+ "nodes.pipeSegment.toolHints.place": "放置(污水坡降 ¼″/英尺)",
+ "nodes.pipeSegment.toolHints.wasteVent": "污水 / 通气",
+ "nodes.pipeSegment.toolHints.size": "管径 减小 / 增大",
+ "nodes.pipeSegment.toolHints.vertical": "垂直立管 ↕,点击放置",
+ "nodes.pipeTrap.toolHints.place": "放置存水弯",
+ "paint.material.defaultName": "材质 {n}",
+ "paint.material.copySuffix": "{name} 副本",
+ "paint.material.usedByOne": "被 1 个部件使用",
+ "paint.material.usedByOther": "被 {count} 个部件使用",
+ "paint.material.paintWith": "使用此材质绘制",
+ "paint.erase": "擦除",
+ "paint.resetAll": "全部重置",
+ "paint.sceneMaterials": "场景材质",
+ "paint.addMaterial": "添加材质",
+ "paint.noCustomMaterials": "尚无自定义材质 — 点击 + 添加。",
+ "materialPicker.source.pascal": "Pascal",
+ "materialPicker.source.workspace": "工作区",
+ "materialPicker.newMaterial": "新建材质",
+ "material.color": "颜色",
+ "material.roughness": "粗糙度",
+ "material.metalness": "金属度",
+ "material.opacity": "不透明度",
+ "material.side": "面",
+ "material.side.front": "正面",
+ "material.side.back": "背面",
+ "material.side.double": "双面",
+ "materials.colors": "颜色",
+ "materials.stone": "石材",
+ "materials.brick": "砖",
+ "materials.tile": "瓷砖",
+ "materials.wallpaper": "墙纸",
+ "materials.concrete": "混凝土",
+ "materials.metal": "金属",
+ "materials.plastic": "塑料",
+ "materials.fabric": "织物",
+ "materials.carpet": "地毯",
+ "materials.leather": "皮革",
+ "materials.glass": "玻璃",
+ "common.mixed": "混合",
+ "terrain.verb.raise": "抬升",
+ "terrain.verb.lower": "降低",
+ "terrain.verb.flatten": "平整",
+ "terrain.verb.smooth": "平滑",
+ "terrain.hint.raise": "拖动以抬升地面。单次最多抬升 {metres} 米 — 松开后再次拖动以继续。",
+ "terrain.hint.lower": "拖动以降低地面。单次最多降低 {metres} 米。",
+ "terrain.hint.flatten": "拖动以将地面平整至目标高度。不会过度平整。",
+ "terrain.hint.smooth": "拖动以柔化坡度并去除山脊。平坦地面保持平坦。",
+ "terrain.brush.size": "大小",
+ "terrain.brush.strength": "强度",
+ "terrain.brush.softness": "柔软度",
+ "terrain.brush.round": "圆形",
+ "terrain.brush.square": "方形",
+ "terrain.flatten.target": "目标",
+ "terrain.flatten.pickTarget": "从地面选取目标高度",
+ "terrain.flatten.samplingHint": "点击地面以将其高度作为目标。",
+ "terrain.flatten.noTargetHint": "尚无目标 — 首次点击将采样其下方的地面。",
+ "terrain.flatten.targetHint": "每次平整笔刷都会向此高度平整。",
+ "terrain.levelLot": "平整地块",
+ "terrain.clearTerrain": "清除地形",
+ "materials.concrete-drywall.label": "已处理石膏板",
+ "materials.concrete-plaster.label": "彩色石膏",
+ "materials.concrete-plate.label": "混凝土板",
+ "materials.concrete-polished.label": "抛光混凝土",
+ "materials.concrete-raw.label": "素混凝土",
+ "materials.concrete-stucco.label": "白灰泥",
+ "materials.fabric-boucle.label": "圈圈纱",
+ "materials.fabric-cotton.label": "棉",
+ "materials.fabric-linen.label": "亚麻",
+ "materials.fabric-suede.label": "麂皮",
+ "materials.fabric-velvet.label": "天鹅绒",
+ "materials.fabric-wool.label": "羊毛",
+ "materials.flooring-agedbrick.label": "做旧砖",
+ "materials.flooring-ceramic53.label": "陶瓷马赛克",
+ "materials.flooring-darkceramic22.label": "深色做旧瓷砖",
+ "materials.flooring-garagedoor.label": "车库面板",
+ "materials.flooring-greenlabradorite.label": "绿拉长石",
+ "materials.flooring-greenquartzitea.label": "绿石英岩 A",
+ "materials.flooring-ground13.label": "土地面",
+ "materials.flooring-lightceramic24.label": "浅色做旧瓷砖",
+ "materials.flooring-pooltiles.label": "泳池砖",
+ "materials.flooring-rusticbrick.label": "复古砖",
+ "materials.flooring-statuarettowhite.label": "白色雕像石",
+ "materials.flooring-terrazzo19.label": "水磨石",
+ "materials.flooring-tile20.label": "马赛克砖",
+ "materials.flooring-tile68.label": "图案砖",
+ "materials.flooring-tile79.label": "石材砖",
+ "materials.flooring-tile85a.label": "方砖",
+ "materials.flooring-tile86.label": "赤陶砖",
+ "materials.flooring-tiles3.label": "棋盘砖",
+ "materials.flooring-tiles4.label": "网格砖",
+ "materials.flooring-wallstone1.label": "石墙",
+ "materials.flooring-weatheredbrick.label": "风化砖",
+ "materials.flooring-woodenceramic2.label": "木纹瓷砖 2",
+ "materials.flooring-woodenceramic3.label": "木纹瓷砖 3",
+ "materials.flooring-woodparquet76.label": "木拼花",
+ "materials.leather-black.label": "黑皮革",
+ "materials.leather-calf.label": "小牛皮革",
+ "materials.metal-brass.label": "黄铜",
+ "materials.metal-chrome.label": "铬",
+ "materials.metal-copper.label": "铜",
+ "materials.metal-polished.label": "抛光金属",
+ "materials.metal-steel.label": "拉丝钢",
+ "materials.preset-aubergine.label": "茄紫",
+ "materials.preset-beige.label": "米色",
+ "materials.preset-berry.label": "莓果",
+ "materials.preset-blush.label": "腮红",
+ "materials.preset-brickred.label": "砖红",
+ "materials.preset-burntorange.label": "焦橙",
+ "materials.preset-charcoal.label": "炭灰",
+ "materials.preset-clay.label": "陶土色",
+ "materials.preset-cream.label": "奶油色",
+ "materials.preset-deepteal.label": "深青",
+ "materials.preset-dustyrose.label": "灰玫瑰",
+ "materials.preset-espresso.label": "浓缩咖啡",
+ "materials.preset-forest.label": "森林绿",
+ "materials.preset-glass.label": "玻璃",
+ "materials.preset-gold.label": "金色",
+ "materials.preset-greige.label": "灰咖",
+ "materials.preset-lavender.label": "薰衣草紫",
+ "materials.preset-lightgrey.label": "浅灰",
+ "materials.preset-metal.label": "金属",
+ "materials.preset-midgrey.label": "中灰",
+ "materials.preset-mint.label": "薄荷绿",
+ "materials.preset-mustard.label": "芥末黄",
+ "materials.preset-navy.label": "海军蓝",
+ "materials.preset-nearblack.label": "近黑",
+ "materials.preset-ochre.label": "赭石",
+ "materials.preset-olive.label": "橄榄绿",
+ "materials.preset-oxblood.label": "牛血红",
+ "materials.preset-paleteal.label": "浅青",
+ "materials.preset-paleyellow.label": "浅黄",
+ "materials.preset-peach.label": "蜜桃色",
+ "materials.preset-petal.label": "花瓣粉",
+ "materials.preset-plum.label": "梅紫",
+ "materials.preset-powderblue.label": "粉蓝",
+ "materials.preset-rose.label": "玫瑰粉",
+ "materials.preset-royalblue.label": "宝蓝",
+ "materials.preset-sage.label": "鼠尾草绿",
+ "materials.preset-sand.label": "沙色",
+ "materials.preset-sky.label": "天空蓝",
+ "materials.preset-slateblue.label": "石板蓝",
+ "materials.preset-softblue.label": "柔蓝",
+ "materials.preset-softwhite.label": "柔白",
+ "materials.preset-tan.label": "棕褐",
+ "materials.preset-taupe.label": "灰褐",
+ "materials.preset-teal.label": "青绿",
+ "materials.preset-terracotta.label": "赤陶色",
+ "materials.preset-tomato.label": "番茄红",
+ "materials.preset-white.label": "白色",
+ "materials.roof-classicshingles.label": "经典木瓦",
+ "materials.roof-claytiles.label": "陶土瓦",
+ "materials.roof-terracottatiles.label": "赤陶瓦",
+ "materials.roof-weatheredshingles.label": "风化木瓦",
+ "materials.wood-finewood27.label": "细木 27",
+ "materials.wood-floorplank1.label": "地板木 1",
+ "materials.wood-hungarianparquet10.label": "匈牙利拼花 10",
+ "materials.wood-hungarianparquet2.label": "匈牙利拼花 2",
+ "materials.wood-squareparquet21.label": "方形拼花 21",
+ "materials.wood-squareparquet23.label": "方形拼花 23",
+ "materials.wood-woodenparquet11.label": "木质拼花 11",
+ "materials.wood-woodfine1.label": "细木纹 1",
+ "materials.wood-woodfine11.label": "细木纹 11",
+ "materials.wood-woodfine13.label": "细木纹 13",
+ "materials.wood-woodfine2.label": "细木纹 2",
+ "materials.wood-woodfine22.label": "细木纹 22",
+ "materials.wood-woodfine24.label": "细木纹 24",
+ "materials.wood-woodparquet121.label": "木拼花 121",
+ "materials.wood-woodparquet14.label": "木拼花 14",
+ "materials.wood-woodparquet56.label": "木拼花 56",
+ "materials.wood-woodparquet65.label": "木拼花 65",
+ "materials.wood-woodparquet99.label": "木拼花 99",
+ "materials.wood-woodplank19.label": "木板 19",
+ "materials.wood-woodplank48.label": "木板 48",
+ "common.placement": "布置",
+ "common.construction": "构造",
+ "common.drainage": "排水",
+ "common.transform": "变换",
+ "common.connections": "连接",
+ "common.fitting": "配件",
+ "common.advanced": "高级",
+ "common.appearance": "外观",
+ "common.mounting": "安装",
+ "nodes.chimney.body": "主体",
+ "nodes.door.frame": "门框",
+ "nodes.dormer.dormerRoof": "老虎窗屋顶",
+ "nodes.downspout.hardware": "五金件",
+ "nodes.ductSegment.air": "气流",
+ "nodes.ductTerminal.terminal": "末端",
+ "nodes.ductTerminal.face": "面板",
+ "nodes.ductTerminal.collar": "接口环",
+ "nodes.fence.structure": "结构",
+ "nodes.gutter.profile": "型材",
+ "nodes.gutter.endCaps": "端盖",
+ "nodes.gutter.hangers": "吊架",
+ "nodes.hvacEquipment.equipment": "设备",
+ "nodes.hvacEquipment.cabinet": "机箱",
+ "nodes.hvacEquipment.supply": "送风",
+ "nodes.hvacEquipment.return": "回风",
+ "nodes.leanToExtension.size": "尺寸",
+ "nodes.leanToExtension.connection": "连接",
+ "nodes.leanToExtension.structure": "结构",
+ "nodes.lineset.lines": "管线",
+ "nodes.lineset.insulation": "保温",
+ "nodes.liquidLine.line": "管线",
+ "nodes.pipeTrap.trap": "存水弯",
+ "nodes.shelf.topology": "结构",
+ "nodes.skylight.type": "类型",
+ "nodes.skylight.curb": "边框",
+ "nodes.skylight.opening": "开启",
+ "nodes.skylight.lantern": "穹顶",
+ "nodes.solarPanel.grid": "阵列",
+ "nodes.solarPanel.panelDimensions": "板尺寸",
+ "nodes.solarPanel.frame": "边框",
+ "editor.deleteWithContents": "删除(含内容)",
+ "editor.saveToCatalog": "保存到目录",
+ "editor.replace": "替换",
+ "editor.replacing": "替换中...",
+ "editor.lock": "锁定",
+ "editor.unlock": "解锁",
+ "editor.setScale": "设置比例",
+ "editor.editScale": "编辑比例",
+ "editor.showScale": "显示比例",
+ "editor.hideScale": "隐藏比例",
+ "editor.uncalibrated": "未校准",
+ "editor.scaled": "已校准",
+ "editor.scaledHidden": "已校准(隐藏)",
+ "editor.chooseImage": "请选择 PNG、JPEG 或 WebP 图片。",
+ "editor.couldNotReplaceImage": "无法替换该图片。",
+ "editor.overlayImageUnavailable": "叠加图片不可用。请替换图片以恢复。",
+ "editor.clickEndsOfKnownDistance": "在平面上点击已知距离的两端,然后输入实际长度。",
+ "editor.drawLineOverKnownDimension": "在已知尺寸上画一条线,然后输入实际长度以精确缩放图像。",
+ "editor.zoomOut": "缩小",
+ "editor.zoomIn": "放大",
+ "editor.fitFloorPlan": "适配平面图",
+ "editor.floor": "楼层",
+ "editor.alignViewNorth": "对齐到北方",
+ "editor.topView": "顶视图",
+ "editor.orbitLeft": "向左旋转",
+ "editor.orbitRight": "向右旋转",
+ "editor.camera": "相机",
+ "editor.visibility": "可见性",
+ "editor.displaySettings": "显示设置",
+ "editor.walkthrough": "漫游",
+ "editor.levelsLabel": "楼层:{mode}",
+ "editor.wallsLabel": "墙体:{mode}",
+ "editor.scans": "扫描",
+ "editor.guides": "参考",
+ "editor.shadows": "阴影",
+ "editor.render": "渲染",
+ "editor.colors": "颜色",
+ "editor.theme": "主题",
+ "editor.edges": "边线",
+ "editor.stacked": "堆叠",
+ "editor.exploded": "分解",
+ "editor.solo": "单层",
+ "editor.manual": "手动",
+ "editor.fullHeight": "全高",
+ "editor.cutaway": "剖切",
+ "editor.low": "矮墙",
+ "editor.viewer3d": "3D",
+ "editor.viewer2d": "2D",
+ "editor.viewerSplit": "分屏",
+ "editor.perspective": "透视",
+ "editor.orthographic": "正交",
+ "editor.solid": "实体",
+ "editor.rendered": "渲染",
+ "editor.solidDetail": "平面快速,无环境光遮蔽",
+ "editor.renderedDetail": "完整环境光遮蔽",
+ "editor.on": "开",
+ "editor.off": "关",
+ "editor.monochrome": "单色",
+ "editor.colored": "彩色",
+ "editor.cameraSnapshot": "相机快照",
+ "editor.takeSnapshot": "拍摄快照",
+ "editor.updateSnapshot": "更新快照",
+ "editor.clearSnapshot": "清除快照",
+ "editor.closeCaptureMode": "关闭捕获模式",
+ "editor.dwvRiserDiagram": "排水立管图",
+ "editor.extrusionHeight": "拉伸高度",
+ "editor.viewerLayout": "查看器布局",
+ "editor.noZonesOnLevel": "此楼层没有区域。",
+ "editor.addOne": "新增",
+ "editor.selectLevelFirst": "请先选择一个楼层",
+ "nodes.block.toolbar.transform": "变换选中组件(G / R / S)",
+ "nodes.block.toolbar.vertexSelect": "顶点选择(1)",
+ "nodes.block.toolbar.edgeSelect": "边选择(2)",
+ "nodes.block.toolbar.faceSelect": "面选择(3)",
+ "nodes.block.toolbar.meshOperations": "网格操作",
+ "nodes.block.toolbar.operations": "操作",
+ "nodes.block.toolbar.loopCut": "循环切割",
+ "nodes.block.toolbar.bevel": "倒角",
+ "nodes.block.toolbar.moveSelection": "移动选择",
+ "nodes.block.toolbar.rotateSelection": "旋转选择",
+ "nodes.block.toolbar.extrudeFaces": "挤出所选面",
+ "nodes.block.toolbar.insetFaces": "内插所选面",
+ "nodes.block.toolbar.loopCutAndSlide": "循环切割并滑动",
+ "nodes.block.toolbar.mergeVertices": "合并顶点",
+ "nodes.block.toolbar.dissolve": "溶解选择",
+ "nodes.block.toolbar.bevelEdges": "倒角所选边",
+ "nodes.block.toolbar.finishEdit": "完成编辑模式(Tab)",
+ "nodes.block.toolbar.selectionAndMore": "选择及其他",
+ "nodes.block.toolbar.selectionActions": "选择操作",
+ "nodes.block.toolbar.selectAll": "全选",
+ "nodes.block.toolbar.invertSelection": "反选",
+ "nodes.block.toolbar.clearSelection": "清除选择",
+ "nodes.block.toolbar.xraySelection": "X 光选择",
+ "nodes.block.toolbar.deleteComponents": "删除组件",
+ "nodes.block.loopCutCount": "循环切割数量",
+ "nodes.block.closeLastOperationPanel": "关闭最后一个操作面板",
+ "nodes.block.adjustLastOp": "调整 {label}(F9)",
+ "nodes.skylight.glassThickness": "玻璃厚度",
+ "nodes.skylight.lanternHeight": "穹顶高度",
+ "nodes.skylight.topScale": "顶部比例",
+ "nodes.skylight.open": "打开",
+ "nodes.skylight.openingAngle": "开启角度",
+ "nodes.skylight.motorHousing": "电机外壳",
+ "nodes.skylight.trackWidth": "轨道宽度",
+ "nodes.skylight.cutoutOffset": "开孔偏移",
+ "nodes.cabinet.baseFlange": "底部法兰",
+ "nodes.cabinet.shelvesInside": "内部搁板",
+ "nodes.cabinet.burnersOn": "炉头启用",
+ "nodes.cabinet.topGrate": "顶部栅格",
+ "nodes.cabinet.baskets": "篮筐",
+ "nodes.cabinet.addLeft": "左侧添加",
+ "nodes.cabinet.addRight": "右侧添加",
+ "nodes.cabinet.showPlinth": "显示踢脚",
+ "nodes.cabinet.plinthHeight": "踢脚高度",
+ "nodes.cabinet.showCountertop": "显示台面",
+ "nodes.cabinet.countertopHeight": "台面高度",
+ "nodes.cabinet.countertopDepth": "台面深度",
+ "nodes.cabinet.seatingOverhang": "座椅悬挑",
+ "nodes.cabinet.finishedBack": "成品背板",
+ "nodes.cabinet.waterfallEnds": "瀑布端",
+ "nodes.cabinet.barCounter": "吧台",
+ "nodes.cabinet.barHeight": "吧台高度",
+ "nodes.cabinet.barDepth": "吧台深度",
+ "nodes.cabinet.sharedPlinthCountertop": "共享踢脚与台面",
+ "nodes.cabinet.islandAndBar": "岛台与吧台",
+ "nodes.cabinet.standardDimensions": "标准尺寸",
+ "nodes.cabinet.appliesStandardDimensions": "将深度、柜体、踢脚和台面厚度应用到该组合。",
+ "nodes.dormer.addWindow": "添加窗户",
+ "nodes.dormer.editWindow": "编辑窗户",
+ "nodes.dormer.moveWindow": "移动窗户",
+ "nodes.dormer.windowsTitle": "窗户({count})",
+ "nodes.dormer.noWindows": "没有窗户",
+ "nodes.dormer.increaseWidth": "增加老虎窗宽度以添加另一扇窗。",
+ "nodes.zone.architecturalRoom": "建筑房间",
+ "nodes.zone.roomName": "房间名称",
+ "nodes.zone.roomNumber": "房间编号",
+ "nodes.zone.enclosure": "围合",
+ "nodes.zone.autoDetect": "自动检测",
+ "nodes.zone.enclosed": "围合",
+ "nodes.zone.open": "敞开",
+ "nodes.zone.occupancy": "用途",
+ "nodes.zone.floorFinish": "地面饰面",
+ "nodes.zone.wallFinish": "墙面饰面",
+ "nodes.zone.ceilingFinish": "顶面饰面",
+ "nodes.zone.ceilingHeight": "吊顶高度",
+ "nodes.zone.clearDimensions": "净空",
+ "nodes.zone.none": "无",
+ "nodes.zone.insideFaces": "内表面",
+ "nodes.zone.finishFaces": "饰面表面",
+ "nodes.zone.wallSurface": "墙面",
+ "nodes.zone.floorSurface": "地面",
+ "nodes.zone.volume": "体积",
+ "nodes.zone.roomDocumentation": "房间文档",
+ "nodes.zone.roomQuantities": "房间工程量",
+ "nodes.zone.zoneQuantities": "区域工程量",
+ "nodes.zone.enclosedRoom": "围合房间",
+ "nodes.zone.footprintOnly": "仅轮廓",
+ "nodes.zone.boundaryUnavailable": "区域边界不可用",
+ "nodes.zone.notProven": "未计算",
+ "nodes.gutter.downspouts": "落水管",
+ "nodes.gutter.addDownspout": "添加落水管",
+ "nodes.gutter.removeDownspout": "移除落水管",
+ "nodes.door.documentation": "文档",
+ "nodes.window.documentation": "文档",
+ "nodes.spawn.actions": "操作",
+ "nodes.ductFitting.swapWH": "交换宽/高",
+ "nodes.ductFitting.swapWidthHeight": "交换宽度和高度",
+ "nodes.boxVent.baseFlange": "底部法兰",
+ "nodes.shared.mark": "标记",
+ "nodes.shared.construction": "构造",
+ "nodes.shared.framed": "框架",
+ "nodes.shared.masonry": "砌体",
+ "nodes.shared.dimensionTo": "尺寸基准",
+ "nodes.shared.nominal": "标称",
+ "nodes.shared.roughOpening": "毛口",
+ "nodes.shared.masonryOpening": "砌体口",
+ "nodes.shared.finishOpening": "完成口",
+ "nodes.shared.roWidth": "毛口宽度",
+ "nodes.shared.roHeight": "毛口高度",
+ "nodes.shared.moWidth": "砌体口宽度",
+ "nodes.shared.moHeight": "砌体口高度",
+ "nodes.shared.foWidth": "完成口宽度",
+ "nodes.shared.foHeight": "完成口高度",
+ "nodes.shared.leaveBlankHint": "在相关制造商或专业承包商验证前,请将 RO、MO 和 FO 值留空。",
+ "nodes.shared.autoAssigned": "自动分配",
+ "nodes.shared.verify": "验证",
+ "panel.section.actions": "操作",
+ "panel.section.position": "位置",
+ "panel.section.rotation": "旋转",
+ "panel.section.scaleOpacity": "缩放与不透明度",
+ "editor.edgeOff": "关",
+ "editor.edgeSoft": "柔和",
+ "editor.edgeStrong": "明显",
+ "editor.edgeOffDetail": "无边线",
+ "editor.edgeSoftDetail": "仅主要折痕有淡轮廓",
+ "editor.edgeStrongDetail": "清晰、不透明的边线",
+ "nodes.skylight.frame": "边框",
+ "nodes.cabinet.modules": "模块",
+ "nodes.cabinet.style": "样式",
+ "nodes.cabinet.back": "后侧",
+ "nodes.cabinet.left": "左侧",
+ "nodes.cabinet.right": "右侧",
+ "common.shelves": "搁板",
+ "common.drawers": "抽屉",
+ "nodes.zone.occupancyUse": "功能 / 用途",
+ "nodes.zone.topViewAriaLabel": "区域边界尺寸的顶视图",
+ "nodes.gutter.downspoutDefaultName": "落水管 {index}",
+ "nodes.elevator.levelFallback": "楼层 {n}",
+ "nodes.measurement.extrusionHeight": "拉伸高度",
+ "common.dismiss": "关闭",
+ "editor.riserDiagram": "排水/排污/通气立管图",
+ "editor.followsLevel": "跟随楼层",
+ "editor.customHeight": "自定义高度",
+ "editor.currentlyLabel": "当前 {value}",
+ "editor.capture": "采集",
+ "editor.top": "顶部",
+ "buildTab.roofSource.conicalHint": "选择一面弧形墙以匹配其半径和弧度。",
+ "nodes.roof.toolHints.placementTooltip": "放置面 — 点击或按 P 键切换",
+ "nodes.roof.defaultName": "屋顶 {count}",
+ "nodes.roof.preview": "屋顶预览"
+}
diff --git a/packages/editor/src/lib/level-display.ts b/packages/editor/src/lib/level-display.ts
new file mode 100644
index 0000000000..11a8aa4def
--- /dev/null
+++ b/packages/editor/src/lib/level-display.ts
@@ -0,0 +1,29 @@
+import type { LevelNode } from '@pascal-app/core'
+
+/**
+ * Resolve a human-readable label for a level in the active locale.
+ *
+ * If the user has named the level, their name wins. Otherwise the
+ * ordinal-shaped defaults (`level.groundFloor`, `level.floor` with the
+ * ordinal parameter, `level.basement` with the magnitude) keep the
+ * HUD in sync with the active language. The old `getLevelDisplayName`
+ * helper in `@pascal-app/core` always returned English defaults and
+ * was removed in favor of this locale-aware version.
+ *
+ * Pass the result of `useTranslations()` in as `t` so the helper stays
+ * callable from non-component contexts (e.g. command palette option
+ * lists, view toggles) without reaching into a hook.
+ */
+export function localizedLevelName(
+ level: LevelNode,
+ t: (key: string, params?: Record) => string,
+): string {
+ return (
+ level.name ||
+ (level.level === 0
+ ? t('level.groundFloor')
+ : level.level > 0
+ ? t('level.floor', { n: level.level })
+ : t('level.basement', { n: -level.level }))
+ )
+}
\ No newline at end of file
diff --git a/packages/editor/src/lib/material-paint.ts b/packages/editor/src/lib/material-paint.ts
index f0b097bc77..2e213965df 100644
--- a/packages/editor/src/lib/material-paint.ts
+++ b/packages/editor/src/lib/material-paint.ts
@@ -75,8 +75,8 @@ function getCatalogEntryForActivePaintMaterial(material: ActivePaintMaterial | n
return getCatalogMaterialById(catalogId)
}
-export function getActivePaintMaterialLabel(material: ActivePaintMaterial | null | undefined) {
- return getCatalogEntryForActivePaintMaterial(material)?.label ?? 'Custom'
+export function getActivePaintMaterialLabelKey(material: ActivePaintMaterial | null | undefined) {
+ return getCatalogEntryForActivePaintMaterial(material)?.labelKey ?? null
}
export function buildRoofSurfaceMaterialPatch(
diff --git a/packages/editor/src/lib/paint-scope.test.ts b/packages/editor/src/lib/paint-scope.test.ts
index 768fdbe11f..0588a0a4da 100644
--- a/packages/editor/src/lib/paint-scope.test.ts
+++ b/packages/editor/src/lib/paint-scope.test.ts
@@ -55,6 +55,18 @@ describe('cyclePaintScope', () => {
})
describe('paintScopeLabel', () => {
+ // Identity translator — tests assert against the i18n key (with any
+ // interpolation substituted), which keeps the suite locale-independent.
+ // The real English/Zh strings live next to the rest of the i18n keys.
+ const identityT = (
+ key: string,
+ vars?: Record,
+ ): string => {
+ if (!vars) return key
+ return `${key} ${Object.entries(vars)
+ .map(([k, v]) => `${k}=${v}`)
+ .join(' ')}`
+ }
const info = (over: Partial): PaintHoverInfo => ({
scopes: ['single'],
slotLabel: 'Seat cushion',
@@ -62,17 +74,23 @@ describe('paintScopeLabel', () => {
...over,
})
it('single shows the hovered slot label', () => {
- expect(paintScopeLabel('single', info({ slotLabel: 'Seat cushion' }))).toBe('Seat cushion')
+ expect(paintScopeLabel('single', info({ slotLabel: 'Seat cushion' }), identityT)).toBe(
+ 'Seat cushion',
+ )
})
it('single falls back when there is no slot label', () => {
- expect(paintScopeLabel('single', info({ slotLabel: '' }))).toBe('This surface')
+ expect(paintScopeLabel('single', info({ slotLabel: '' }), identityT)).toBe(
+ 'helper.paint.thisSurface',
+ )
})
it('object reads "Whole "', () => {
- expect(paintScopeLabel('object', info({ nodeNoun: 'shelf' }))).toBe('Whole shelf')
+ expect(paintScopeLabel('object', info({ nodeNoun: 'shelf' }), identityT)).toBe(
+ 'helper.paint.wholeNoun noun=shelf',
+ )
})
it('matching / room are kind-agnostic', () => {
- expect(paintScopeLabel('matching', info({}))).toBe('All matching')
- expect(paintScopeLabel('room', info({}))).toBe('Room')
+ expect(paintScopeLabel('matching', info({}), identityT)).toBe('helper.paint.allMatching')
+ expect(paintScopeLabel('room', info({}), identityT)).toBe('helper.paint.room')
})
})
diff --git a/packages/editor/src/lib/paint-scope.ts b/packages/editor/src/lib/paint-scope.ts
index eb7c893923..c9fb71993d 100644
--- a/packages/editor/src/lib/paint-scope.ts
+++ b/packages/editor/src/lib/paint-scope.ts
@@ -66,16 +66,22 @@ export function cyclePaintScope(scope: PaintScope, scopes: PaintScope[]): PaintS
return list[(index + 1) % list.length] ?? 'single'
}
-export function paintScopeLabel(scope: PaintScope, info: PaintHoverInfo): string {
+export type Translator = (key: string, vars?: Record) => string
+
+export function paintScopeLabel(
+ scope: PaintScope,
+ info: PaintHoverInfo,
+ t: Translator,
+): string {
switch (scope) {
case 'object':
- return `Whole ${info.nodeNoun}`
+ return t('helper.paint.wholeNoun', { noun: info.nodeNoun })
case 'matching':
- return 'All matching'
+ return t('helper.paint.allMatching')
case 'room':
- return 'Room'
+ return t('helper.paint.room')
default:
- return info.slotLabel || 'This surface'
+ return info.slotLabel || t('helper.paint.thisSurface')
}
}
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/block/definition.ts b/packages/nodes/src/block/definition.ts
index 4ec7bdacd2..c30e8899a4 100644
--- a/packages/nodes/src/block/definition.ts
+++ b/packages/nodes/src/block/definition.ts
@@ -121,11 +121,12 @@ export const blockDefinition: NodeDefinition = {
preview: () => import('./preview'),
tool: () => import('./tool'),
toolHints: [
- { key: 'Left click', label: 'Place block' },
- { key: 'Esc', label: 'Cancel' },
+ { key: 'Left click', label: 'Place block', labelKey: 'nodes.block.toolHints.place' },
+ { key: 'Esc', label: 'Cancel', labelKey: 'common.cancel' },
],
presentation: {
label: 'Block',
+ labelKey: 'panel.nodeType.block',
description: 'A topology-backed solid edited directly in the canvas.',
icon: { kind: 'url', src: '/icons/cube.webp' },
paletteSection: 'structure',
diff --git a/packages/nodes/src/block/panel.tsx b/packages/nodes/src/block/panel.tsx
index 0e0de80149..492a65c7a7 100644
--- a/packages/nodes/src/block/panel.tsx
+++ b/packages/nodes/src/block/panel.tsx
@@ -19,6 +19,8 @@ import {
SliderControl,
triggerSFX,
useInteractionScope,
+ useTranslations,
+ type Translator,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Check, Move, Plus, Trash2 } from 'lucide-react'
@@ -54,12 +56,14 @@ const NEW_BLOCK_SLOT_MATERIAL = {
function materialRefLabel(
ref: string | undefined,
sceneMaterials: ReturnType['materials'],
+ t: Translator,
): string {
const parsed = parseMaterialRef(ref)
- if (!parsed) return 'Default material'
+ if (!parsed) return t('nodes.block.defaultMaterial')
if (parsed.kind === 'scene')
return sceneMaterials[parsed.id as keyof typeof sceneMaterials]?.name ?? ref ?? parsed.id
- return getCatalogMaterialById(parsed.id)?.label ?? ref ?? parsed.id
+ const catalogMaterial = getCatalogMaterialById(parsed.id)
+ return catalogMaterial ? t(catalogMaterial.labelKey) : ref ?? parsed.id
}
function materialRefPreview(
@@ -84,6 +88,7 @@ function materialRefPreview(
}
export default function BlockPanel() {
+ const t = useTranslations()
const selectedId = useViewer((state) => state.selection.selectedIds[0])
const setViewerSelection = useViewer((state) => state.setSelection)
const node = useScene((state) => {
@@ -150,9 +155,9 @@ export default function BlockPanel() {
const slotDeclarations = blockSlots(node)
const canOperateOnFaces = editing && selection.mode === 'face'
const slotEditTitle = !editing
- ? 'Enter Edit Mode to use slot actions'
+ ? t('nodes.block.editModeSlotActions')
: readOnly
- ? 'Scene is read-only'
+ ? t('nodes.block.readOnly')
: undefined
const faceCountBySlot = new Map()
for (const face of node.topology.faces) {
@@ -185,7 +190,7 @@ export default function BlockPanel() {
...current.materials,
[resolution.newSceneMaterial.id as SceneMaterialId]: {
...resolution.newSceneMaterial,
- name: 'Block Accent',
+ name: t('nodes.block.blockAccent'),
},
}
: current.materials,
@@ -202,10 +207,15 @@ export default function BlockPanel() {
})
if (!committed) return
useScene.getState().markDirty(node.id)
- const faceLabel = selectedFaceIds.length === 1 ? 'face' : 'faces'
+ const faceLabel =
+ selectedFaceIds.length === 1 ? t('nodes.block.face') : t('nodes.block.faces')
setSlotNotice({
nodeId: node.id,
- text: `${result.slotNames[result.slotId] ?? result.slotId} applied to ${selectedFaceIds.length} ${faceLabel} with an accent material. Use Paint (P) to replace it.`,
+ text: t('nodes.block.slotApplied', {
+ slot: result.slotNames[result.slotId] ?? result.slotId,
+ count: selectedFaceIds.length,
+ faceLabel,
+ }),
})
triggerSFX('sfx:menu-click')
}
@@ -255,23 +265,30 @@ export default function BlockPanel() {
}
const selectionLabel = !editing
- ? 'Enter Edit Mode to assign faces'
+ ? t('nodes.block.editModeAssignFaces')
: selection.mode !== 'face'
- ? 'Switch to Face Select (3)'
+ ? t('nodes.block.faceSelectSwitch')
: materialSelection.kind === 'empty'
- ? 'No faces selected'
+ ? t('nodes.block.noFacesSelected')
: materialSelection.kind === 'mixed'
- ? `${selectedFaceIds.length} faces · Mixed slots`
- : `${selectedFaceIds.length} ${selectedFaceIds.length === 1 ? 'face' : 'faces'} · ${slotDeclarations.find((slot) => slot.slotId === materialSelection.slotId)?.label ?? materialSelection.slotId}`
+ ? t('nodes.block.mixedSlots', { count: selectedFaceIds.length })
+ : t('nodes.block.singleSlot', {
+ count: selectedFaceIds.length,
+ faceLabel:
+ selectedFaceIds.length === 1 ? t('nodes.block.face') : t('nodes.block.faces'),
+ slotLabel:
+ slotDeclarations.find((slot) => slot.slotId === materialSelection.slotId)
+ ?.label ?? materialSelection.slotId,
+ })
return (
-
-
+
+
{(
[
- { axis: 0, label: 'X', onChange: updatePositionX },
- { axis: 1, label: 'Y', onChange: updatePositionY },
- { axis: 2, label: 'Z', onChange: updatePositionZ },
+ { axis: 0, label: t('common.x'), onChange: updatePositionX },
+ { axis: 1, label: t('common.y'), onChange: updatePositionY },
+ { axis: 2, label: t('common.z'), onChange: updatePositionZ },
] as const
).map(({ axis, label, onChange }) => (
-
+
{selectionLabel}
@@ -296,11 +313,11 @@ export default function BlockPanel() {
className={SLOT_DISABLED_ACTION_CLASS}
disabled={!canOperateOnFaces || selectedFaceIds.length === 0 || readOnly}
icon={ }
- label="Add slot"
+ label={t('nodes.block.addSlot')}
onClick={addMaterialSlot}
title={
slotEditTitle ??
- (selectedFaceIds.length === 0 ? 'Select one or more faces first' : undefined)
+ (selectedFaceIds.length === 0 ? t('nodes.block.selectFacesFirst') : undefined)
}
/>
@@ -323,8 +340,8 @@ export default function BlockPanel() {
const faceCount = faceCountBySlot.get(slot.slotId) ?? 0
const materialLabel =
ref || slot.slotId === BLOCK_BODY_SLOT_ID
- ? materialRefLabel(ref, sceneMaterials)
- : 'Unpainted'
+ ? materialRefLabel(ref, sceneMaterials, t)
+ : t('nodes.block.unpainted')
return (
assignSlot(slot.slotId)}
@@ -353,7 +370,7 @@ export default function BlockPanel() {
- {materialLabel} · {faceCount} {faceCount === 1 ? 'face' : 'faces'}
+ {materialLabel} · {faceCount}{' '}
+ {faceCount === 1 ? t('nodes.block.face') : t('nodes.block.faces')}
@@ -384,10 +402,10 @@ export default function BlockPanel() {
) : (
removeMaterialSlot(slot.slotId)}
- title={slotEditTitle ?? 'Delete material slot and use Body on its faces'}
+ title={slotEditTitle ?? t('nodes.block.slotDeleteTitle')}
type="button"
>
@@ -399,13 +417,13 @@ export default function BlockPanel() {
-
+
- } label="Move" onClick={move} />
+ } label={t('common.move')} onClick={move} />
}
- label="Delete"
+ label={t('common.delete')}
onClick={() => {
useScene.getState().deleteNode(node.id)
setViewerSelection({ selectedIds: [] })
diff --git a/packages/nodes/src/block/parametrics.ts b/packages/nodes/src/block/parametrics.ts
index 195e24286c..2e457fb8af 100644
--- a/packages/nodes/src/block/parametrics.ts
+++ b/packages/nodes/src/block/parametrics.ts
@@ -5,6 +5,7 @@ export const blockParametrics: ParametricDescriptor = {
groups: [
{
label: 'Position',
+ labelKey: 'common.position',
fields: [{ key: 'position', kind: 'vec3' }],
},
],
diff --git a/packages/nodes/src/block/selection.tsx b/packages/nodes/src/block/selection.tsx
index 87d420ebfe..03fa17aa49 100644
--- a/packages/nodes/src/block/selection.tsx
+++ b/packages/nodes/src/block/selection.tsx
@@ -23,6 +23,7 @@ import {
triggerSFX,
useEditor,
useInteractionScope,
+ useTranslations,
} from '@pascal-app/editor'
import { Html } from '@react-three/drei'
import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
@@ -1449,6 +1450,7 @@ function LastOperationPanel({
onClose: () => void
onRepeat: () => void
}) {
+ const t = useTranslations()
return (
{operation.label}
-
Adjust Last Operation · F9
+
+ {t('nodes.block.adjustLastOp')}
+
(null)
const menuScaleRef = useRef(null)
@@ -3671,7 +3676,7 @@ function BlockEditor({
setTransformTool('transform')}
>
@@ -3679,7 +3684,7 @@ function BlockEditor({
switchMode('vertex')}
>
@@ -3687,7 +3692,7 @@ function BlockEditor({
switchMode('edge')}
>
@@ -3695,7 +3700,7 @@ function BlockEditor({
switchMode('face')}
>
@@ -3727,11 +3732,11 @@ function BlockEditor({
{toolbarPanel === 'operations' ? (
-
+
beginKeyboardTransformModal('translate')}
shortcut="G"
>
@@ -3739,7 +3744,7 @@ function BlockEditor({
beginKeyboardTransformModal('rotate')}
shortcut="R"
>
@@ -3747,7 +3752,7 @@ function BlockEditor({
@@ -3755,7 +3760,7 @@ function BlockEditor({
@@ -3765,7 +3770,7 @@ function BlockEditor({
active={loopCutActive}
controls={
}
- label="Loop Cut and Slide"
+ label={t('nodes.block.toolbar.loopCutAndSlide')}
onClick={() => {
playBlockSfx('tool-select')
setTransformTool('loop-cut')
@@ -3791,7 +3796,7 @@ function BlockEditor({
@@ -3799,7 +3804,7 @@ function BlockEditor({
@@ -3808,7 +3813,7 @@ function BlockEditor({
{
playBlockSfx('tool-select')
setBevelSegments(DEFAULT_BEVEL_SEGMENTS)
@@ -3834,14 +3839,14 @@ function BlockEditor({
) : null}
-
+
setToolbarPanel((current) => (current === 'selection' ? null : 'selection'))
}
@@ -3850,12 +3855,12 @@ function BlockEditor({
{toolbarPanel === 'selection' ? (
setXray((value) => !value)}
>
{xray ? : }
@@ -3890,7 +3895,7 @@ function BlockEditor({
= {
move: () => import('./move-tool'),
},
toolHints: [
- { key: 'Left click', label: 'Place box vent on roof' },
- { key: 'Esc', label: 'Cancel' },
+ { key: 'Left click', label: 'Place box vent', labelKey: 'nodes.boxVent.toolHints.place' },
+ { key: 'Esc', label: 'Cancel', labelKey: 'common.cancel' },
],
presentation: {
label: 'Box Vent',
+ labelKey: 'panel.nodeType.boxVent',
description: 'Small louvered exhaust vent that sits on a roof slope.',
icon: { kind: 'url', src: '/icons/box-vent.webp' },
paletteSection: 'structure',
diff --git a/packages/nodes/src/box-vent/geometry.ts b/packages/nodes/src/box-vent/geometry.ts
index dee0ec1521..6ff7e3b1ee 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 { copyUvToSecondaryChannel } from '../shared/primitive-uv'
diff --git a/packages/nodes/src/box-vent/panel.tsx b/packages/nodes/src/box-vent/panel.tsx
index 537496152f..786b2fbc4c 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,24 +169,24 @@ export default function BoxVentPanel() {
icon="/icons/roof.webp"
onBack={node.roofSegmentId ? handleBack : undefined}
onClose={handleClose}
- title={node.name || 'Box Vent'}
+ title={node.name || t('nodes.boxVent.boxVent')}
width={300}
>
-
+
handleUpdate({ style: v as BoxVentNode['style'] })}
options={[
- { label: 'Box', value: 'box' },
- { label: 'Cap', value: 'cap' },
- { label: 'Dome', value: 'dome' },
+ { label: t('nodes.boxVent.box'), value: 'box' },
+ { label: t('nodes.boxVent.cap'), value: 'cap' },
+ { label: t('nodes.boxVent.dome'), value: 'dome' },
]}
value={node.style ?? 'cap'}
/>
-
+
previewProp({ width: v })}
@@ -196,7 +198,7 @@ export default function BoxVentPanel() {
value={Math.round(node.width * 100) / 100}
/>
previewProp({ depth: v })}
@@ -208,7 +210,7 @@ export default function BoxVentPanel() {
value={Math.round(node.depth * 100) / 100}
/>
previewProp({ height: v })}
@@ -225,7 +227,7 @@ export default function BoxVentPanel() {
flare further past the body. */}
{node.style === 'cap' && (
previewProp({ hoodOverhang: v })}
@@ -240,7 +242,7 @@ export default function BoxVentPanel() {
{node.style === 'box' && (
<>
previewProp({ baseInset: v })}
@@ -252,7 +254,7 @@ export default function BoxVentPanel() {
value={Math.round((node.baseInset ?? 0.06) * 1000) / 1000}
/>
previewProp({ baseHeight: v })}
@@ -264,7 +266,7 @@ export default function BoxVentPanel() {
value={Math.round((node.baseHeight ?? 0.04) * 1000) / 1000}
/>
previewProp({ capHeight: v })}
@@ -295,7 +297,7 @@ export default function BoxVentPanel() {
value={Math.round((node.capHeight ?? 0.07) * 1000) / 1000}
/>
previewProp({ capGap: v })}
@@ -307,7 +309,7 @@ export default function BoxVentPanel() {
value={Math.round((node.capGap ?? 0) * 1000) / 1000}
/>
previewProp({ topTaper: v })}
@@ -323,7 +325,7 @@ export default function BoxVentPanel() {
{node.style === 'dome' && (
<>
previewProp({ domeCurvature: v })}
@@ -335,7 +337,7 @@ export default function BoxVentPanel() {
value={Math.round((node.domeCurvature ?? 1.0) * 100) / 100}
/>
previewProp({ hoodOverhang: v })}
@@ -350,9 +352,9 @@ export default function BoxVentPanel() {
)}
-
+
@@ -372,7 +374,7 @@ export default function BoxVentPanel() {
value={Math.round((node.position[0] ?? 0) * 100) / 100}
/>
@@ -415,7 +417,7 @@ export default function BoxVentPanel() {
value={Math.round((node.position[2] ?? 0) * 100) / 100}
/>
previewProp({ rotation: (deg * Math.PI) / 180 })}
@@ -428,18 +430,18 @@ export default function BoxVentPanel() {
/>
-
+
- } 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/box-vent/parametrics.ts b/packages/nodes/src/box-vent/parametrics.ts
index b9d1d16563..43aeae487e 100644
--- a/packages/nodes/src/box-vent/parametrics.ts
+++ b/packages/nodes/src/box-vent/parametrics.ts
@@ -16,17 +16,22 @@ export const boxVentParametrics: ParametricDescriptor = {
groups: [
{
label: 'Style',
+ labelKey: 'common.style',
fields: [
{
key: 'style',
kind: 'enum',
options: ['standard', 'low-profile', 'dome'],
+ optionLabelKeys: {
+ "dome": "nodes.boxVent.dome"
+ },
display: 'segmented',
},
],
},
{
label: 'Dimensions',
+ labelKey: 'common.dimensions',
fields: [
{ key: 'width', kind: 'number', unit: 'm', min: 0.15, max: 1000, step: 0.01 },
{ key: 'depth', kind: 'number', unit: 'm', min: 0.15, max: 1000, step: 0.01 },
diff --git a/packages/nodes/src/box-vent/tool.tsx b/packages/nodes/src/box-vent/tool.tsx
index 27dd2da61f..62a1e4a4c4 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'
@@ -37,6 +37,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)
@@ -111,10 +112,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: getDownSlopeYaw(hit.localX, hit.localZ, hit.segment),
diff --git a/packages/nodes/src/building/definition.ts b/packages/nodes/src/building/definition.ts
index fc76d61ff3..1167acc58a 100644
--- a/packages/nodes/src/building/definition.ts
+++ b/packages/nodes/src/building/definition.ts
@@ -49,6 +49,7 @@ export const buildingDefinition: NodeDefinition = {
presentation: {
label: 'Building',
+ labelKey: 'panel.nodeType.building',
description: 'A building container holding one or more levels.',
icon: { kind: 'url', src: '/icons/building.webp' },
paletteSection: 'site',
diff --git a/packages/nodes/src/cabinet/compartment-card.tsx b/packages/nodes/src/cabinet/compartment-card.tsx
index d98ddf3213..eb86e075c6 100644
--- a/packages/nodes/src/cabinet/compartment-card.tsx
+++ b/packages/nodes/src/cabinet/compartment-card.tsx
@@ -1,6 +1,6 @@
'use client'
-import { SegmentedControl, SliderControl, ToggleControl } from '@pascal-app/editor'
+import { SegmentedControl, SliderControl, ToggleControl, useTranslations } from '@pascal-app/editor'
import { ArrowDown, ArrowUp, Minus, Plus, Trash } from 'lucide-react'
import {
type CabinetCompartment,
@@ -208,6 +208,7 @@ export function CompartmentCard({
allowHood?: boolean
wallCabinet?: boolean
}) {
+ const t = useTranslations()
const type = compartment.type as CabinetCompartmentType
const isFridge = isFridgeCompartmentType(type)
const isHood = isHoodCompartmentType(type)
@@ -278,7 +279,7 @@ export function CompartmentCard({
{total > 1 && !isHood && !isCooktop && type !== 'sink' && (
onReplace(patchCompartment(compartment, { shelfCount: value }))}
@@ -302,7 +303,7 @@ export function CompartmentCard({
{type === 'drawer' && (
onReplace(patchCompartment(compartment, { drawerCount: value }))}
@@ -326,7 +327,7 @@ export function CompartmentCard({
/>
onReplace(patchCompartment(compartment, { shelfCount: value }))}
@@ -410,7 +411,7 @@ export function CompartmentCard({
{
const count = compartmentCooktopElementCount(
compartment,
@@ -430,7 +431,7 @@ export function CompartmentCard({
{type === 'cooktop-gas' && (
onReplace(patchCompartment(compartment, { cooktopShowGrate: checked }))
}
@@ -458,7 +459,7 @@ export function CompartmentCard({
{type === 'pull-out-pantry' && (
onReplace(patchCompartment(compartment, { shelfCount: value }))}
diff --git a/packages/nodes/src/cabinet/definition.ts b/packages/nodes/src/cabinet/definition.ts
index 3e7245c2be..ff0fe43460 100644
--- a/packages/nodes/src/cabinet/definition.ts
+++ b/packages/nodes/src/cabinet/definition.ts
@@ -2209,26 +2209,31 @@ export const cabinetDefinition: NodeDefinition = {
},
tool: () => import('./tool'),
toolHints: [
- { key: 'Click', label: 'Place cabinet' },
+ { key: 'Click', label: 'Place cabinet', labelKey: 'nodes.cabinet.toolHints.place' },
{
key: 'I',
label: 'Placement type',
+ labelKey: 'nodes.cabinet.toolHints.placementType',
chip: {
subscribe: (onChange) => useCabinetPlacementType.subscribe(onChange),
value: () => useCabinetPlacementType.getState().type,
cycle: () => void useCabinetPlacementType.getState().cycleType(),
- labels: { cabinet: 'Type: Cabinet', island: 'Type: Island' },
+ labels: {
+ cabinet: 'nodes.cabinet.toolHints.typeCabinet',
+ island: 'nodes.cabinet.toolHints.typeIsland',
+ },
icons: { cabinet: 'lucide:rectangle-horizontal', island: 'lucide:table-2' },
tooltip: 'Placement type — click or press I to toggle',
},
},
- { key: 'Alt', label: 'Force place' },
- { key: 'R / T', label: 'Rotate' },
- { key: 'Esc', label: 'Cancel run / exit' },
+ { key: 'Alt', label: 'Force place', labelKey: 'editor.forcePlace' },
+ { key: 'R / T', label: 'Rotate', labelKey: 'editor.rotate' },
+ { key: 'Esc', label: 'Cancel run / exit', labelKey: 'common.cancel' },
],
presentation: {
label: 'Modular Cabinet',
+ labelKey: 'panel.nodeType.cabinet',
description: 'A configurable parametric base cabinet.',
icon: { kind: 'url', src: '/icons/item.webp' },
paletteSection: 'furnish',
@@ -2393,6 +2398,7 @@ export const cabinetModuleDefinition: NodeDefinition =
presentation: {
label: 'Cabinet Module',
+ labelKey: 'panel.nodeType.cabinetModule',
description: 'An editable module inside a modular cabinet run.',
icon: { kind: 'url', src: '/icons/item.webp' },
paletteSection: 'furnish',
diff --git a/packages/nodes/src/cabinet/panel.tsx b/packages/nodes/src/cabinet/panel.tsx
index 3bd828137f..c8226af89e 100644
--- a/packages/nodes/src/cabinet/panel.tsx
+++ b/packages/nodes/src/cabinet/panel.tsx
@@ -13,6 +13,7 @@ import {
PanelWrapper,
SegmentedControl,
SliderControl,
+ useTranslations,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { AlertTriangle, Pause, Play, Plus } from 'lucide-react'
@@ -81,39 +82,39 @@ import {
} from './widths'
const HANDLE_STYLE_OPTIONS = [
- { value: 'bar', label: 'Bar' },
- { value: 'knob', label: 'Knob' },
- { value: 'cutout', label: 'Cutout' },
- { value: 'hole', label: 'Hole' },
- { value: 'none', label: 'None' },
+ { value: 'bar', labelKey: 'nodes.cabinet.handle.bar' },
+ { value: 'knob', labelKey: 'nodes.cabinet.handle.knob' },
+ { value: 'cutout', labelKey: 'nodes.cabinet.handle.cutout' },
+ { value: 'hole', labelKey: 'nodes.cabinet.handle.hole' },
+ { value: 'none', labelKey: 'nodes.cabinet.handle.none' },
] as const
const HANDLE_POSITION_OPTIONS = [
- { value: 'auto', label: 'Auto' },
- { value: 'top', label: 'Top' },
- { value: 'center', label: 'Center' },
+ { value: 'auto', labelKey: 'nodes.cabinet.handlePosition.auto' },
+ { value: 'top', labelKey: 'nodes.cabinet.handlePosition.top' },
+ { value: 'center', labelKey: 'nodes.cabinet.handlePosition.center' },
] as const
const FRONT_OVERLAY_OPTIONS = [
- { value: 'full', label: 'Overlay' },
- { value: 'inset', label: 'Inset' },
+ { value: 'full', labelKey: 'nodes.cabinet.frontOverlay.full' },
+ { value: 'inset', labelKey: 'nodes.cabinet.frontOverlay.inset' },
] as const
const FRONT_STYLE_OPTIONS = [
- { value: 'slab', label: 'Slab' },
- { value: 'shaker', label: 'Shaker' },
- { value: 'raised-arch', label: 'Raised Arch' },
+ { value: 'slab', labelKey: 'nodes.cabinet.frontStyle.slab' },
+ { value: 'shaker', labelKey: 'nodes.cabinet.frontStyle.shaker' },
+ { value: 'raised-arch', labelKey: 'nodes.cabinet.frontStyle.raisedArch' },
] as const
const CABINET_TIER_OPTIONS = [
- { value: 'base', label: 'Base Cabinet' },
- { value: 'tall', label: 'Tall Cabinet' },
+ { value: 'base', labelKey: 'nodes.cabinet.tier.base' },
+ { value: 'tall', labelKey: 'nodes.cabinet.tier.tall' },
] as const
const TOP_FINISH_OPTIONS = [
- { value: 'none', label: 'None' },
- { value: 'top-cabinet', label: 'Top Cabinet' },
- { value: 'trim', label: 'Trim / Soffit' },
+ { value: 'none', labelKey: 'nodes.cabinet.topFinish.none' },
+ { value: 'top-cabinet', labelKey: 'nodes.cabinet.topFinish.topCabinet' },
+ { value: 'trim', labelKey: 'nodes.cabinet.topFinish.trim' },
] as const
const EMPTY_MODULES: CabinetModuleNodeType[] = []
@@ -121,10 +122,10 @@ const EMPTY_MODULE_IDS: AnyNodeId[] = []
const PRESET_BUTTON_CLASS =
'flex h-9 items-center justify-center rounded-md border border-border/40 bg-[#252527] px-3 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-border/70 hover:bg-[#303033]'
-const REFLOW_REJECTED_MESSAGE =
- 'No space in this run. No base cabinet can shrink enough to fit this item.'
+const REFLOW_REJECTED_KEY = 'nodes.cabinet.reflowRejected'
export default function CabinetPanel() {
+ const t = useTranslations()
const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection)
const [isAnimating, setIsAnimating] = useState(false)
@@ -180,8 +181,8 @@ export default function CabinetPanel() {
})
const showReflowRejected = useCallback(() => {
- setReflowNotice({ message: REFLOW_REJECTED_MESSAGE })
- }, [])
+ setReflowNotice({ message: t(REFLOW_REJECTED_KEY) })
+ }, [t])
useEffect(() => {
if (selectedId) setReflowNotice(null)
@@ -577,13 +578,13 @@ export default function CabinetPanel() {
icon="/icons/item.webp"
onBack={node.type === 'cabinet-module' ? backToRun : undefined}
onClose={close}
- title={node.name || 'Modular Cabinet'}
+ title={node.name || t('nodes.cabinet.fallbackTitle')}
width={320}
>
{node.type === 'cabinet-module' &&
parentRun?.type === 'cabinet' &&
cabinetModuleSupportsPresets(node) && (
-
+
{CABINET_PRESETS.map((preset) => (
)}
-
+
{node.type === 'cabinet-module' && !isHoodOnlyNode && (
- Standard width
+ {t('nodes.cabinet.standardWidth')}
)}
updateNode({ width: value })}
@@ -634,7 +635,7 @@ export default function CabinetPanel() {
{!isHoodOnlyNode && (
<>
updateNode({ depth: value })}
@@ -644,7 +645,7 @@ export default function CabinetPanel() {
value={node.depth}
/>
{node.type === 'cabinet-module' && parentRun?.type === 'cabinet' && !isHoodOnlyNode && (
-
+
{
@@ -678,17 +679,17 @@ export default function CabinetPanel() {
}}
options={CABINET_TIER_OPTIONS.map((option) => ({
value: option.value,
- label: option.label,
+ label: t(option.labelKey),
}))}
value={resolveCabinetType(node, parentRun)}
/>
{resolveCabinetType(node, parentRun) === 'base' &&
(hasWallCabinet ? (
-
+
) : (
<>
-
-
+
+
>
))}
@@ -696,11 +697,11 @@ export default function CabinetPanel() {
)}
{canAddTopFinish && (
-
+
- Finish
+ {t('nodes.cabinet.finish')}
@@ -712,7 +713,7 @@ export default function CabinetPanel() {
})
}
options={TOP_FINISH_OPTIONS.map((option) => ({
- label: option.label,
+ label: t(option.labelKey),
value: option.value,
}))}
value={node.topFinish ?? 'none'}
@@ -721,7 +722,7 @@ export default function CabinetPanel() {
{node.topFinish !== 'none' && (
<>
updateNode({
topFinishHeight: cabinetCeilingGap(
@@ -732,7 +733,7 @@ export default function CabinetPanel() {
}
/>
updateNode({ topFinishHeight: value })}
@@ -742,7 +743,7 @@ export default function CabinetPanel() {
value={node.topFinishHeight}
/>
updateNode({ topFinishDepth: value })}
@@ -759,7 +760,7 @@ export default function CabinetPanel() {
{planningReport &&
(planningReport.errors.length > 0 || planningReport.warnings.length > 0) && (
-
+
{planningReport.errors.map((planningIssue) => (
+
{
@@ -803,10 +804,10 @@ export default function CabinetPanel() {
= 0.99
- ? 'Close cabinet'
- : 'Open cabinet'
+ ? t('nodes.cabinet.closeCabinet')
+ : t('nodes.cabinet.openCabinet')
}
className="flex h-7 shrink-0 items-center gap-1.5 rounded-lg border border-border/40 bg-[#2C2C2E] px-2.5 text-[11px] font-medium text-foreground transition-colors hover:bg-[#3e3e3e]"
onClick={() => {
@@ -818,23 +819,27 @@ export default function CabinetPanel() {
}}
title={
isAnimating
- ? 'Stop animation'
+ ? t('nodes.cabinet.stopAnimation')
: (node.operationState ?? 0) >= 0.99
- ? 'Close cabinet'
- : 'Play animation'
+ ? t('nodes.cabinet.closeCabinet')
+ : t('nodes.cabinet.playAnimation')
}
type="button"
>
{isAnimating ? : }
- {isAnimating ? 'Stop' : (node.operationState ?? 0) >= 0.99 ? 'Close' : 'Play'}
+ {isAnimating
+ ? t('nodes.cabinet.stop')
+ : (node.operationState ?? 0) >= 0.99
+ ? t('nodes.cabinet.close')
+ : t('nodes.cabinet.play')}
)}
-
+
{reflowNotice ? (
}
- label="Add compartment"
+ label={t('nodes.cabinet.addCompartment')}
onClick={addCompartment}
/>
@@ -877,11 +882,11 @@ export default function CabinetPanel() {
{!isHoodOnlyNode && (
<>
-
+
- Style
+ {t('nodes.cabinet.fronts.style')}
@@ -889,14 +894,14 @@ export default function CabinetPanel() {
}
options={FRONT_STYLE_OPTIONS.map((option) => ({
value: option.value,
- label: option.label,
+ label: t(option.labelKey),
}))}
value={node.frontStyle ?? 'slab'}
/>
- Mounting
+ {t('nodes.cabinet.fronts.mounting')}
@@ -904,14 +909,14 @@ export default function CabinetPanel() {
}
options={FRONT_OVERLAY_OPTIONS.map((option) => ({
value: option.value,
- label: option.label,
+ label: t(option.labelKey),
}))}
value={node.frontOverlay ?? 'full'}
/>
- Reveal gap
+ {t('nodes.cabinet.fronts.revealGap')}
-
+
- Style
+ {t('nodes.cabinet.handles.style')}
@@ -946,7 +951,7 @@ export default function CabinetPanel() {
}
options={HANDLE_STYLE_OPTIONS.map((option) => ({
value: option.value,
- label: option.label,
+ label: t(option.labelKey),
}))}
value={node.handleStyle}
/>
@@ -954,7 +959,7 @@ export default function CabinetPanel() {
{(node.handleStyle === 'bar' || node.handleStyle === 'knob') && (
- Position
+ {t('nodes.cabinet.handles.position')}
@@ -962,7 +967,7 @@ export default function CabinetPanel() {
}
options={HANDLE_POSITION_OPTIONS.map((option) => ({
value: option.value,
- label: option.label,
+ label: t(option.labelKey),
}))}
value={node.handlePosition ?? 'auto'}
/>
diff --git a/packages/nodes/src/cabinet/parametrics.ts b/packages/nodes/src/cabinet/parametrics.ts
index ce253b1752..3f7970a21c 100644
--- a/packages/nodes/src/cabinet/parametrics.ts
+++ b/packages/nodes/src/cabinet/parametrics.ts
@@ -5,6 +5,7 @@ export const cabinetParametrics: ParametricDescriptor = {
groups: [
{
label: 'Dimensions',
+ labelKey: 'common.dimensions',
fields: [
{ key: 'width', kind: 'number', unit: 'm', min: 0.3, max: 3, step: 0.05 },
{ key: 'depth', kind: 'number', unit: 'm', min: 0.3, max: 1.2, step: 0.01 },
@@ -13,6 +14,7 @@ export const cabinetParametrics: ParametricDescriptor = {
},
{
label: 'Position',
+ labelKey: 'common.position',
fields: [{ key: 'position', kind: 'vec3' }],
},
],
@@ -30,6 +32,7 @@ export const cabinetModuleParametrics: ParametricDescriptor =
groups: [
{
label: 'Dimensions',
+ labelKey: 'common.dimensions',
fields: [
{ key: 'width', kind: 'number', unit: 'm', min: 0.3, max: 3, step: 0.05 },
{ key: 'depth', kind: 'number', unit: 'm', min: 0.3, max: 1.2, step: 0.01 },
@@ -38,6 +41,7 @@ export const cabinetModuleParametrics: ParametricDescriptor =
},
{
label: 'Position',
+ labelKey: 'common.position',
fields: [{ key: 'position', kind: 'vec3' }],
},
],
diff --git a/packages/nodes/src/cabinet/run-panel.tsx b/packages/nodes/src/cabinet/run-panel.tsx
index 94b62e8e0b..d309c7fa1c 100644
--- a/packages/nodes/src/cabinet/run-panel.tsx
+++ b/packages/nodes/src/cabinet/run-panel.tsx
@@ -14,6 +14,7 @@ import {
SegmentedControl,
SliderControl,
ToggleControl,
+ useTranslations,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Plus, Trash } from 'lucide-react'
@@ -404,6 +405,7 @@ export function CabinetRunPanel({
modules: CabinetModuleNodeType[]
onClose: () => void
}) {
+ const t = useTranslations()
const setSelection = useViewer((s) => s.setSelection)
const sortedModules = useMemo(
() => [...modules].sort((a, b) => a.position[0] - b.position[0]),
@@ -464,7 +466,7 @@ export function CabinetRunPanel({
title={node.name || 'Modular Cabinet'}
width={320}
>
-
+
{sortedModules.map((module, index) => (
}
- label="Add left"
+ label={t('nodes.cabinet.addLeft')}
onClick={() => addModule('left')}
/>
}
- label="Add right"
+ label={t('nodes.cabinet.addRight')}
onClick={() => addModule('right')}
/>
-
+
{node.runTier === 'base' && (
- Standard dimensions
+ {t('nodes.cabinet.standardDimensions')}
- Applies depth, carcass, plinth, and countertop thickness to this run.
+ {t('nodes.cabinet.appliesStandardDimensions')}
)}
updateRun({ depth: value })}
@@ -542,7 +544,7 @@ export function CabinetRunPanel({
value={node.depth}
/>
minCabinetCarcassHeightForStack(module)))}
onChange={(value) => updateRun({ carcassHeight: value })}
@@ -553,12 +555,12 @@ export function CabinetRunPanel({
/>
updateRun({ showPlinth: checked })}
/>
{node.showPlinth && (
updateRun({ plinthHeight: value })}
@@ -570,13 +572,13 @@ export function CabinetRunPanel({
)}
updateRun({ withCountertop: checked })}
/>
{node.withCountertop && (
<>
updateRun({ countertopThickness: value })}
@@ -586,7 +588,7 @@ export function CabinetRunPanel({
value={node.countertopThickness}
/>
updateRun({ countertopOverhang: value })}
@@ -600,11 +602,11 @@ export function CabinetRunPanel({
-
+
{node.withCountertop && node.barLedge?.edge !== 'back' && (
updateRun({ countertopBackOverhang: value })}
@@ -616,19 +618,19 @@ export function CabinetRunPanel({
)}
updateRun({ withFinishedBack: checked })}
/>
{node.withCountertop && (
updateRun({ withWaterfall: checked })}
/>
)}
updateRun({
barLedge: checked ? { edge: 'back', height: 1.06, depth: 0.35 } : undefined,
@@ -644,14 +646,14 @@ export function CabinetRunPanel({
})
}
options={[
- { value: 'back', label: 'Back' },
- { value: 'left', label: 'Left' },
- { value: 'right', label: 'Right' },
+ { value: 'back', label: t('nodes.cabinet.back') },
+ { value: 'left', label: t('nodes.cabinet.left') },
+ { value: 'right', label: t('nodes.cabinet.right') },
]}
value={node.barLedge.edge}
/>
updateRun({ barLedge: { ...node.barLedge!, height: value } })}
@@ -661,7 +663,7 @@ export function CabinetRunPanel({
value={node.barLedge.height}
/>
updateRun({ barLedge: { ...node.barLedge!, depth: value } })}
@@ -675,11 +677,11 @@ export function CabinetRunPanel({
-
+
- Style
+ {t('nodes.cabinet.style')}
@@ -732,7 +734,7 @@ export function CabinetRunPanel({
-
+
diff --git a/packages/nodes/src/ceiling/definition.ts b/packages/nodes/src/ceiling/definition.ts
index f362f83166..851a81e7a6 100644
--- a/packages/nodes/src/ceiling/definition.ts
+++ b/packages/nodes/src/ceiling/definition.ts
@@ -214,13 +214,14 @@ export const ceilingDefinition: NodeDefinition
= {
},
toolHints: [
- { key: 'Left click', label: 'Trace ceiling outline' },
- { key: 'Enter', label: 'Finish ceiling', minDraftVertices: 3 },
- { key: 'Esc', label: 'Cancel' },
+ { key: 'Left click', label: 'Trace ceiling outline', labelKey: 'nodes.ceiling.toolHints.trace' },
+ { key: 'Enter', label: 'Finish ceiling', labelKey: 'nodes.ceiling.toolHints.finish', minDraftVertices: 3 },
+ { key: 'Esc', label: 'Cancel', labelKey: 'common.cancel' },
],
presentation: {
label: 'Ceiling',
+ labelKey: 'panel.nodeType.ceiling',
description: 'A polygon-bounded ceiling surface that hosts ceiling-mounted items.',
icon: { kind: 'url', src: '/icons/ceiling.webp' },
paletteSection: 'structure',
diff --git a/packages/nodes/src/ceiling/panel.tsx b/packages/nodes/src/ceiling/panel.tsx
index 1f8772954d..f5fff38d82 100644
--- a/packages/nodes/src/ceiling/panel.tsx
+++ b/packages/nodes/src/ceiling/panel.tsx
@@ -20,6 +20,7 @@ import {
useEditingHole,
useEditor,
useInteractionScope,
+ useTranslations,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
@@ -34,6 +35,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 unit = useViewer((s) => s.unit)
const metricNotation = useViewer((s) => s.metricNotation)
@@ -227,39 +229,41 @@ export function CeilingPanel() {
const heightPresets =
unit === 'imperial'
? [
- { label: 'Low (8\'0")', height: 2.4384 },
- { label: 'Standard (8\'6")', height: 2.5908 },
- { label: 'High (9\'0")', height: 2.7432 },
+ { labelKey: 'nodes.ceiling.heightPresets.lowImperial', height: 2.4384 },
+ { labelKey: 'nodes.ceiling.heightPresets.standardImperial', height: 2.5908 },
+ { labelKey: 'nodes.ceiling.heightPresets.highImperial', height: 2.7432 },
]
: [
- { label: 'Low (2.4m)', height: 2.4 },
- { label: 'Standard (2.5m)', height: 2.5 },
- { label: 'High (3.0m)', height: 3.0 },
+ { labelKey: 'nodes.ceiling.heightPresets.low', height: 2.4 },
+ { labelKey: 'nodes.ceiling.heightPresets.standard', height: 2.5 },
+ { labelKey: 'nodes.ceiling.heightPresets.high', height: 3.0 },
]
return (
-
+
{isFollows ? (
- Currently {formatLinearMeasurement(resolvedHeight, unit, metricNotation)}
+ {t('nodes.ceiling.currently', {
+ value: formatLinearMeasurement(resolvedHeight, unit, metricNotation),
+ })}
) : (
handleHeightChange(preset.height)}
title={
fits
? undefined
- : `Taller than this level (${formatLinearMeasurement(maxHeight, unit, metricNotation)} available). Raise the level height first.`
+ : t('nodes.ceiling.tooTall', {
+ available: formatLinearMeasurement(maxHeight, unit, metricNotation),
+ })
}
/>
)
@@ -295,20 +301,21 @@ export function CeilingPanel() {
{Number.isFinite(maxHeight) && (
- Limited by the level to {formatLinearMeasurement(maxHeight, unit, metricNotation)} —
- raise the level height for a taller ceiling.
+ {t('nodes.ceiling.limitedBy', {
+ available: formatLinearMeasurement(maxHeight, unit, metricNotation),
+ })}
)}
-
+
- Area
+ {t('common.area')}
{area.toFixed(2)} m²
-
+
{node.holes && node.holes.length > 0 ? (
{node.holes.map((hole, index) => {
@@ -317,7 +324,10 @@ 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('nodes.ceiling.editing')})`}
{holeArea.toFixed(2)} m² · {hole.length} pts ·{' '}
- {isAutoHole ? autoLabel : 'Manual'}
+ {isAutoHole ? autoLabel : t('nodes.ceiling.manual')}
{isEditing ? (
useInteractionScope
.getState()
@@ -353,7 +364,7 @@ export function CeilingPanel() {
/>
) : isAutoHole ? (
- Auto
+ {t('nodes.ceiling.auto')}
) : (
<>
@@ -379,7 +390,9 @@ export function CeilingPanel() {
})}
) : (
-
No holes
+
+ {t('nodes.ceiling.noHoles')}
+
)}
@@ -387,14 +400,18 @@ 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/parametrics.ts b/packages/nodes/src/ceiling/parametrics.ts
index 894e362f81..76405fb2c9 100644
--- a/packages/nodes/src/ceiling/parametrics.ts
+++ b/packages/nodes/src/ceiling/parametrics.ts
@@ -12,6 +12,7 @@ export const ceilingParametrics: ParametricDescriptor
= {
groups: [
{
label: 'Dimensions',
+ labelKey: 'common.dimensions',
fields: [{ key: 'height', kind: 'number', unit: 'm', min: 0.1, max: 1000, step: 0.05 }],
},
],
diff --git a/packages/nodes/src/chimney/definition.ts b/packages/nodes/src/chimney/definition.ts
index a1bd20615e..20127eacf0 100644
--- a/packages/nodes/src/chimney/definition.ts
+++ b/packages/nodes/src/chimney/definition.ts
@@ -403,12 +403,13 @@ export const chimneyDefinition: NodeDefinition = {
tool: () => import('./tool'),
toolHints: [
- { key: 'Left click', label: 'Place chimney on roof' },
- { key: 'Esc', label: 'Cancel' },
+ { key: 'Left click', label: 'Place chimney on roof', labelKey: 'nodes.chimney.toolHints.place' },
+ { key: 'Esc', label: 'Cancel', labelKey: 'common.cancel' },
],
presentation: {
label: 'Chimney',
+ labelKey: 'panel.nodeType.chimney',
description: 'Vertical masonry stack on a roof segment.',
icon: { kind: 'url', src: '/icons/chimney.webp' },
paletteSection: 'structure',
diff --git a/packages/nodes/src/chimney/geometry.ts b/packages/nodes/src/chimney/geometry.ts
index 540b8d6807..6139303123 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 a260f06fd0..d825bd28e6 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 d706dbd5e8..a7e42035e9 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'
@@ -39,16 +40,17 @@ 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' },
+const CHIMNEY_TYPE_OPTIONS: Array<{ labelKey: string; value: ChimneyType }> = [
+ { labelKey: 'nodes.chimney.cap', value: 'cap' },
+ { labelKey: 'nodes.chimney.flues', value: 'flues' },
+ { labelKey: 'nodes.chimney.shoulder', value: 'shoulder' },
+ { labelKey: 'nodes.chimney.bands', value: 'bands' },
+ { labelKey: 'nodes.chimney.cricket', value: 'cricket' },
+ { labelKey: 'nodes.chimney.panels', value: 'panels' },
]
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)
@@ -342,10 +344,10 @@ export default function ChimneyPanel() {
icon="/icons/roof.webp"
onBack={node.roofSegmentId ? handleBack : undefined}
onClose={handleClose}
- title={node.name || 'Chimney'}
+ title={node.name || t('nodes.chimney.chimney')}
width={300}
>
-
+
applyPreset(v as ChimneyPresetKey)}
options={CHIMNEY_PRESET_KEYS.map((k) => ({
@@ -358,17 +360,17 @@ 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'}
/>
previewProp({ width: v })}
@@ -381,7 +383,7 @@ export default function ChimneyPanel() {
/>
{(node.bodyShape ?? 'square') !== 'round' && (
previewProp({ depth: v })}
@@ -394,7 +396,7 @@ export default function ChimneyPanel() {
/>
)}
previewProp({ bodyHollowDepth: v })}
@@ -406,7 +408,7 @@ export default function ChimneyPanel() {
value={Math.round((node.bodyHollowDepth ?? 0.6) * 100) / 100}
/>
previewProp({ bodyHollowMargin: v })}
@@ -419,7 +421,7 @@ export default function ChimneyPanel() {
/>
{(node.bodyShape ?? 'square') !== 'round' && (
previewProp({ cornerBevel: v })}
@@ -433,9 +435,9 @@ export default function ChimneyPanel() {
)}
-
+
previewProp({ heightAboveRidge: v })}
@@ -447,7 +449,7 @@ export default function ChimneyPanel() {
value={Math.round(node.heightAboveRidge * 100) / 100}
/>
previewProp({ cutoutOffset: v })}
@@ -460,9 +462,9 @@ export default function ChimneyPanel() {
/>
-
+
{
@@ -481,7 +483,7 @@ export default function ChimneyPanel() {
value={Math.round(worldX_now * 100) / 100}
/>
{
@@ -497,7 +499,7 @@ export default function ChimneyPanel() {
value={Math.round(worldZ_now * 100) / 100}
/>
{
@@ -523,7 +525,7 @@ export default function ChimneyPanel() {
/>
-
+
{CHIMNEY_TYPE_OPTIONS.filter((option) => {
// Cricket and Panels both rely on a flat face — hide them for
@@ -546,7 +548,7 @@ export default function ChimneyPanel() {
onClick={() => setChimneyType(option.value)}
type="button"
>
-
{option.label}
+
{t(option.labelKey)}
)
})}
@@ -558,17 +560,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 })}
@@ -580,7 +582,7 @@ export default function ChimneyPanel() {
value={Math.round((node.capOverhang ?? 0.04) * 1000) / 1000}
/>
previewProp({ capThickness: v })}
@@ -602,16 +604,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 })}
@@ -623,7 +625,7 @@ export default function ChimneyPanel() {
value={Math.round((node.shoulderHeight ?? 0.5) * 100) / 100}
/>
previewProp({ shoulderExtent: v })}
@@ -642,7 +644,7 @@ export default function ChimneyPanel() {
{chimneyType === 'flues' && (
<>
previewProp({ flueCount: Math.round(v) })}
@@ -658,13 +660,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 })}
@@ -676,7 +678,7 @@ export default function ChimneyPanel() {
value={Math.round((node.flueDiameter ?? 0.22) * 100) / 100}
/>
previewProp({ flueHeight: v })}
@@ -689,7 +691,7 @@ export default function ChimneyPanel() {
/>
{(node.flueCount ?? 1) > 1 && (
previewProp({ flueSpacing: v })}
@@ -701,7 +703,7 @@ export default function ChimneyPanel() {
/>
)}
previewProp({ flueWallThickness: v })}
@@ -723,16 +725,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 })}
@@ -744,7 +746,7 @@ export default function ChimneyPanel() {
value={Math.round((node.bandHeight ?? 0.1) * 100) / 100}
/>
previewProp({ bandExtent: v })}
@@ -756,7 +758,7 @@ export default function ChimneyPanel() {
value={Math.round((node.bandExtent ?? 0.04) * 1000) / 1000}
/>
previewProp({ bandOffset: v })}
@@ -778,8 +780,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'}
/>
@@ -789,13 +791,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 })}
@@ -807,7 +809,7 @@ export default function ChimneyPanel() {
value={Math.round((node.cricketLength ?? 0.6) * 100) / 100}
/>
previewProp({ cricketHeight: v })}
@@ -829,15 +831,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 })}
@@ -849,7 +851,7 @@ export default function ChimneyPanel() {
value={Math.round((node.panelDepth ?? 0.03) * 1000) / 1000}
/>
previewProp({ panelHeight: v })}
@@ -861,7 +863,7 @@ export default function ChimneyPanel() {
value={Math.round((node.panelHeight ?? 0.8) * 100) / 100}
/>
previewProp({ panelOffsetTop: v })}
@@ -873,7 +875,7 @@ export default function ChimneyPanel() {
value={Math.round((node.panelOffsetTop ?? 0.15) * 100) / 100}
/>
previewProp({ panelMargin: v })}
@@ -890,12 +892,12 @@ export default function ChimneyPanel() {
)}
-
+
}
- label="Delete"
+ label={t('common.delete')}
onClick={handleDelete}
/>
diff --git a/packages/nodes/src/chimney/parametrics.ts b/packages/nodes/src/chimney/parametrics.ts
index fafe128386..1e417e46a1 100644
--- a/packages/nodes/src/chimney/parametrics.ts
+++ b/packages/nodes/src/chimney/parametrics.ts
@@ -11,11 +11,16 @@ export const chimneyParametrics: ParametricDescriptor = {
groups: [
{
label: 'Body',
+ labelKey: 'nodes.chimney.body',
fields: [
{
key: 'bodyShape',
kind: 'enum',
options: ['square', 'round'],
+ optionLabelKeys: {
+ "square": "nodes.chimney.square",
+ "round": "nodes.chimney.round"
+ },
display: 'segmented',
},
{ key: 'width', kind: 'number', unit: 'm', min: 0.2, max: 1000, step: 0.05 },
@@ -42,11 +47,17 @@ export const chimneyParametrics: ParametricDescriptor = {
},
{
label: 'Shoulder',
+ labelKey: 'nodes.chimney.shoulder',
fields: [
{
key: 'shoulderStyle',
kind: 'enum',
options: ['none', 'tapered', 'corbeled'],
+ optionLabelKeys: {
+ "none": "nodes.chimney.none",
+ "tapered": "nodes.chimney.tapered",
+ "corbeled": "nodes.chimney.corbeled"
+ },
display: 'segmented',
},
{
@@ -71,12 +82,19 @@ export const chimneyParametrics: ParametricDescriptor = {
},
{
label: 'Cap',
+ labelKey: 'nodes.chimney.cap',
fields: [
{ key: 'cap', kind: 'boolean' },
{
key: 'capShape',
kind: 'enum',
options: ['none', 'sloped', 'flat', 'stepped'],
+ optionLabelKeys: {
+ "none": "nodes.chimney.none",
+ "sloped": "nodes.chimney.sloped",
+ "flat": "nodes.chimney.flat",
+ "stepped": "nodes.chimney.stepped"
+ },
display: 'segmented',
visibleIf: (n) => n.cap === true,
},
@@ -102,12 +120,17 @@ export const chimneyParametrics: ParametricDescriptor = {
},
{
label: 'Flues',
+ labelKey: 'nodes.chimney.flues',
fields: [
{ key: 'flueCount', kind: 'number', min: 0, max: 4, step: 1 },
{
key: 'flueShape',
kind: 'enum',
options: ['round', 'square'],
+ optionLabelKeys: {
+ "round": "nodes.chimney.round",
+ "square": "nodes.chimney.square"
+ },
display: 'segmented',
visibleIf: (n) => n.flueCount > 0,
},
@@ -141,11 +164,16 @@ export const chimneyParametrics: ParametricDescriptor = {
},
{
label: 'Cricket',
+ labelKey: 'nodes.chimney.cricket',
fields: [
{
key: 'cricketStyle',
kind: 'enum',
options: ['none', 'simple'],
+ optionLabelKeys: {
+ "none": "nodes.chimney.none",
+ "simple": "nodes.chimney.simple"
+ },
display: 'segmented',
visibleIf: (n) => n.bodyShape === 'square',
},
@@ -153,6 +181,10 @@ export const chimneyParametrics: ParametricDescriptor = {
key: 'cricketSide',
kind: 'enum',
options: ['front', 'back'],
+ optionLabelKeys: {
+ "front": "nodes.chimney.front",
+ "back": "nodes.chimney.back"
+ },
display: 'segmented',
visibleIf: (n) => n.bodyShape === 'square' && n.cricketStyle !== 'none',
},
diff --git a/packages/nodes/src/chimney/roof-trim.ts b/packages/nodes/src/chimney/roof-trim.ts
index 225b0c96d9..b311a8eae6 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/column/definition.ts b/packages/nodes/src/column/definition.ts
index b07d4090b5..f44b3567ed 100644
--- a/packages/nodes/src/column/definition.ts
+++ b/packages/nodes/src/column/definition.ts
@@ -411,8 +411,8 @@ export const columnDefinition: NodeDefinition = {
// registry-first path mounts this and skips the legacy ``.
tool: () => import('./tool'),
toolHints: [
- { key: 'Left click', label: 'Place column' },
- { key: 'Esc', label: 'Cancel' },
+ { key: 'Left click', label: 'Place column', labelKey: 'nodes.column.toolHints.place' },
+ { key: 'Esc', label: 'Cancel', labelKey: 'common.cancel' },
],
computeFloorplanLevelData: computeColumnFloorplanLevelData,
floorplanDependsOnSiblings: true,
@@ -435,6 +435,7 @@ export const columnDefinition: NodeDefinition = {
presentation: {
label: 'Column',
+ labelKey: 'panel.nodeType.column',
description: 'A parametric column with configurable cross-section, base, and capital.',
icon: { kind: 'url', src: '/icons/column.webp' },
paletteSection: 'structure',
diff --git a/packages/nodes/src/column/panel.tsx b/packages/nodes/src/column/panel.tsx
index 519c5474b6..a98335a093 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'
@@ -46,6 +47,7 @@ const COLUMN_PRESET_OPTIONS = Object.entries(COLUMN_PRESETS).map(([value, preset
const COLUMN_PROPORTION_PRESETS = {
slender: {
label: 'Slender',
+ labelKey: 'nodes.column.slender' as const,
height: 3.6,
width: 0.34,
baseHeight: 0.18,
@@ -56,6 +58,7 @@ const COLUMN_PROPORTION_PRESETS = {
},
standard: {
label: 'Standard',
+ labelKey: 'nodes.column.standard' as const,
height: 2.9,
width: 0.44,
baseHeight: 0.22,
@@ -66,6 +69,7 @@ const COLUMN_PROPORTION_PRESETS = {
},
heavy: {
label: 'Heavy',
+ labelKey: 'nodes.column.heavy' as const,
height: 3,
width: 0.58,
baseHeight: 0.28,
@@ -76,6 +80,7 @@ const COLUMN_PROPORTION_PRESETS = {
},
stout: {
label: 'Short / Stout',
+ labelKey: 'nodes.column.shortStout' as const,
height: 2.2,
width: 0.62,
baseHeight: 0.3,
@@ -92,21 +97,22 @@ const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
([value, preset]) => ({
value: value as ColumnProportionPresetId,
label: preset.label,
+ labelKey: preset.labelKey,
}),
)
-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' },
+const SUPPORT_STYLE_OPTIONS: Array<{ labelKey: string; value: ColumnNode['supportStyle'] }> = [
+ { labelKey: 'nodes.column.vertical', value: 'vertical' },
+ { labelKey: 'nodes.column.aFrame', value: 'a-frame' },
+ { labelKey: 'nodes.column.yFrame', value: 'y-frame' },
+ { labelKey: 'nodes.column.vFrame', value: 'v-frame' },
+ { labelKey: 'nodes.column.xBrace', value: 'x-brace' },
+ { labelKey: 'nodes.column.kBrace', value: 'k-brace' },
+ { labelKey: 'nodes.column.singleStrut', value: 'single-strut' },
+ { labelKey: 'nodes.column.tripod', value: 'tripod' },
+ { labelKey: 'nodes.column.trestle', value: 'trestle' },
+ { labelKey: 'nodes.column.portalFrame', value: 'portal-frame' },
+ { labelKey: 'nodes.column.boxFrame', value: 'box-frame' },
]
type NonVerticalSupportStyle = Exclude
@@ -287,6 +293,7 @@ function shaftProfileUpdates(shaftProfile: ColumnNode['shaftProfile']): Partial<
}
export default function ColumnPanel() {
+ 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)
@@ -347,10 +354,10 @@ export default function ColumnPanel() {
-
+
{
@@ -359,7 +366,7 @@ export default function ColumnPanel() {
}}
value=""
>
- Apply preset...
+ {t('nodes.column.applyPreset')}
{COLUMN_PRESET_OPTIONS.map((option) => (
{option.label}
@@ -368,7 +375,7 @@ export default function ColumnPanel() {
-
+
{SUPPORT_STYLE_OPTIONS.map((option) => {
const isSelected = supportStyle === option.value
@@ -404,7 +411,7 @@ export default function ColumnPanel() {
}}
type="button"
>
- {option.label}
+ {t(option.labelKey)}
)
})}
@@ -412,7 +419,7 @@ export default function ColumnPanel() {
{isBraceSupport ? (
<>
handleUpdate({ braceWidth: value, width: value })}
@@ -422,7 +429,7 @@ export default function ColumnPanel() {
value={node.braceWidth ?? node.width}
/>
handleUpdate({ braceDepth: value, depth: value })}
@@ -439,7 +446,7 @@ export default function ColumnPanel() {
[
{
value: 'round',
- label: 'Round',
+ labelKey: 'nodes.column.round' as const,
icon: (
{
@@ -526,14 +533,14 @@ export default function ColumnPanel() {
{option.icon}
- {option.label}
+ {t(option.labelKey)}
)
})}
handleUpdate({ edgeSoftness: value })}
@@ -544,7 +551,7 @@ export default function ColumnPanel() {
/>
{(node.crossSection === 'square' || node.crossSection === 'rectangular') && (
handleUpdate({ shaftCornerRadius: value })}
@@ -558,7 +565,7 @@ export default function ColumnPanel() {
)}
-
+
{managedByLeanTo && (
Height and footprint are controlled by the lean-to extension. Rotate or change the
@@ -574,17 +581,17 @@ export default function ColumnPanel() {
}}
value=""
>
- Apply proportion...
+ {t('nodes.column.applyProportion')}
{COLUMN_PROPORTION_OPTIONS.map((option) => (
- {option.label}
+ {t(option.labelKey)}
))}
)}
{!managedByLeanTo && (
handleUpdate({ height: value })}
@@ -605,7 +612,7 @@ export default function ColumnPanel() {
supportStyle === 'portal-frame' ||
supportStyle === 'box-frame') && (
@@ -624,7 +631,7 @@ export default function ColumnPanel() {
/>
)}
handleUpdate({ bracePlateEnabled: checked })}
/>
>
) : !managedByLeanTo ? (
<>
@@ -683,7 +690,7 @@ export default function ColumnPanel() {
/>
{node.crossSection === 'rectangular' && (
handleUpdate({ depth: value })}
@@ -698,7 +705,7 @@ export default function ColumnPanel() {
{!isBraceSupport && (
-
+
@@ -706,14 +713,14 @@ export default function ColumnPanel() {
}
value={shaftProfile}
>
- Straight
- Tapered
- Bulged
- Hourglass
+ {t('nodes.column.straight')}
+ {t('nodes.column.tapered')}
+ {t('nodes.column.bulged')}
+ {t('nodes.column.hourglass')}
{shaftProfile === 'straight' && (
handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
@@ -725,7 +732,7 @@ export default function ColumnPanel() {
{shaftProfile === 'tapered' && (
<>
handleUpdate({ shaftStartScale: value })}
@@ -734,7 +741,7 @@ export default function ColumnPanel() {
value={node.shaftStartScale ?? 0.82}
/>
handleUpdate({ shaftEndScale: value })}
@@ -743,7 +750,7 @@ export default function ColumnPanel() {
value={node.shaftEndScale ?? 0.72}
/>
handleUpdate({ shaftTaper: value })}
@@ -756,7 +763,7 @@ export default function ColumnPanel() {
{shaftProfile === 'bulged' && (
<>
handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
@@ -765,7 +772,7 @@ export default function ColumnPanel() {
value={node.shaftStartScale ?? 0.68}
/>