From 6eb497206d37dec572b8a8906c4260fdcd4a8062 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:20:59 +0000 Subject: [PATCH 01/23] feat(core): add localization registry and formatting Co-authored-by: Aymeric Rabot --- apps/editor/app/client-bootstrap.tsx | 13 ++ packages/core/package.json | 5 + packages/core/src/i18n/format.ts | 98 +++++++++++ packages/core/src/i18n/i18n.test.tsx | 137 +++++++++++++++ packages/core/src/i18n/index.ts | 20 +++ packages/core/src/i18n/registry.ts | 156 ++++++++++++++++++ packages/core/src/i18n/use-translate.ts | 33 ++++ packages/core/src/index.ts | 1 + .../editor/src/components/editor/index.tsx | 7 + .../components/ui/controls/slider-control.tsx | 6 +- packages/editor/src/index.tsx | 16 ++ packages/editor/src/lib/measurement-parser.ts | 5 +- packages/editor/src/lib/measurements.ts | 14 +- 13 files changed, 501 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/i18n/format.ts create mode 100644 packages/core/src/i18n/i18n.test.tsx create mode 100644 packages/core/src/i18n/index.ts create mode 100644 packages/core/src/i18n/registry.ts create mode 100644 packages/core/src/i18n/use-translate.ts diff --git a/apps/editor/app/client-bootstrap.tsx b/apps/editor/app/client-bootstrap.tsx index 821544fec6..5313824e6e 100644 --- a/apps/editor/app/client-bootstrap.tsx +++ b/apps/editor/app/client-bootstrap.tsx @@ -9,8 +9,14 @@ // `loaded` guard inside `../lib/bootstrap` keeps the side effect // idempotent under HMR. import '../lib/bootstrap' +import { getLocale, setLocale, subscribe } from '@pascal-app/core' import { type ReactNode, useEffect } from 'react' +if (typeof window !== 'undefined') { + const queryLocale = new URLSearchParams(window.location.search).get('lang') + if (queryLocale) setLocale(queryLocale) +} + export function ClientBootstrap({ children, enableDevDiagnostics, @@ -22,5 +28,12 @@ export function ClientBootstrap({ if (!enableDevDiagnostics) return import('react-scan').then(({ scan }) => scan({ enabled: true })) }, [enableDevDiagnostics]) + useEffect(() => { + const syncDocumentLocale = () => { + document.documentElement.lang = getLocale() + } + syncDocumentLocale() + return subscribe(syncDocumentLocale) + }, []) return children } diff --git a/packages/core/package.json b/packages/core/package.json index c4b9c92cee..c3aaf4f8f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,6 +11,11 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, + "./i18n": { + "types": "./dist/i18n/index.d.ts", + "import": "./dist/i18n/index.js", + "default": "./dist/i18n/index.js" + }, "./clone-scene-graph": { "types": "./dist/utils/clone-scene-graph.d.ts", "import": "./dist/utils/clone-scene-graph.js", diff --git a/packages/core/src/i18n/format.ts b/packages/core/src/i18n/format.ts new file mode 100644 index 0000000000..33795b9059 --- /dev/null +++ b/packages/core/src/i18n/format.ts @@ -0,0 +1,98 @@ +import { getLocale } from './registry' + +export interface NumberFormatOptions { + /** Minimum fraction digits. */ + min?: number + /** Maximum fraction digits. */ + max?: number + useGrouping?: boolean + locale?: string +} + +export type LengthUnit = 'm' | 'cm' | 'mm' | 'ft' | 'in' + +const UNIT_SCALE: Record = { + m: 1, + cm: 100, + mm: 1000, + ft: 1 / 0.3048, + in: 1 / 0.0254, +} + +const DEFAULT_UNIT_DIGITS: Record = { + m: 2, + cm: 1, + mm: 0, + ft: 2, + in: 1, +} + +function fractionDigits(value: number | undefined, fallback: number): number { + if (!(typeof value === 'number' && Number.isFinite(value))) return fallback + return Math.max(0, Math.min(20, Math.round(value))) +} + +function decimalSeparator(locale: string): string { + try { + return ( + new Intl.NumberFormat(locale) + .formatToParts(1.1) + .find((part) => part.type === 'decimal')?.value ?? '.' + ) + } catch { + return '.' + } +} + +export function formatNumber(value: number, options: NumberFormatOptions = {}): string { + if (!Number.isFinite(value)) return '--' + const minimumFractionDigits = fractionDigits(options.min, 0) + const maximumFractionDigits = Math.max( + minimumFractionDigits, + fractionDigits(options.max, Math.max(3, minimumFractionDigits)), + ) + + try { + return new Intl.NumberFormat(options.locale ?? getLocale(), { + minimumFractionDigits, + maximumFractionDigits, + useGrouping: options.useGrouping ?? false, + }).format(value) + } catch { + return new Intl.NumberFormat('en', { + minimumFractionDigits, + maximumFractionDigits, + useGrouping: options.useGrouping ?? false, + }).format(value) + } +} + +export function formatLength( + meters: number, + unit: LengthUnit, + options: NumberFormatOptions = {}, +): string { + if (!Number.isFinite(meters)) return '--' + return `${formatNumber(meters * UNIT_SCALE[unit], { + ...options, + max: options.max ?? DEFAULT_UNIT_DIGITS[unit], + })}\u00a0${unit}` +} + +/** + * Normalize a decimal for parsers that expect `.`. Localized inputs accept + * their locale separator and `.`; grouping is intentionally not accepted in + * numeric editor fields because it is ambiguous with decimal commas. + */ +export function normalizeDecimalInput(input: string, locale = getLocale()): string { + const separator = decimalSeparator(locale) + if (separator === '.' || input.includes('.')) return input + return input.replaceAll(separator, '.') +} + +export function parseLocalizedNumber(input: string, locale = getLocale()): number | null { + const normalized = normalizeDecimalInput(input.trim(), locale).replace('−', '-') + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null + const value = Number(normalized) + return Number.isFinite(value) ? value : null +} diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx new file mode 100644 index 0000000000..12e646dd83 --- /dev/null +++ b/packages/core/src/i18n/i18n.test.tsx @@ -0,0 +1,137 @@ +import { afterEach, describe, expect, spyOn, test } from 'bun:test' +import { renderToString } from 'react-dom/server' +import { + BuildingNode, + formatLength, + formatNumber, + LevelNode, + parseLocalizedNumber, + registerMessages, + setLocale, + SiteNode, + subscribe, + translate, + useTranslate, +} from '../index' + +afterEach(() => { + setLocale('en') +}) + +describe('message registry', () => { + test('merges host overrides and falls back from a regional locale to its language', () => { + registerMessages('pt', 'viewer', { + 'controls.open': 'Abrir', + 'controls.close': 'Fechar', + }) + registerMessages('pt-BR', 'viewer', { 'controls.close': 'Encerrar' }) + registerMessages('pt-BR', 'viewer', { 'controls.close': 'Fechar agora' }) + + setLocale('pt-BR') + + expect(translate('viewer.controls.open', 'Open')).toBe('Abrir') + expect(translate('viewer.controls.close', 'Close')).toBe('Fechar agora') + expect(translate('viewer.controls.unknown', 'Unknown')).toBe('Unknown') + }) + + test('interpolates values and selects plurals with Intl.PluralRules', () => { + registerMessages('fr', 'editor', { + selection: { + one: '{count} élément sélectionné', + other: '{count} éléments sélectionnés', + }, + welcome: 'Bonjour {name}', + }) + setLocale('fr') + + expect(translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 1 })) + .toBe('1 élément sélectionné') + expect(translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 3 })) + .toBe('3 éléments sélectionnés') + expect(translate('editor.welcome', 'Hello {name}', { name: 'Ada' })).toBe('Bonjour Ada') + }) + + test('notifies subscribers for locale and active-catalog changes', () => { + let changes = 0 + const unsubscribe = subscribe(() => { + changes += 1 + }) + + setLocale('ja') + registerMessages('ja', 'viewer', { test: 'テスト' }) + unsubscribe() + setLocale('es') + + expect(changes).toBe(2) + }) + + test('warns once in development and never exposes the missing key', () => { + const previous = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const warn = spyOn(console, 'warn').mockImplementation(() => {}) + setLocale('ja') + + expect(translate('viewer.test.missing-warning', 'Visible fallback')).toBe('Visible fallback') + expect(translate('viewer.test.missing-warning', 'Visible fallback')).toBe('Visible fallback') + expect(warn).toHaveBeenCalledTimes(1) + + warn.mockRestore() + if (previous === undefined) delete process.env.NODE_ENV + else process.env.NODE_ENV = previous + }) +}) + +describe('locale formatting', () => { + test('formats numbers and lengths without coupling locale to the unit', () => { + setLocale('fr') + expect(formatNumber(2.75, { max: 2 })).toBe('2,75') + expect(formatLength(2.75, 'm')).toBe('2,75\u00a0m') + expect(formatLength(2.75, 'mm')).toBe('2 750\u00a0mm') + + setLocale('en') + expect(formatLength(2.75, 'm')).toBe('2.75\u00a0m') + }) + + test('accepts the locale decimal separator as well as a dot', () => { + expect(parseLocalizedNumber('2,75', 'fr')).toBe(2.75) + expect(parseLocalizedNumber('2.75', 'fr')).toBe(2.75) + expect(parseLocalizedNumber('2,75', 'en')).toBeNull() + }) +}) + +test('useTranslate renders the English snapshot on the server', () => { + registerMessages('ja', 'viewer', { greeting: 'こんにちは' }) + setLocale('ja') + + function Greeting() { + const t = useTranslate() + return {t('viewer.greeting', 'Hello')} + } + + expect(renderToString()).toContain('Hello') +}) + +test('building and serializing a scene is locale-neutral', () => { + function serializeFixture() { + const level = LevelNode.parse({ id: 'level_i18n', parentId: 'building_i18n', children: [] }) + const building = BuildingNode.parse({ + id: 'building_i18n', + parentId: 'site_i18n', + children: [level.id], + }) + const site = SiteNode.parse({ id: 'site_i18n', children: [building.id] }) + return JSON.stringify({ + nodes: { + [site.id]: site, + [building.id]: building, + [level.id]: level, + }, + rootNodeIds: [site.id], + }) + } + + setLocale('en') + const english = serializeFixture() + setLocale('ja') + expect(serializeFixture()).toBe(english) +}) diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts new file mode 100644 index 0000000000..1da25a9408 --- /dev/null +++ b/packages/core/src/i18n/index.ts @@ -0,0 +1,20 @@ +export { + formatLength, + formatNumber, + type LengthUnit, + type NumberFormatOptions, + normalizeDecimalInput, + parseLocalizedNumber, +} from './format' +export { + getLocale, + type PluralMessage, + registerMessages, + setLocale, + subscribe, + translate, + type TranslationMessage, + type TranslationMessages, + type TranslationParams, +} from './registry' +export { type Translate, useTranslate } from './use-translate' diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts new file mode 100644 index 0000000000..842d40c8f6 --- /dev/null +++ b/packages/core/src/i18n/registry.ts @@ -0,0 +1,156 @@ +export type TranslationParams = Record + +export type PluralMessage = { + zero?: string + one?: string + two?: string + few?: string + many?: string + other: string +} + +export type TranslationMessage = string | PluralMessage +export type TranslationMessages = Record + +const catalogs = new Map>() +const listeners = new Set<() => void>() +const warnedMissingKeys = new Set() + +let locale = 'en' +let version = 0 + +function canonicalLocale(value: string): string { + const trimmed = value.trim() + if (!trimmed) return 'en' + try { + return Intl.getCanonicalLocales(trimmed)[0] ?? 'en' + } catch { + return 'en' + } +} + +function localeFallbacks(value: string): string[] { + const exact = canonicalLocale(value) + try { + const language = new Intl.Locale(exact).language + return language === exact ? [exact] : [exact, language] + } catch { + return [exact] + } +} + +function isDevelopment(): boolean { + try { + const meta = import.meta as { env?: { DEV?: boolean } } + if (typeof meta.env?.DEV === 'boolean') return meta.env.DEV + } catch { + // Fall through for runtimes without import.meta.env. + } + return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development' +} + +function emitChange(): void { + version += 1 + for (const listener of [...listeners]) listener() +} + +export function registerMessages( + messageLocale: string, + namespace: string, + messages: TranslationMessages, +): void { + const normalizedLocale = canonicalLocale(messageLocale) + const normalizedNamespace = namespace.replace(/^\.+|\.+$/g, '') + const catalog = catalogs.get(normalizedLocale) ?? new Map() + + for (const [key, message] of Object.entries(messages)) { + const normalizedKey = key.replace(/^\.+/g, '') + const fullKey = normalizedNamespace ? `${normalizedNamespace}.${normalizedKey}` : normalizedKey + catalog.set(fullKey, message) + } + + catalogs.set(normalizedLocale, catalog) + emitChange() +} + +export function setLocale(nextLocale: string): void { + const normalized = canonicalLocale(nextLocale) + if (normalized === locale) return + locale = normalized + emitChange() +} + +export function getLocale(): string { + return locale +} + +export function subscribe(listener: () => void): () => void { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} + +export function getI18nVersion(): number { + return version +} + +function messageFor(key: string, requestedLocale: string): TranslationMessage | undefined { + for (const candidate of localeFallbacks(requestedLocale)) { + const message = catalogs.get(candidate)?.get(key) + if (message !== undefined) return message + } + return undefined +} + +function warnMissing(key: string, requestedLocale: string): void { + if (!isDevelopment() || requestedLocale === 'en') return + const warningId = `${requestedLocale}:${key}` + if (warnedMissingKeys.has(warningId)) return + warnedMissingKeys.add(warningId) + console.warn(`[i18n] Missing "${key}" for locale "${requestedLocale}"; using English.`) +} + +function selectPlural( + message: TranslationMessage, + fallback: TranslationMessage, + count: number | undefined, + requestedLocale: string, +): string { + if (typeof message === 'string') return message + if (count === undefined) return message.other + + let category: Intl.LDMLPluralRule = 'other' + try { + category = new Intl.PluralRules(requestedLocale).select(count) + } catch { + category = new Intl.PluralRules('en').select(count) + } + + const translated = message[category] ?? message.other + if (translated !== undefined) return translated + if (typeof fallback === 'string') return fallback + return fallback[category] ?? fallback.other +} + +function interpolate(message: string, params: TranslationParams | undefined): string { + if (!params) return message + return message.replace(/\{([A-Za-z_][\w.-]*)\}/g, (placeholder, name: string) => { + const value = params[name] + return value === undefined ? placeholder : String(value) + }) +} + +export function translate( + key: string, + englishDefault: TranslationMessage, + params?: TranslationParams, + requestedLocale = locale, +): string { + const normalizedLocale = canonicalLocale(requestedLocale) + const translated = normalizedLocale === 'en' ? undefined : messageFor(key, normalizedLocale) + if (translated === undefined) warnMissing(key, normalizedLocale) + const message = translated ?? englishDefault + const count = typeof params?.count === 'number' ? params.count : undefined + return interpolate(selectPlural(message, englishDefault, count, normalizedLocale), params) +} diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts new file mode 100644 index 0000000000..e66887487e --- /dev/null +++ b/packages/core/src/i18n/use-translate.ts @@ -0,0 +1,33 @@ +'use client' + +import { useCallback, useSyncExternalStore } from 'react' +import { + getI18nVersion, + subscribe, + translate, + type TranslationMessage, + type TranslationParams, +} from './registry' + +const SERVER_SNAPSHOT = -1 + +export type Translate = ( + key: string, + englishDefault: TranslationMessage, + params?: TranslationParams, +) => string + +/** + * The server and hydration snapshot always uses the in-code English message. + * Once the client subscribes, the active locale is read from the registry and + * locale or message registrations re-render the component. + */ +export function useTranslate(): Translate { + const version = useSyncExternalStore(subscribe, getI18nVersion, () => SERVER_SNAPSHOT) + + return useCallback( + (key, englishDefault, params) => + translate(key, englishDefault, params, version === SERVER_SNAPSHOT ? 'en' : undefined), + [version], + ) +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ced875d413..70a3fd81d3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,7 @@ export { hiddenWallPointerEventsHeld, holdHiddenWallPointerEvents, } from './events/hidden-wall-pointer-hold' +export * from './i18n' export { type ItemClipEntry, itemClipRegistry } from './hooks/scene-registry/item-clip-registry' export { sceneRegistry, diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 173b14f149..5483314863 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -8,6 +8,7 @@ import { getSceneMaterialIdFromRef, initSpaceDetectionSync, initSpatialGridSync, + setLocale, spatialGridManager, useScene, } from '@pascal-app/core' @@ -202,6 +203,8 @@ function initializeEditorRuntime(): () => void { } } export interface EditorProps { + /** UI locale. Hosts may also call `setLocale` before mounting. */ + locale?: string // Layout version — 'v1' (default) or 'v2' (navbar + two-column) layoutVersion?: 'v1' | 'v2' // Viewport edge the tool menu docks to (desktop only; mobile stays at the bottom) @@ -1961,6 +1964,10 @@ function EditorContent({ } export default function Editor(props: EditorProps) { + useClientLayoutEffect(() => { + if (props.locale) setLocale(props.locale) + }, [props.locale]) + return ( diff --git a/packages/editor/src/components/ui/controls/slider-control.tsx b/packages/editor/src/components/ui/controls/slider-control.tsx index 3bf74c6b75..bdfc13e270 100644 --- a/packages/editor/src/components/ui/controls/slider-control.tsx +++ b/packages/editor/src/components/ui/controls/slider-control.tsx @@ -1,6 +1,6 @@ 'use client' -import { useScene } from '@pascal-app/core' +import { parseLocalizedNumber, useScene } from '@pascal-app/core' import { useCallback, useEffect, useRef, useState } from 'react' import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser' import { useLinearDisplay } from '../../../lib/use-linear-display' @@ -270,8 +270,8 @@ export function SliderControl({ system: isImperial ? 'us' : 'metric', }) : null - const bare = Number(text) - const stored = parsed ?? (text.trim() && Number.isFinite(bare) ? toStored(bare) : null) + const bare = parseLocalizedNumber(text) + const stored = parsed ?? (bare === null ? null : toStored(bare)) return stored === null ? null : clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision)))) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 5292aca737..382e57f019 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -32,6 +32,22 @@ export { subscribeSceneCommits, useScene, } from '@pascal-app/core' +export { + formatLength, + formatNumber, + getLocale, + normalizeDecimalInput, + parseLocalizedNumber, + registerMessages, + setLocale, + subscribe, + translate, + type Translate, + type TranslationMessage, + type TranslationMessages, + type TranslationParams, + useTranslate, +} from '@pascal-app/core' export { useViewer } from '@pascal-app/viewer' export type { EditorProps } from './components/editor' export { default as Editor } from './components/editor' diff --git a/packages/editor/src/lib/measurement-parser.ts b/packages/editor/src/lib/measurement-parser.ts index 2cafe7387b..d569b452f2 100644 --- a/packages/editor/src/lib/measurement-parser.ts +++ b/packages/editor/src/lib/measurement-parser.ts @@ -1,6 +1,7 @@ // Must precede the lingo import: lingo clones its kind table with // structuredClone while its module body evaluates. See the file's own comment. import './structured-clone-fallback' +import { normalizeDecimalInput } from '@pascal-app/core' import { type Kind, parseQuantity, quantity } from '@pascal-app/lingo' /** @@ -68,7 +69,7 @@ export function parseMeasurement( spec: LingoUnitSpec, options: ParseMeasurementOptions = {}, ): number | null { - const result = parseQuantity(raw, { + const result = parseQuantity(normalizeDecimalInput(raw), { kind: spec.kind, unit: options.bareUnit ?? spec.unitId, system: options.system, @@ -91,7 +92,7 @@ export interface MeasurementHintOptions extends ParseMeasurementOptions { clamp?: (stored: number) => number } -const PLAIN_NUMBER = /^[+-]?\d*\.?\d*$/ +const PLAIN_NUMBER = /^[+-]?\d*(?:[.,]\d*)?$/ /** * A faint "= 1.83 m" preview of the value currently being typed, shown only diff --git a/packages/editor/src/lib/measurements.ts b/packages/editor/src/lib/measurements.ts index 2257e19329..3485345fb5 100644 --- a/packages/editor/src/lib/measurements.ts +++ b/packages/editor/src/lib/measurements.ts @@ -1,4 +1,4 @@ -import type { MeasurementPoint } from '@pascal-app/core' +import { formatNumber, type MeasurementPoint } from '@pascal-app/core' export type LinearUnit = 'metric' | 'imperial' export type MetricNotation = 'meters' | 'millimeters' @@ -153,7 +153,9 @@ export function formatAreaLabel( ): string { if (!Number.isFinite(squareMeters)) return '--' - return `${squareMetersToAreaUnit(squareMeters, unit).toFixed(fractionDigits)}${getAreaUnitLabel(unit)}` + return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { + max: fractionDigits, + })}${getAreaUnitLabel(unit)}` } export function cubicMetersToVolumeUnit(cubicMeters: number, unit: LinearUnit): number { @@ -171,7 +173,9 @@ export function formatVolumeLabel( ): string { if (!Number.isFinite(cubicMeters)) return '--' - return `${cubicMetersToVolumeUnit(cubicMeters, unit).toFixed(fractionDigits)}${getVolumeUnitLabel(unit)}` + return `${formatNumber(cubicMetersToVolumeUnit(cubicMeters, unit), { + max: fractionDigits, + })}${getVolumeUnitLabel(unit)}` } export function formatLinearMeasurement( @@ -200,11 +204,11 @@ export function formatLinearMeasurement( if (metricNotation === 'millimeters') { const roundedMillimeters = Math.round(absoluteMeters * 1000) const sign = meters < 0 && roundedMillimeters !== 0 ? '-' : '' - return `${sign}${roundedMillimeters}mm` + return `${sign}${formatNumber(roundedMillimeters, { max: 0 })}mm` } const roundedMeters = Number.parseFloat(absoluteMeters.toFixed(2)) const sign = meters < 0 && roundedMeters !== 0 ? '-' : '' - return `${sign}${roundedMeters}m` + return `${sign}${formatNumber(roundedMeters, { max: 2 })}m` } From 7b250829491a39a4c22d08776f2b87c83a0273de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:26:56 +0000 Subject: [PATCH 02/23] feat(editor): localize viewer and build panel surfaces --- apps/editor/app/client-bootstrap.tsx | 3 + packages/editor/package.json | 1 + .../src/components/editor/room-labels.tsx | 3 +- .../components/ui/build-panel/build-panel.tsx | 86 +++- .../viewer/floorplan-compass-button.tsx | 7 +- .../components/viewer/floorplan-preview.tsx | 48 ++- .../components/viewer/viewer-controls-bar.tsx | 183 +++++--- .../components/viewer/viewer-scene-header.tsx | 45 +- .../viewer/viewer-stage-switcher.tsx | 6 +- packages/editor/src/i18n/locales.ts | 407 ++++++++++++++++++ 10 files changed, 674 insertions(+), 115 deletions(-) create mode 100644 packages/editor/src/i18n/locales.ts diff --git a/apps/editor/app/client-bootstrap.tsx b/apps/editor/app/client-bootstrap.tsx index 5313824e6e..65d19e3594 100644 --- a/apps/editor/app/client-bootstrap.tsx +++ b/apps/editor/app/client-bootstrap.tsx @@ -10,8 +10,11 @@ // idempotent under HMR. import '../lib/bootstrap' import { getLocale, setLocale, subscribe } from '@pascal-app/core' +import { registerBundledLocales } from '@pascal-app/editor/locales' import { type ReactNode, useEffect } from 'react' +registerBundledLocales() + if (typeof window !== 'undefined') { const queryLocale = new URLSearchParams(window.location.search).get('lang') if (queryLocale) setLocale(queryLocale) diff --git a/packages/editor/package.json b/packages/editor/package.json index 4e909a1078..91915cb8ca 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -7,6 +7,7 @@ "exports": { ".": "./src/index.tsx", "./catalog": "./src/components/ui/item-catalog/catalog-items.tsx", + "./locales": "./src/i18n/locales.ts", "./procedural-items": "./src/components/procedural-items/inspector.tsx", "./distribution": "./src/components/tools/distribution/distribution-public.ts" }, diff --git a/packages/editor/src/components/editor/room-labels.tsx b/packages/editor/src/components/editor/room-labels.tsx index 73e1052d48..96bdb1652f 100644 --- a/packages/editor/src/components/editor/room-labels.tsx +++ b/packages/editor/src/components/editor/room-labels.tsx @@ -7,6 +7,7 @@ import { containsPoint, DEFAULT_LEVEL_HEIGHT, emitter, + formatNumber, type Polygon, polygonInteriorPoint, type Ring, @@ -93,7 +94,7 @@ export function roomLabelsVisible(state: RoomLabelVisibilityState): boolean { } export function formatRoomArea(squareMeters: number, unit: LinearUnit): string { - return `${squareMetersToAreaUnit(squareMeters, unit).toFixed(1)} ${getAreaUnitLabel(unit)}` + return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { max: 1 })} ${getAreaUnitLabel(unit)}` } function useRoomLabelsVisible() { diff --git a/packages/editor/src/components/ui/build-panel/build-panel.tsx b/packages/editor/src/components/ui/build-panel/build-panel.tsx index beba274971..05ad85fcae 100644 --- a/packages/editor/src/components/ui/build-panel/build-panel.tsx +++ b/packages/editor/src/components/ui/build-panel/build-panel.tsx @@ -1,5 +1,6 @@ 'use client' +import { type Translate, useTranslate } from '@pascal-app/core' import { ChevronRight } from 'lucide-react' import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react' import { cn } from '../../../lib/utils' @@ -70,6 +71,7 @@ export function BuildPanelAdvancedSection({ description?: string hint?: string }) { + const t = useTranslate() const [open, setOpen] = useState(false) useEffect(() => setOpen(readAdvancedOpen()), []) useEffect(() => { @@ -94,7 +96,9 @@ export function BuildPanelAdvancedSection({ open && 'rotate-90', )} /> -

Advanced

+

+ {t('editor.buildPanel.advanced', 'Advanced')} +

{hint ? ( {hint} ) : null} @@ -214,29 +218,75 @@ export function BuildPanelRoomsSection({ /** Extra attributes per tile (e.g. a host's guide-target hook). */ tileProps?: (variant: WallDrawVariant) => Record }) { + const t = useTranslate() const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant) + const activeMessages = active ? roomVariantMessages(active.id, t) : null return ( - + - {WALL_DRAW_VARIANTS.map((variant) => ( - onSelect(variant.id)} - onMouseEnter={() => onHover?.(variant.id)} - size="lg" - title={variant.title} - {...tileProps?.(variant.id)} - /> - ))} + {WALL_DRAW_VARIANTS.map((variant) => { + const messages = roomVariantMessages(variant.id, t) + return ( + onSelect(variant.id)} + onMouseEnter={() => onHover?.(variant.id)} + size="lg" + title={messages.title} + {...tileProps?.(variant.id)} + /> + ) + })} ) } + +function roomVariantMessages(variant: WallDrawVariant, t: Translate) { + if (variant === 'rectangle') { + return { + label: t('editor.buildPanel.rooms.rectangle.label', 'Rectangle'), + title: t('editor.buildPanel.rooms.rectangle.title', 'Rectangle room'), + lead: t('editor.buildPanel.rooms.rectangle.lead', 'Click two corners'), + rest: t( + 'editor.buildPanel.rooms.rectangle.rest', + 'on the grid. Walls, floor and ceiling are created together.', + ), + } + } + if (variant === 'walls') { + return { + label: t('editor.buildPanel.rooms.walls.label', 'Walls'), + title: t('editor.buildPanel.rooms.walls.title', 'Walls'), + lead: t('editor.buildPanel.rooms.walls.lead', 'Draw one wall at a time.'), + rest: t( + 'editor.buildPanel.rooms.walls.rest', + 'Close a loop and it becomes a room; leave it open for partitions.', + ), + } + } + return { + label: t('editor.buildPanel.rooms.polygon.label', 'Polygon'), + title: t('editor.buildPanel.rooms.polygon.title', 'Polygon room'), + lead: t('editor.buildPanel.rooms.polygon.lead', 'Click each corner,'), + rest: t( + 'editor.buildPanel.rooms.polygon.rest', + 'then the first one again to close. For L-shapes and angled rooms.', + ), + } +} diff --git a/packages/editor/src/components/viewer/floorplan-compass-button.tsx b/packages/editor/src/components/viewer/floorplan-compass-button.tsx index f53b16d1b2..9bf29e709a 100644 --- a/packages/editor/src/components/viewer/floorplan-compass-button.tsx +++ b/packages/editor/src/components/viewer/floorplan-compass-button.tsx @@ -1,5 +1,6 @@ 'use client' +import { useTranslate } from '@pascal-app/core' import type React from 'react' import { cn } from '../../lib/utils' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip' @@ -18,11 +19,13 @@ export function FloorplanCompassButton({ needleRef, style, }: FloorplanCompassButtonProps) { + const t = useTranslate() + const label = t('viewer.floorplan.alignNorth', 'Align view to north') return ( - Align view to north + {label} ) } diff --git a/packages/editor/src/components/viewer/floorplan-preview.tsx b/packages/editor/src/components/viewer/floorplan-preview.tsx index 36ea8198a2..a907d16fb9 100644 --- a/packages/editor/src/components/viewer/floorplan-preview.tsx +++ b/packages/editor/src/components/viewer/floorplan-preview.tsx @@ -9,7 +9,9 @@ import { getLevelDisplayName, isNodeKindEnabled, nodeRegistry, + type Translate, useScene, + useTranslate, } from '@pascal-app/core' import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema' import { useViewer } from '@pascal-app/viewer' @@ -143,11 +145,22 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox { } } -function levelLabel(level: AnyNode): string { - return getLevelDisplayName({ - name: (level as { name?: string }).name, - level: (level as { level?: number }).level ?? 0, +function levelLabel(level: AnyNode, t: Translate): string { + const levelNumber = (level as { level?: number }).level ?? 0 + const levelName = (level as { name?: string }).name + const english = getLevelDisplayName({ + name: levelName, + level: levelNumber, }) + const authoredName = levelName?.trim() + if (authoredName && authoredName !== `Level ${levelNumber}`) return authoredName + if (levelNumber === 0) return t('core.level.groundFloor', 'Ground floor') + if (levelNumber === -1) return t('core.level.basement', 'Basement') + if (levelNumber < -1) { + return t('core.level.basementNumber', 'Basement {number}', { number: -levelNumber }) + } + if (levelNumber > 0) return t('core.level.floorNumber', 'Floor {number}', { number: levelNumber }) + return english } function collectLevelTree(root: AnyNode, nodes: Record): AnyNode[] { @@ -309,6 +322,7 @@ export function FloorplanPreview({ showLevelSelector = true, synchronizeNavigation = false, }: FloorplanPreviewProps) { + const t = useTranslate() const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes)) const storeInstalledPlugins = useScene((state) => scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins, @@ -869,7 +883,7 @@ export function FloorplanPreview({ className={className} style={{ display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }} > - No floor plans are available for this scene. + {t('viewer.floorplan.empty', 'No floor plans are available for this scene.')} ) } @@ -882,7 +896,11 @@ export function FloorplanPreview({ {synchronizeNavigation ? : null}
- Floor + {t('viewer.floorplan.floor', 'Floor')} @@ -1013,28 +1031,28 @@ export function FloorplanPreview({ }} > @@ -228,7 +249,7 @@ export const ViewerSceneHeader = ({ key={section.kind} > - {SECTION_TITLES[section.kind]} + {sectionTitle(section.kind, t)} {section.rows.map((row) => { const active = diff --git a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx index ea137c6556..5dc4f39461 100644 --- a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx +++ b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx @@ -1,5 +1,6 @@ 'use client' +import { useTranslate } from '@pascal-app/core' import { Box, Columns2, Map as MapIcon } from 'lucide-react' import type { ReactNode } from 'react' import { cn } from '../../lib/utils' @@ -22,11 +23,12 @@ export function ViewerStageSwitcher({ modes, onChange, }: ViewerStageSwitcherProps) { + const t = useTranslate() const enabledModes = normalizeViewerStageModes(modes) return (
1 ? 'hidden md:flex' : undefined} icon={} - label="Split" + label={t('viewer.stage.split', 'Split')} onClick={() => onChange('split')} /> ) : null} diff --git a/packages/editor/src/i18n/locales.ts b/packages/editor/src/i18n/locales.ts new file mode 100644 index 0000000000..31f928467c --- /dev/null +++ b/packages/editor/src/i18n/locales.ts @@ -0,0 +1,407 @@ +import { registerMessages, type TranslationMessages } from '@pascal-app/core' + +export type BundledLocale = 'zh-CN' | 'fr' | 'es' | 'ja' + +type LocalePack = { + core: TranslationMessages + editor: TranslationMessages + viewer: TranslationMessages +} + +export const bundledLocalePacks: Record = { + 'zh-CN': { + core: { + 'level.groundFloor': '首层', + 'level.basement': '地下层', + 'level.basementNumber': '地下 {number} 层', + 'level.floorNumber': '{number} 层', + }, + editor: { + 'buildPanel.advanced': '高级', + 'buildPanel.rooms.hint': '墙体 · 楼板 · 吊顶', + 'buildPanel.rooms.title': '房间', + 'buildPanel.rooms.rectangle.label': '矩形', + 'buildPanel.rooms.rectangle.title': '矩形房间', + 'buildPanel.rooms.rectangle.lead': '点击两个对角点', + 'buildPanel.rooms.rectangle.rest': '在网格上。墙体、楼板和吊顶将一并创建。', + 'buildPanel.rooms.polygon.label': '多边形', + 'buildPanel.rooms.polygon.title': '多边形房间', + 'buildPanel.rooms.polygon.lead': '依次点击各个角点,', + 'buildPanel.rooms.polygon.rest': '然后再次点击起点闭合。适用于 L 形和斜角房间。', + 'buildPanel.rooms.walls.label': '墙体', + 'buildPanel.rooms.walls.title': '墙体', + 'buildPanel.rooms.walls.lead': '逐面绘制墙体。', + 'buildPanel.rooms.walls.rest': '闭合轮廓后会形成房间;保持开口可绘制隔墙。', + 'buildPanel.rooms.idle': '选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和吊顶。', + }, + viewer: { + 'common.on': '开启', + 'common.off': '关闭', + 'levels.stacked': '叠层', + 'levels.exploded': '分解', + 'levels.solo': '单层', + 'levels.manual': '手动', + 'levels.label': '楼层:{mode}', + 'walls.fullHeight': '全高墙', + 'walls.cutaway': '剖切墙', + 'walls.low': '矮墙', + 'walls.label': '墙体:{mode}', + 'theme.title': '场景主题', + 'theme.studio': '影棚', + 'theme.paper': '纸面', + 'theme.sunset': '日落', + 'theme.overcast': '阴天', + 'theme.blueprint': '蓝图', + 'theme.mediterranean': '地中海', + 'theme.twilight': '暮色', + 'theme.night': '夜景', + 'theme.verdant': '绿意', + 'visibility.title': '可见性', + 'visibility.scans': '扫描模型', + 'visibility.guides': '参考图', + 'display.title': '显示设置', + 'display.shadows': '阴影', + 'display.camera': '相机', + 'display.perspective': '透视', + 'display.orthographic': '正交', + 'display.colors': '颜色', + 'display.colored': '彩色', + 'display.monochrome': '单色', + 'display.render.title': '渲染', + 'display.render.solid': '实体', + 'display.render.solidDetail': '平整快速,不启用环境光遮蔽', + 'display.render.rendered': '渲染效果', + 'display.render.renderedDetail': '完整环境光遮蔽', + 'display.edges.title': '边线', + 'display.edges.off': '关闭', + 'display.edges.offDetail': '不显示边线', + 'display.edges.soft': '柔和', + 'display.edges.softDetail': '淡化显示主要折线', + 'display.edges.strong': '清晰', + 'display.edges.strongDetail': '清晰、不透明的边线', + 'walkthrough.label': '漫游:{state}', + 'camera.orbitLeft': '向左旋转', + 'camera.orbitRight': '向右旋转', + 'camera.topView': '俯视图', + 'floorplan.alignNorth': '视图对齐北向', + 'floorplan.empty': '此场景没有可用的平面图。', + 'floorplan.viewLabel': '{floor}二维视图', + 'floorplan.floorPlan': '平面图', + 'floorplan.floor': '楼层', + 'floorplan.zoomOut': '缩小', + 'floorplan.fit': '适合窗口', + 'floorplan.zoomIn': '放大', + 'header.units': '单元', + 'header.rooms': '房间', + 'header.zones': '分区', + 'header.back': '返回', + 'header.untitled': '未命名', + 'header.selection': '当前选择', + 'header.levels': '楼层', + 'stage.layout': '视图布局', + 'stage.split': '分屏', + }, + }, + fr: { + core: { + 'level.groundFloor': 'Rez-de-chaussée', + 'level.basement': 'Sous-sol', + 'level.basementNumber': 'Sous-sol {number}', + 'level.floorNumber': 'Étage {number}', + }, + editor: { + 'buildPanel.advanced': 'Avancé', + 'buildPanel.rooms.hint': 'murs · sol · plafond', + 'buildPanel.rooms.title': 'Pièces', + 'buildPanel.rooms.rectangle.label': 'Rectangle', + 'buildPanel.rooms.rectangle.title': 'Pièce rectangulaire', + 'buildPanel.rooms.rectangle.lead': 'Cliquez sur deux angles', + 'buildPanel.rooms.rectangle.rest': + 'de la grille. Les murs, le sol et le plafond sont créés ensemble.', + 'buildPanel.rooms.polygon.label': 'Polygone', + 'buildPanel.rooms.polygon.title': 'Pièce polygonale', + 'buildPanel.rooms.polygon.lead': 'Cliquez sur chaque angle,', + 'buildPanel.rooms.polygon.rest': + 'puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.', + 'buildPanel.rooms.walls.label': 'Murs', + 'buildPanel.rooms.walls.title': 'Murs', + 'buildPanel.rooms.walls.lead': 'Tracez un mur à la fois.', + 'buildPanel.rooms.walls.rest': + 'Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.', + 'buildPanel.rooms.idle': + 'Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.', + }, + viewer: { + 'common.on': 'Activé', + 'common.off': 'Désactivé', + 'levels.stacked': 'Superposés', + 'levels.exploded': 'Éclatés', + 'levels.solo': 'Isolé', + 'levels.manual': 'Manuel', + 'levels.label': 'Niveaux : {mode}', + 'walls.fullHeight': 'Pleine hauteur', + 'walls.cutaway': 'Vue en coupe', + 'walls.low': 'Bas', + 'walls.label': 'Murs : {mode}', + 'theme.title': 'Ambiance', + 'theme.studio': 'Studio', + 'theme.paper': 'Papier', + 'theme.sunset': 'Coucher de soleil', + 'theme.overcast': 'Ciel couvert', + 'theme.blueprint': 'Plan', + 'theme.mediterranean': 'Méditerranéen', + 'theme.twilight': 'Crépuscule', + 'theme.night': 'Nuit', + 'theme.verdant': 'Verdoyant', + 'visibility.title': 'Visibilité', + 'visibility.scans': 'Relevés 3D', + 'visibility.guides': 'Repères', + 'display.title': 'Paramètres d’affichage', + 'display.shadows': 'Ombres', + 'display.camera': 'Caméra', + 'display.perspective': 'Perspective', + 'display.orthographic': 'Orthographique', + 'display.colors': 'Couleurs', + 'display.colored': 'En couleur', + 'display.monochrome': 'Monochrome', + 'display.render.title': 'Rendu', + 'display.render.solid': 'Solide', + 'display.render.solidDetail': 'Rapide et uniforme, sans occlusion ambiante', + 'display.render.rendered': 'Réaliste', + 'display.render.renderedDetail': 'Occlusion ambiante complète', + 'display.edges.title': 'Arêtes', + 'display.edges.off': 'Désactivées', + 'display.edges.offDetail': 'Aucune ligne d’arête', + 'display.edges.soft': 'Douces', + 'display.edges.softDetail': 'Contour léger des plis principaux', + 'display.edges.strong': 'Marquées', + 'display.edges.strongDetail': 'Lignes d’arête nettes et opaques', + 'walkthrough.label': 'Visite : {state}', + 'camera.orbitLeft': 'Pivoter à gauche', + 'camera.orbitRight': 'Pivoter à droite', + 'camera.topView': 'Vue de dessus', + 'floorplan.alignNorth': 'Orienter la vue vers le nord', + 'floorplan.empty': 'Aucun plan d’étage n’est disponible pour cette scène.', + 'floorplan.viewLabel': 'Vue 2D — {floor}', + 'floorplan.floorPlan': 'Plan d’étage', + 'floorplan.floor': 'Niveau', + 'floorplan.zoomOut': 'Zoom arrière', + 'floorplan.fit': 'Ajuster le plan', + 'floorplan.zoomIn': 'Zoom avant', + 'header.units': 'Unités', + 'header.rooms': 'Pièces', + 'header.zones': 'Zones', + 'header.back': 'Retour', + 'header.untitled': 'Sans titre', + 'header.selection': 'Sélection', + 'header.levels': 'Niveaux', + 'stage.layout': 'Disposition des vues', + 'stage.split': 'Partagé', + }, + }, + es: { + core: { + 'level.groundFloor': 'Planta baja', + 'level.basement': 'Sótano', + 'level.basementNumber': 'Sótano {number}', + 'level.floorNumber': 'Planta {number}', + }, + editor: { + 'buildPanel.advanced': 'Avanzado', + 'buildPanel.rooms.hint': 'muros · suelo · techo', + 'buildPanel.rooms.title': 'Estancias', + 'buildPanel.rooms.rectangle.label': 'Rectángulo', + 'buildPanel.rooms.rectangle.title': 'Estancia rectangular', + 'buildPanel.rooms.rectangle.lead': 'Haz clic en dos esquinas', + 'buildPanel.rooms.rectangle.rest': + 'de la cuadrícula. Los muros, el suelo y el techo se crean a la vez.', + 'buildPanel.rooms.polygon.label': 'Polígono', + 'buildPanel.rooms.polygon.title': 'Estancia poligonal', + 'buildPanel.rooms.polygon.lead': 'Haz clic en cada esquina', + 'buildPanel.rooms.polygon.rest': + 'y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.', + 'buildPanel.rooms.walls.label': 'Muros', + 'buildPanel.rooms.walls.title': 'Muros', + 'buildPanel.rooms.walls.lead': 'Dibuja un muro cada vez.', + 'buildPanel.rooms.walls.rest': + 'Cierra el contorno para crear una estancia o déjalo abierto para tabiques.', + 'buildPanel.rooms.idle': + 'Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.', + }, + viewer: { + 'common.on': 'Activado', + 'common.off': 'Desactivado', + 'levels.stacked': 'Apiladas', + 'levels.exploded': 'Separadas', + 'levels.solo': 'Aislada', + 'levels.manual': 'Manual', + 'levels.label': 'Plantas: {mode}', + 'walls.fullHeight': 'Altura completa', + 'walls.cutaway': 'Vista seccionada', + 'walls.low': 'Bajos', + 'walls.label': 'Muros: {mode}', + 'theme.title': 'Ambiente', + 'theme.studio': 'Estudio', + 'theme.paper': 'Papel', + 'theme.sunset': 'Atardecer', + 'theme.overcast': 'Nublado', + 'theme.blueprint': 'Plano técnico', + 'theme.mediterranean': 'Mediterráneo', + 'theme.twilight': 'Crepúsculo', + 'theme.night': 'Noche', + 'theme.verdant': 'Verde', + 'visibility.title': 'Visibilidad', + 'visibility.scans': 'Escaneos 3D', + 'visibility.guides': 'Guías', + 'display.title': 'Ajustes de visualización', + 'display.shadows': 'Sombras', + 'display.camera': 'Cámara', + 'display.perspective': 'Perspectiva', + 'display.orthographic': 'Ortográfica', + 'display.colors': 'Colores', + 'display.colored': 'A color', + 'display.monochrome': 'Monocromo', + 'display.render.title': 'Renderizado', + 'display.render.solid': 'Sólido', + 'display.render.solidDetail': 'Plano y rápido, sin oclusión ambiental', + 'display.render.rendered': 'Realista', + 'display.render.renderedDetail': 'Oclusión ambiental completa', + 'display.edges.title': 'Aristas', + 'display.edges.off': 'Desactivadas', + 'display.edges.offDetail': 'Sin líneas de arista', + 'display.edges.soft': 'Suaves', + 'display.edges.softDetail': 'Contorno tenue de los pliegues principales', + 'display.edges.strong': 'Marcadas', + 'display.edges.strongDetail': 'Líneas de arista nítidas y opacas', + 'walkthrough.label': 'Recorrido: {state}', + 'camera.orbitLeft': 'Orbitar a la izquierda', + 'camera.orbitRight': 'Orbitar a la derecha', + 'camera.topView': 'Vista superior', + 'floorplan.alignNorth': 'Orientar la vista al norte', + 'floorplan.empty': 'No hay planos de planta disponibles para esta escena.', + 'floorplan.viewLabel': 'Vista 2D de {floor}', + 'floorplan.floorPlan': 'Plano de planta', + 'floorplan.floor': 'Planta', + 'floorplan.zoomOut': 'Alejar', + 'floorplan.fit': 'Ajustar plano', + 'floorplan.zoomIn': 'Acercar', + 'header.units': 'Unidades', + 'header.rooms': 'Estancias', + 'header.zones': 'Zonas', + 'header.back': 'Volver', + 'header.untitled': 'Sin título', + 'header.selection': 'Selección', + 'header.levels': 'Plantas', + 'stage.layout': 'Disposición de vistas', + 'stage.split': 'Dividida', + }, + }, + ja: { + core: { + 'level.groundFloor': '1階', + 'level.basement': '地下1階', + 'level.basementNumber': '地下{number}階', + 'level.floorNumber': '{number}階', + }, + editor: { + 'buildPanel.advanced': '詳細', + 'buildPanel.rooms.hint': '壁 · 床 · 天井', + 'buildPanel.rooms.title': '部屋', + 'buildPanel.rooms.rectangle.label': '長方形', + 'buildPanel.rooms.rectangle.title': '長方形の部屋', + 'buildPanel.rooms.rectangle.lead': '対角の2点をクリック', + 'buildPanel.rooms.rectangle.rest': + 'グリッド上で指定します。壁・床・天井をまとめて作成します。', + 'buildPanel.rooms.polygon.label': '多角形', + 'buildPanel.rooms.polygon.title': '多角形の部屋', + 'buildPanel.rooms.polygon.lead': '各頂点をクリックし、', + 'buildPanel.rooms.polygon.rest': + '最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。', + 'buildPanel.rooms.walls.label': '壁', + 'buildPanel.rooms.walls.title': '壁', + 'buildPanel.rooms.walls.lead': '壁を1面ずつ描画します。', + 'buildPanel.rooms.walls.rest': + '輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。', + 'buildPanel.rooms.idle': '形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。', + }, + viewer: { + 'common.on': 'オン', + 'common.off': 'オフ', + 'levels.stacked': '積層', + 'levels.exploded': '分解', + 'levels.solo': '単独', + 'levels.manual': '手動', + 'levels.label': '階層:{mode}', + 'walls.fullHeight': '全高', + 'walls.cutaway': 'カット表示', + 'walls.low': '低く表示', + 'walls.label': '壁:{mode}', + 'theme.title': 'シーンテーマ', + 'theme.studio': 'スタジオ', + 'theme.paper': 'ペーパー', + 'theme.sunset': '夕景', + 'theme.overcast': '曇天', + 'theme.blueprint': '青図', + 'theme.mediterranean': '地中海', + 'theme.twilight': '薄暮', + 'theme.night': '夜景', + 'theme.verdant': '緑景', + 'visibility.title': '表示', + 'visibility.scans': '3Dスキャン', + 'visibility.guides': 'ガイド', + 'display.title': '表示設定', + 'display.shadows': '影', + 'display.camera': 'カメラ', + 'display.perspective': '透視投影', + 'display.orthographic': '平行投影', + 'display.colors': 'カラー', + 'display.colored': 'カラー表示', + 'display.monochrome': 'モノクロ', + 'display.render.title': 'レンダリング', + 'display.render.solid': 'ソリッド', + 'display.render.solidDetail': '軽量表示(アンビエントオクルージョンなし)', + 'display.render.rendered': 'レンダリング', + 'display.render.renderedDetail': 'アンビエントオクルージョンを完全表示', + 'display.edges.title': 'エッジ', + 'display.edges.off': 'オフ', + 'display.edges.offDetail': 'エッジ線を表示しない', + 'display.edges.soft': 'ソフト', + 'display.edges.softDetail': '主要な稜線を薄く表示', + 'display.edges.strong': '強調', + 'display.edges.strongDetail': 'エッジ線をくっきり表示', + 'walkthrough.label': 'ウォークスルー:{state}', + 'camera.orbitLeft': '左に回転', + 'camera.orbitRight': '右に回転', + 'camera.topView': '上面図', + 'floorplan.alignNorth': '北を上に表示', + 'floorplan.empty': 'このシーンには表示できる平面図がありません。', + 'floorplan.viewLabel': '{floor}の2Dビュー', + 'floorplan.floorPlan': '平面図', + 'floorplan.floor': '階', + 'floorplan.zoomOut': '縮小', + 'floorplan.fit': '全体を表示', + 'floorplan.zoomIn': '拡大', + 'header.units': '住戸', + 'header.rooms': '部屋', + 'header.zones': 'ゾーン', + 'header.back': '戻る', + 'header.untitled': '名称未設定', + 'header.selection': '選択', + 'header.levels': '階層', + 'stage.layout': 'ビュー配置', + 'stage.split': '分割', + }, + }, +} + +let registered = false + +export function registerBundledLocales(): void { + if (registered) return + registered = true + for (const [locale, namespaces] of Object.entries(bundledLocalePacks)) { + registerMessages(locale, 'core', namespaces.core) + registerMessages(locale, 'editor', namespaces.editor) + registerMessages(locale, 'viewer', namespaces.viewer) + } +} From abc7decdd761d71dbd185ce41a5d701ab7fbf0ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:27:17 +0000 Subject: [PATCH 03/23] chore(i18n): format localization sources --- packages/core/src/i18n/format.ts | 5 ++--- packages/core/src/i18n/i18n.test.tsx | 12 +++++++----- packages/core/src/i18n/index.ts | 2 +- packages/core/src/i18n/use-translate.ts | 2 +- packages/core/src/index.ts | 2 +- packages/editor/src/index.tsx | 20 +++++++++----------- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/core/src/i18n/format.ts b/packages/core/src/i18n/format.ts index 33795b9059..d1104ab71b 100644 --- a/packages/core/src/i18n/format.ts +++ b/packages/core/src/i18n/format.ts @@ -35,9 +35,8 @@ function fractionDigits(value: number | undefined, fallback: number): number { function decimalSeparator(locale: string): string { try { return ( - new Intl.NumberFormat(locale) - .formatToParts(1.1) - .find((part) => part.type === 'decimal')?.value ?? '.' + new Intl.NumberFormat(locale).formatToParts(1.1).find((part) => part.type === 'decimal') + ?.value ?? '.' ) } catch { return '.' diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 12e646dd83..243cad8b97 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -7,8 +7,8 @@ import { LevelNode, parseLocalizedNumber, registerMessages, - setLocale, SiteNode, + setLocale, subscribe, translate, useTranslate, @@ -44,10 +44,12 @@ describe('message registry', () => { }) setLocale('fr') - expect(translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 1 })) - .toBe('1 élément sélectionné') - expect(translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 3 })) - .toBe('3 éléments sélectionnés') + expect( + translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 1 }), + ).toBe('1 élément sélectionné') + expect( + translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 3 }), + ).toBe('3 éléments sélectionnés') expect(translate('editor.welcome', 'Hello {name}', { name: 'Ada' })).toBe('Bonjour Ada') }) diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts index 1da25a9408..f179664638 100644 --- a/packages/core/src/i18n/index.ts +++ b/packages/core/src/i18n/index.ts @@ -12,9 +12,9 @@ export { registerMessages, setLocale, subscribe, - translate, type TranslationMessage, type TranslationMessages, type TranslationParams, + translate, } from './registry' export { type Translate, useTranslate } from './use-translate' diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts index e66887487e..4687b45a11 100644 --- a/packages/core/src/i18n/use-translate.ts +++ b/packages/core/src/i18n/use-translate.ts @@ -4,9 +4,9 @@ import { useCallback, useSyncExternalStore } from 'react' import { getI18nVersion, subscribe, - translate, type TranslationMessage, type TranslationParams, + translate, } from './registry' const SERVER_SNAPSHOT = -1 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 70a3fd81d3..8b1903787a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,7 +67,6 @@ export { hiddenWallPointerEventsHeld, holdHiddenWallPointerEvents, } from './events/hidden-wall-pointer-hold' -export * from './i18n' export { type ItemClipEntry, itemClipRegistry } from './hooks/scene-registry/item-clip-registry' export { sceneRegistry, @@ -114,6 +113,7 @@ export { type WallConstructionResolution, } from './hooks/spatial-grid/support-host-patch' export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query' +export * from './i18n' export { createAngleAccumulator } from './lib/angle-accumulator' export { ARTIFACT_URL_PREFIX, diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 382e57f019..abbd50c16e 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -22,6 +22,12 @@ export { DEFAULT_STAIR_THICKNESS, DEFAULT_STAIR_TYPE, DEFAULT_STAIR_WIDTH, + formatLength, + formatNumber, + getLocale, + normalizeDecimalInput, + parseLocalizedNumber, + registerMessages, type SceneCommit, type SceneCommitListener, type SceneCommitOrigin, @@ -29,23 +35,15 @@ export { type SceneNodePatch, type ScenePatch, type SceneSnapshot, - subscribeSceneCommits, - useScene, -} from '@pascal-app/core' -export { - formatLength, - formatNumber, - getLocale, - normalizeDecimalInput, - parseLocalizedNumber, - registerMessages, setLocale, subscribe, - translate, + subscribeSceneCommits, type Translate, type TranslationMessage, type TranslationMessages, type TranslationParams, + translate, + useScene, useTranslate, } from '@pascal-app/core' export { useViewer } from '@pascal-app/viewer' From 304eb1a091d6845da8c4d6f8f06a38fb456b8a7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:31:37 +0000 Subject: [PATCH 04/23] feat(i18n): add catalog maintenance tooling --- .github/workflows/ci.yml | 3 + i18n/README.md | 61 +++ i18n/glossary.json | 45 ++ package.json | 5 +- .../components/ui/build-panel/build-panel.tsx | 6 +- packages/editor/src/i18n/catalogs/es.json | 345 +++++++++++++ packages/editor/src/i18n/catalogs/fr.json | 345 +++++++++++++ packages/editor/src/i18n/catalogs/ja.json | 345 +++++++++++++ packages/editor/src/i18n/catalogs/zh-CN.json | 345 +++++++++++++ packages/editor/src/i18n/locales.test.ts | 22 + packages/editor/src/i18n/locales.ts | 425 ++-------------- packages/editor/src/i18n/source-catalog.json | 469 ++++++++++++++++++ scripts/i18n.test.ts | 57 +++ scripts/i18n.ts | 463 +++++++++++++++++ 14 files changed, 2540 insertions(+), 396 deletions(-) create mode 100644 i18n/README.md create mode 100644 i18n/glossary.json create mode 100644 packages/editor/src/i18n/catalogs/es.json create mode 100644 packages/editor/src/i18n/catalogs/fr.json create mode 100644 packages/editor/src/i18n/catalogs/ja.json create mode 100644 packages/editor/src/i18n/catalogs/zh-CN.json create mode 100644 packages/editor/src/i18n/locales.test.ts create mode 100644 packages/editor/src/i18n/source-catalog.json create mode 100644 scripts/i18n.test.ts create mode 100644 scripts/i18n.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb1f1d782e..dd88e5e69d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,9 @@ jobs: - name: Lint & format check run: bun run check + - name: Check localization catalogs + run: bun run i18n:check + - name: Validate agent skills and plugin packages if: needs.changes.outputs.skills == 'true' || github.event_name == 'push' run: bun run skills:validate diff --git a/i18n/README.md b/i18n/README.md new file mode 100644 index 0000000000..55549d1532 --- /dev/null +++ b/i18n/README.md @@ -0,0 +1,61 @@ +# Localization + +English UI copy stays beside the component that renders it: + +```tsx +const t = useTranslate() +t('viewer.camera.topView', 'Top view') +``` + +The in-code text is the fallback and source of truth. Scene data, exports, +catalog content, MCP/agent text, and developer errors are not translated. + +## Add or adapt a language + +1. Run `bun run i18n:extract` after adding or changing `t()` calls. +2. Add a catalog in `packages/editor/src/i18n/catalogs/.json`, or run + `bun run i18n:translate -- ` for an existing bundled locale. +3. Review every translation in context. The AI command only prepares a diff; + it never runs in the app or CI. +4. Add the locale to `packages/editor/src/i18n/locales.ts` when it should ship + with the editor. +5. Run `bun run i18n:check`. + +Each translation records the hash of its English source. Changing the English +message makes the translation stale until a person updates or approves it. +`i18n:check` also rejects missing keys and changed placeholders. + +To adapt a bundled pack without forking, register an override after it: + +```ts +import { registerMessages, setLocale } from '@pascal-app/core/i18n' +import { registerBundledLocales } from '@pascal-app/editor/locales' + +registerBundledLocales() +registerMessages('fr', 'viewer', { + 'camera.topView': 'Vue en plan', +}) +setLocale('fr') +``` + +Later registrations win. Regional fallback is automatic (`pt-BR` → `pt` → +the English literal). An embedding host can set the locale before mount, pass +``, or use `?lang=fr` in the standalone app and iframe. +Calling `setLocale` later switches the mounted UI. + +Community packs use the same `registerMessages(locale, namespace, messages)` +contract. Keep namespaces with their owner: `core.*`, `viewer.*`, `editor.*`, +`node..*`, or `plugin..*`. + +## AI-assisted fill + +`bun run i18n:translate -- fr` refreshes only missing or stale entries. Set: + +- `I18N_PROVIDER=openai`, `OPENAI_API_KEY`, and `I18N_MODEL`; or +- `I18N_PROVIDER=anthropic`, `ANTHROPIC_API_KEY`, and `I18N_MODEL`. + +`OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` are optional. The command sends the +English copy, source context, and `i18n/glossary.json` to the selected provider, +validates placeholders, and writes JSON only. API keys are never required by +`i18n:check` and must not be committed. Human review remains mandatory, +especially for architecture/CAD terminology and short controls. diff --git a/i18n/glossary.json b/i18n/glossary.json new file mode 100644 index 0000000000..1b93c3701e --- /dev/null +++ b/i18n/glossary.json @@ -0,0 +1,45 @@ +{ + "productContext": "Pascal is an architecture and building-design editor. Use concise professional UI language familiar to architects, CAD users, builders, and interior designers.", + "terms": { + "floor plan": { + "zh-CN": "平面图", + "fr": "plan d’étage", + "es": "plano de planta", + "ja": "平面図", + "note": "A 2D architectural drawing, not a project plan." + }, + "level": { + "zh-CN": "楼层", + "fr": "niveau", + "es": "planta", + "ja": "階層", + "note": "A building storey." + }, + "room": { + "zh-CN": "房间", + "fr": "pièce", + "es": "estancia", + "ja": "部屋" + }, + "scan": { + "zh-CN": "扫描模型", + "fr": "relevé 3D", + "es": "escaneo 3D", + "ja": "3Dスキャン", + "note": "A captured 3D reference, not the verb scan." + }, + "wall": { + "zh-CN": "墙体", + "fr": "mur", + "es": "muro", + "ja": "壁" + }, + "walkthrough": { + "zh-CN": "漫游", + "fr": "visite", + "es": "recorrido", + "ja": "ウォークスルー", + "note": "First-person navigation through the building." + } + } +} diff --git a/package.json b/package.json index 9853ad0dbd..306dda9347 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,11 @@ "checks": "bun run check && bun run check-types", "check:fix": "biome check --write", "check-types": "turbo run check-types", + "i18n:check": "bun scripts/i18n.ts check", + "i18n:extract": "bun scripts/i18n.ts extract", + "i18n:translate": "bun scripts/i18n.ts translate", "test": "turbo run test", - "ci": "bun run check && bun run skills:validate && bun run check-types && bun run test && bun run build", + "ci": "bun run check && bun run i18n:check && bun run skills:validate && bun run check-types && bun run test && bun run build", "postinstall": "([ -n \"$CI\" ] || [ ! -e .git ] || git config core.hooksPath .githooks)", "skills:validate": "bun scripts/validate-skills.ts && bun scripts/sync-agent-versions.ts --check && bun test scripts/sync-agent-versions.test.ts scripts/cursor-plugin-policy.test.ts scripts/clawhub-ignore-policy.test.ts scripts/claude-mcp-config-policy.test.ts scripts/openai-tool-annotation-policy.test.ts scripts/path-containment.test.ts scripts/public-skill-discovery-policy.test.ts", "kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'", diff --git a/packages/editor/src/components/ui/build-panel/build-panel.tsx b/packages/editor/src/components/ui/build-panel/build-panel.tsx index 05ad85fcae..aa727530e5 100644 --- a/packages/editor/src/components/ui/build-panel/build-panel.tsx +++ b/packages/editor/src/components/ui/build-panel/build-panel.tsx @@ -5,7 +5,6 @@ import { ChevronRight } from 'lucide-react' import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react' import { cn } from '../../../lib/utils' import { - WALL_DRAW_IDLE_CAPTION, WALL_DRAW_VARIANTS, type WallDrawVariant, } from '../../../lib/wall-draw-variant' @@ -250,7 +249,10 @@ export function BuildPanelRoomsSection({ lead={activeMessages?.lead} rest={ activeMessages?.rest ?? - t('editor.buildPanel.rooms.idle', WALL_DRAW_IDLE_CAPTION) + t( + 'editor.buildPanel.rooms.idle', + 'Pick a shape to start. Every room brings its own walls, floor and ceiling.', + ) } /> diff --git a/packages/editor/src/i18n/catalogs/es.json b/packages/editor/src/i18n/catalogs/es.json new file mode 100644 index 0000000000..b17c7aafe7 --- /dev/null +++ b/packages/editor/src/i18n/catalogs/es.json @@ -0,0 +1,345 @@ +{ + "locale": "es", + "messages": { + "core.level.basement": { + "message": "Sótano", + "sourceHash": "8caa959c0ae800c3" + }, + "core.level.basementNumber": { + "message": "Sótano {number}", + "sourceHash": "14927085ee91ba05" + }, + "core.level.floorNumber": { + "message": "Planta {number}", + "sourceHash": "e59074a7b9b3f56e" + }, + "core.level.groundFloor": { + "message": "Planta baja", + "sourceHash": "cde3efc9b7ddfa41" + }, + "editor.buildPanel.advanced": { + "message": "Avanzado", + "sourceHash": "f571cb2d06ae01ae" + }, + "editor.buildPanel.rooms.hint": { + "message": "muros · suelo · techo", + "sourceHash": "fb40866c2d6db5fb" + }, + "editor.buildPanel.rooms.idle": { + "message": "Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.", + "sourceHash": "271039bd919dc4c1" + }, + "editor.buildPanel.rooms.polygon.label": { + "message": "Polígono", + "sourceHash": "4e186fc057733162" + }, + "editor.buildPanel.rooms.polygon.lead": { + "message": "Haz clic en cada esquina", + "sourceHash": "345e3aca3879b5c5" + }, + "editor.buildPanel.rooms.polygon.rest": { + "message": "y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.", + "sourceHash": "4b869bde0632749b" + }, + "editor.buildPanel.rooms.polygon.title": { + "message": "Estancia poligonal", + "sourceHash": "8ec6152e139b0e3f" + }, + "editor.buildPanel.rooms.rectangle.label": { + "message": "Rectángulo", + "sourceHash": "2c2c20272b0370cc" + }, + "editor.buildPanel.rooms.rectangle.lead": { + "message": "Haz clic en dos esquinas", + "sourceHash": "060faa48e299d7c9" + }, + "editor.buildPanel.rooms.rectangle.rest": { + "message": "de la cuadrícula. Los muros, el suelo y el techo se crean a la vez.", + "sourceHash": "5c868318eb6e0952" + }, + "editor.buildPanel.rooms.rectangle.title": { + "message": "Estancia rectangular", + "sourceHash": "83a45603ac7f41e8" + }, + "editor.buildPanel.rooms.title": { + "message": "Estancias", + "sourceHash": "3e7408ed44676176" + }, + "editor.buildPanel.rooms.walls.label": { + "message": "Muros", + "sourceHash": "aac9a732d2159ccc" + }, + "editor.buildPanel.rooms.walls.lead": { + "message": "Dibuja un muro cada vez.", + "sourceHash": "9843edd68426fa2d" + }, + "editor.buildPanel.rooms.walls.rest": { + "message": "Cierra el contorno para crear una estancia o déjalo abierto para tabiques.", + "sourceHash": "e3c4f3c4a5a9d8a6" + }, + "editor.buildPanel.rooms.walls.title": { + "message": "Muros", + "sourceHash": "aac9a732d2159ccc" + }, + "viewer.camera.orbitLeft": { + "message": "Orbitar a la izquierda", + "sourceHash": "8bf25afa3cb8baea" + }, + "viewer.camera.orbitRight": { + "message": "Orbitar a la derecha", + "sourceHash": "b9cc64c1d1bb0d46" + }, + "viewer.camera.topView": { + "message": "Vista superior", + "sourceHash": "1ab1eb3a14f4b0e0" + }, + "viewer.common.off": { + "message": "Desactivado", + "sourceHash": "50accd740fa2e736" + }, + "viewer.common.on": { + "message": "Activado", + "sourceHash": "9cc01faad79c28da" + }, + "viewer.display.camera": { + "message": "Cámara", + "sourceHash": "14622745dbf93f4c" + }, + "viewer.display.colored": { + "message": "A color", + "sourceHash": "d3d478fa6c6d7ddc" + }, + "viewer.display.colors": { + "message": "Colores", + "sourceHash": "82b74c4f5ef43336" + }, + "viewer.display.edges.off": { + "message": "Desactivadas", + "sourceHash": "50accd740fa2e736" + }, + "viewer.display.edges.offDetail": { + "message": "Sin líneas de arista", + "sourceHash": "4ff58913a4492199" + }, + "viewer.display.edges.soft": { + "message": "Suaves", + "sourceHash": "d266d83ec490df4e" + }, + "viewer.display.edges.softDetail": { + "message": "Contorno tenue de los pliegues principales", + "sourceHash": "0c583f1d4201293b" + }, + "viewer.display.edges.strong": { + "message": "Marcadas", + "sourceHash": "f82927a01d4c5e45" + }, + "viewer.display.edges.strongDetail": { + "message": "Líneas de arista nítidas y opacas", + "sourceHash": "2416a48df28d6205" + }, + "viewer.display.edges.title": { + "message": "Aristas", + "sourceHash": "027bbc8b7287c06a" + }, + "viewer.display.monochrome": { + "message": "Monocromo", + "sourceHash": "ecde2f7c26914be3" + }, + "viewer.display.orthographic": { + "message": "Ortográfica", + "sourceHash": "ed59dd8ce7797a66" + }, + "viewer.display.perspective": { + "message": "Perspectiva", + "sourceHash": "316e25993ff72b88" + }, + "viewer.display.render.rendered": { + "message": "Realista", + "sourceHash": "f363702a10aedaeb" + }, + "viewer.display.render.renderedDetail": { + "message": "Oclusión ambiental completa", + "sourceHash": "8bb9e54fc786ca77" + }, + "viewer.display.render.solid": { + "message": "Sólido", + "sourceHash": "ef3e765e692faf4c" + }, + "viewer.display.render.solidDetail": { + "message": "Plano y rápido, sin oclusión ambiental", + "sourceHash": "8827df1cdeb02ed9" + }, + "viewer.display.render.title": { + "message": "Renderizado", + "sourceHash": "5e58af99d6d4fa6a" + }, + "viewer.display.shadows": { + "message": "Sombras", + "sourceHash": "4c6bcb30b40047fc" + }, + "viewer.display.title": { + "message": "Ajustes de visualización", + "sourceHash": "d0db46f206a88e2e" + }, + "viewer.floorplan.alignNorth": { + "message": "Orientar la vista al norte", + "sourceHash": "2bdf83801d4a40af" + }, + "viewer.floorplan.empty": { + "message": "No hay planos de planta disponibles para esta escena.", + "sourceHash": "286a757d8b52bb02" + }, + "viewer.floorplan.fit": { + "message": "Ajustar plano", + "sourceHash": "bf63c679ab94dbcd" + }, + "viewer.floorplan.floor": { + "message": "Planta", + "sourceHash": "d7148fa065bc709b" + }, + "viewer.floorplan.floorPlan": { + "message": "Plano de planta", + "sourceHash": "fee6203bb9046929" + }, + "viewer.floorplan.viewLabel": { + "message": "Vista 2D de {floor}", + "sourceHash": "28b255b7bf5f1084" + }, + "viewer.floorplan.zoomIn": { + "message": "Acercar", + "sourceHash": "d17804412f8dd081" + }, + "viewer.floorplan.zoomOut": { + "message": "Alejar", + "sourceHash": "23b012a283b2c14a" + }, + "viewer.header.back": { + "message": "Volver", + "sourceHash": "7ceade8f25b58fe9" + }, + "viewer.header.levels": { + "message": "Plantas", + "sourceHash": "cc3d601d59e2e26c" + }, + "viewer.header.rooms": { + "message": "Estancias", + "sourceHash": "3e7408ed44676176" + }, + "viewer.header.selection": { + "message": "Selección", + "sourceHash": "170b2049ef0f9131" + }, + "viewer.header.units": { + "message": "Unidades", + "sourceHash": "b404dded35241274" + }, + "viewer.header.untitled": { + "message": "Sin título", + "sourceHash": "6547a4c0c368042a" + }, + "viewer.header.zones": { + "message": "Zonas", + "sourceHash": "665db49a26f81425" + }, + "viewer.levels.exploded": { + "message": "Separadas", + "sourceHash": "be12f8788f1f0a05" + }, + "viewer.levels.label": { + "message": "Plantas: {mode}", + "sourceHash": "4bf4a6861d7d633e" + }, + "viewer.levels.manual": { + "message": "Manual", + "sourceHash": "387082cd5f118bed" + }, + "viewer.levels.solo": { + "message": "Aislada", + "sourceHash": "e9f59b513f2f45ac" + }, + "viewer.levels.stacked": { + "message": "Apiladas", + "sourceHash": "c203c84eee18c0f6" + }, + "viewer.stage.layout": { + "message": "Disposición de vistas", + "sourceHash": "9dd21ca47cffa3b8" + }, + "viewer.stage.split": { + "message": "Dividida", + "sourceHash": "e4cd64b1680ac0d2" + }, + "viewer.theme.blueprint": { + "message": "Plano técnico", + "sourceHash": "f14b89d713d2528a" + }, + "viewer.theme.mediterranean": { + "message": "Mediterráneo", + "sourceHash": "142bcb2841f423c5" + }, + "viewer.theme.night": { + "message": "Noche", + "sourceHash": "b399942ec5d7e1ba" + }, + "viewer.theme.overcast": { + "message": "Nublado", + "sourceHash": "f512d1b947b8ae5a" + }, + "viewer.theme.paper": { + "message": "Papel", + "sourceHash": "583542b3bc837192" + }, + "viewer.theme.studio": { + "message": "Estudio", + "sourceHash": "7abc2dc6e39b1747" + }, + "viewer.theme.sunset": { + "message": "Atardecer", + "sourceHash": "efe94d3ec8d8c44e" + }, + "viewer.theme.title": { + "message": "Ambiente", + "sourceHash": "82720864e5034398" + }, + "viewer.theme.twilight": { + "message": "Crepúsculo", + "sourceHash": "4c10ea0aa0e2ea41" + }, + "viewer.theme.verdant": { + "message": "Verde", + "sourceHash": "f9592fb0672740c0" + }, + "viewer.visibility.guides": { + "message": "Guías", + "sourceHash": "892d5dd34372028b" + }, + "viewer.visibility.scans": { + "message": "Escaneos 3D", + "sourceHash": "3260bfa69cca50e5" + }, + "viewer.visibility.title": { + "message": "Visibilidad", + "sourceHash": "2cefbb554eadbb6e" + }, + "viewer.walkthrough.label": { + "message": "Recorrido: {state}", + "sourceHash": "31afd9dee47ba943" + }, + "viewer.walls.cutaway": { + "message": "Vista seccionada", + "sourceHash": "4da8b3678ed77b45" + }, + "viewer.walls.fullHeight": { + "message": "Altura completa", + "sourceHash": "131bac2467f4fdf6" + }, + "viewer.walls.label": { + "message": "Muros: {mode}", + "sourceHash": "32f3d058cf2526d2" + }, + "viewer.walls.low": { + "message": "Bajos", + "sourceHash": "803532407fb5962b" + } + } +} diff --git a/packages/editor/src/i18n/catalogs/fr.json b/packages/editor/src/i18n/catalogs/fr.json new file mode 100644 index 0000000000..0d31ca9fe6 --- /dev/null +++ b/packages/editor/src/i18n/catalogs/fr.json @@ -0,0 +1,345 @@ +{ + "locale": "fr", + "messages": { + "core.level.basement": { + "message": "Sous-sol", + "sourceHash": "8caa959c0ae800c3" + }, + "core.level.basementNumber": { + "message": "Sous-sol {number}", + "sourceHash": "14927085ee91ba05" + }, + "core.level.floorNumber": { + "message": "Étage {number}", + "sourceHash": "e59074a7b9b3f56e" + }, + "core.level.groundFloor": { + "message": "Rez-de-chaussée", + "sourceHash": "cde3efc9b7ddfa41" + }, + "editor.buildPanel.advanced": { + "message": "Avancé", + "sourceHash": "f571cb2d06ae01ae" + }, + "editor.buildPanel.rooms.hint": { + "message": "murs · sol · plafond", + "sourceHash": "fb40866c2d6db5fb" + }, + "editor.buildPanel.rooms.idle": { + "message": "Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.", + "sourceHash": "271039bd919dc4c1" + }, + "editor.buildPanel.rooms.polygon.label": { + "message": "Polygone", + "sourceHash": "4e186fc057733162" + }, + "editor.buildPanel.rooms.polygon.lead": { + "message": "Cliquez sur chaque angle,", + "sourceHash": "345e3aca3879b5c5" + }, + "editor.buildPanel.rooms.polygon.rest": { + "message": "puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.", + "sourceHash": "4b869bde0632749b" + }, + "editor.buildPanel.rooms.polygon.title": { + "message": "Pièce polygonale", + "sourceHash": "8ec6152e139b0e3f" + }, + "editor.buildPanel.rooms.rectangle.label": { + "message": "Rectangle", + "sourceHash": "2c2c20272b0370cc" + }, + "editor.buildPanel.rooms.rectangle.lead": { + "message": "Cliquez sur deux angles", + "sourceHash": "060faa48e299d7c9" + }, + "editor.buildPanel.rooms.rectangle.rest": { + "message": "de la grille. Les murs, le sol et le plafond sont créés ensemble.", + "sourceHash": "5c868318eb6e0952" + }, + "editor.buildPanel.rooms.rectangle.title": { + "message": "Pièce rectangulaire", + "sourceHash": "83a45603ac7f41e8" + }, + "editor.buildPanel.rooms.title": { + "message": "Pièces", + "sourceHash": "3e7408ed44676176" + }, + "editor.buildPanel.rooms.walls.label": { + "message": "Murs", + "sourceHash": "aac9a732d2159ccc" + }, + "editor.buildPanel.rooms.walls.lead": { + "message": "Tracez un mur à la fois.", + "sourceHash": "9843edd68426fa2d" + }, + "editor.buildPanel.rooms.walls.rest": { + "message": "Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.", + "sourceHash": "e3c4f3c4a5a9d8a6" + }, + "editor.buildPanel.rooms.walls.title": { + "message": "Murs", + "sourceHash": "aac9a732d2159ccc" + }, + "viewer.camera.orbitLeft": { + "message": "Pivoter à gauche", + "sourceHash": "8bf25afa3cb8baea" + }, + "viewer.camera.orbitRight": { + "message": "Pivoter à droite", + "sourceHash": "b9cc64c1d1bb0d46" + }, + "viewer.camera.topView": { + "message": "Vue de dessus", + "sourceHash": "1ab1eb3a14f4b0e0" + }, + "viewer.common.off": { + "message": "Désactivé", + "sourceHash": "50accd740fa2e736" + }, + "viewer.common.on": { + "message": "Activé", + "sourceHash": "9cc01faad79c28da" + }, + "viewer.display.camera": { + "message": "Caméra", + "sourceHash": "14622745dbf93f4c" + }, + "viewer.display.colored": { + "message": "En couleur", + "sourceHash": "d3d478fa6c6d7ddc" + }, + "viewer.display.colors": { + "message": "Couleurs", + "sourceHash": "82b74c4f5ef43336" + }, + "viewer.display.edges.off": { + "message": "Désactivées", + "sourceHash": "50accd740fa2e736" + }, + "viewer.display.edges.offDetail": { + "message": "Aucune ligne d’arête", + "sourceHash": "4ff58913a4492199" + }, + "viewer.display.edges.soft": { + "message": "Douces", + "sourceHash": "d266d83ec490df4e" + }, + "viewer.display.edges.softDetail": { + "message": "Contour léger des plis principaux", + "sourceHash": "0c583f1d4201293b" + }, + "viewer.display.edges.strong": { + "message": "Marquées", + "sourceHash": "f82927a01d4c5e45" + }, + "viewer.display.edges.strongDetail": { + "message": "Lignes d’arête nettes et opaques", + "sourceHash": "2416a48df28d6205" + }, + "viewer.display.edges.title": { + "message": "Arêtes", + "sourceHash": "027bbc8b7287c06a" + }, + "viewer.display.monochrome": { + "message": "Monochrome", + "sourceHash": "ecde2f7c26914be3" + }, + "viewer.display.orthographic": { + "message": "Orthographique", + "sourceHash": "ed59dd8ce7797a66" + }, + "viewer.display.perspective": { + "message": "Perspective", + "sourceHash": "316e25993ff72b88" + }, + "viewer.display.render.rendered": { + "message": "Réaliste", + "sourceHash": "f363702a10aedaeb" + }, + "viewer.display.render.renderedDetail": { + "message": "Occlusion ambiante complète", + "sourceHash": "8bb9e54fc786ca77" + }, + "viewer.display.render.solid": { + "message": "Solide", + "sourceHash": "ef3e765e692faf4c" + }, + "viewer.display.render.solidDetail": { + "message": "Rapide et uniforme, sans occlusion ambiante", + "sourceHash": "8827df1cdeb02ed9" + }, + "viewer.display.render.title": { + "message": "Rendu", + "sourceHash": "5e58af99d6d4fa6a" + }, + "viewer.display.shadows": { + "message": "Ombres", + "sourceHash": "4c6bcb30b40047fc" + }, + "viewer.display.title": { + "message": "Paramètres d’affichage", + "sourceHash": "d0db46f206a88e2e" + }, + "viewer.floorplan.alignNorth": { + "message": "Orienter la vue vers le nord", + "sourceHash": "2bdf83801d4a40af" + }, + "viewer.floorplan.empty": { + "message": "Aucun plan d’étage n’est disponible pour cette scène.", + "sourceHash": "286a757d8b52bb02" + }, + "viewer.floorplan.fit": { + "message": "Ajuster le plan", + "sourceHash": "bf63c679ab94dbcd" + }, + "viewer.floorplan.floor": { + "message": "Niveau", + "sourceHash": "d7148fa065bc709b" + }, + "viewer.floorplan.floorPlan": { + "message": "Plan d’étage", + "sourceHash": "fee6203bb9046929" + }, + "viewer.floorplan.viewLabel": { + "message": "Vue 2D — {floor}", + "sourceHash": "28b255b7bf5f1084" + }, + "viewer.floorplan.zoomIn": { + "message": "Zoom avant", + "sourceHash": "d17804412f8dd081" + }, + "viewer.floorplan.zoomOut": { + "message": "Zoom arrière", + "sourceHash": "23b012a283b2c14a" + }, + "viewer.header.back": { + "message": "Retour", + "sourceHash": "7ceade8f25b58fe9" + }, + "viewer.header.levels": { + "message": "Niveaux", + "sourceHash": "cc3d601d59e2e26c" + }, + "viewer.header.rooms": { + "message": "Pièces", + "sourceHash": "3e7408ed44676176" + }, + "viewer.header.selection": { + "message": "Sélection", + "sourceHash": "170b2049ef0f9131" + }, + "viewer.header.units": { + "message": "Unités", + "sourceHash": "b404dded35241274" + }, + "viewer.header.untitled": { + "message": "Sans titre", + "sourceHash": "6547a4c0c368042a" + }, + "viewer.header.zones": { + "message": "Zones", + "sourceHash": "665db49a26f81425" + }, + "viewer.levels.exploded": { + "message": "Éclatés", + "sourceHash": "be12f8788f1f0a05" + }, + "viewer.levels.label": { + "message": "Niveaux : {mode}", + "sourceHash": "4bf4a6861d7d633e" + }, + "viewer.levels.manual": { + "message": "Manuel", + "sourceHash": "387082cd5f118bed" + }, + "viewer.levels.solo": { + "message": "Isolé", + "sourceHash": "e9f59b513f2f45ac" + }, + "viewer.levels.stacked": { + "message": "Superposés", + "sourceHash": "c203c84eee18c0f6" + }, + "viewer.stage.layout": { + "message": "Disposition des vues", + "sourceHash": "9dd21ca47cffa3b8" + }, + "viewer.stage.split": { + "message": "Partagé", + "sourceHash": "e4cd64b1680ac0d2" + }, + "viewer.theme.blueprint": { + "message": "Plan", + "sourceHash": "f14b89d713d2528a" + }, + "viewer.theme.mediterranean": { + "message": "Méditerranéen", + "sourceHash": "142bcb2841f423c5" + }, + "viewer.theme.night": { + "message": "Nuit", + "sourceHash": "b399942ec5d7e1ba" + }, + "viewer.theme.overcast": { + "message": "Ciel couvert", + "sourceHash": "f512d1b947b8ae5a" + }, + "viewer.theme.paper": { + "message": "Papier", + "sourceHash": "583542b3bc837192" + }, + "viewer.theme.studio": { + "message": "Studio", + "sourceHash": "7abc2dc6e39b1747" + }, + "viewer.theme.sunset": { + "message": "Coucher de soleil", + "sourceHash": "efe94d3ec8d8c44e" + }, + "viewer.theme.title": { + "message": "Ambiance", + "sourceHash": "82720864e5034398" + }, + "viewer.theme.twilight": { + "message": "Crépuscule", + "sourceHash": "4c10ea0aa0e2ea41" + }, + "viewer.theme.verdant": { + "message": "Verdoyant", + "sourceHash": "f9592fb0672740c0" + }, + "viewer.visibility.guides": { + "message": "Repères", + "sourceHash": "892d5dd34372028b" + }, + "viewer.visibility.scans": { + "message": "Relevés 3D", + "sourceHash": "3260bfa69cca50e5" + }, + "viewer.visibility.title": { + "message": "Visibilité", + "sourceHash": "2cefbb554eadbb6e" + }, + "viewer.walkthrough.label": { + "message": "Visite : {state}", + "sourceHash": "31afd9dee47ba943" + }, + "viewer.walls.cutaway": { + "message": "Vue en coupe", + "sourceHash": "4da8b3678ed77b45" + }, + "viewer.walls.fullHeight": { + "message": "Pleine hauteur", + "sourceHash": "131bac2467f4fdf6" + }, + "viewer.walls.label": { + "message": "Murs : {mode}", + "sourceHash": "32f3d058cf2526d2" + }, + "viewer.walls.low": { + "message": "Bas", + "sourceHash": "803532407fb5962b" + } + } +} diff --git a/packages/editor/src/i18n/catalogs/ja.json b/packages/editor/src/i18n/catalogs/ja.json new file mode 100644 index 0000000000..1f6d1bea20 --- /dev/null +++ b/packages/editor/src/i18n/catalogs/ja.json @@ -0,0 +1,345 @@ +{ + "locale": "ja", + "messages": { + "core.level.basement": { + "message": "地下1階", + "sourceHash": "8caa959c0ae800c3" + }, + "core.level.basementNumber": { + "message": "地下{number}階", + "sourceHash": "14927085ee91ba05" + }, + "core.level.floorNumber": { + "message": "{number}階", + "sourceHash": "e59074a7b9b3f56e" + }, + "core.level.groundFloor": { + "message": "1階", + "sourceHash": "cde3efc9b7ddfa41" + }, + "editor.buildPanel.advanced": { + "message": "詳細", + "sourceHash": "f571cb2d06ae01ae" + }, + "editor.buildPanel.rooms.hint": { + "message": "壁 · 床 · 天井", + "sourceHash": "fb40866c2d6db5fb" + }, + "editor.buildPanel.rooms.idle": { + "message": "形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。", + "sourceHash": "271039bd919dc4c1" + }, + "editor.buildPanel.rooms.polygon.label": { + "message": "多角形", + "sourceHash": "4e186fc057733162" + }, + "editor.buildPanel.rooms.polygon.lead": { + "message": "各頂点をクリックし、", + "sourceHash": "345e3aca3879b5c5" + }, + "editor.buildPanel.rooms.polygon.rest": { + "message": "最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。", + "sourceHash": "4b869bde0632749b" + }, + "editor.buildPanel.rooms.polygon.title": { + "message": "多角形の部屋", + "sourceHash": "8ec6152e139b0e3f" + }, + "editor.buildPanel.rooms.rectangle.label": { + "message": "長方形", + "sourceHash": "2c2c20272b0370cc" + }, + "editor.buildPanel.rooms.rectangle.lead": { + "message": "対角の2点をクリック", + "sourceHash": "060faa48e299d7c9" + }, + "editor.buildPanel.rooms.rectangle.rest": { + "message": "グリッド上で指定します。壁・床・天井をまとめて作成します。", + "sourceHash": "5c868318eb6e0952" + }, + "editor.buildPanel.rooms.rectangle.title": { + "message": "長方形の部屋", + "sourceHash": "83a45603ac7f41e8" + }, + "editor.buildPanel.rooms.title": { + "message": "部屋", + "sourceHash": "3e7408ed44676176" + }, + "editor.buildPanel.rooms.walls.label": { + "message": "壁", + "sourceHash": "aac9a732d2159ccc" + }, + "editor.buildPanel.rooms.walls.lead": { + "message": "壁を1面ずつ描画します。", + "sourceHash": "9843edd68426fa2d" + }, + "editor.buildPanel.rooms.walls.rest": { + "message": "輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。", + "sourceHash": "e3c4f3c4a5a9d8a6" + }, + "editor.buildPanel.rooms.walls.title": { + "message": "壁", + "sourceHash": "aac9a732d2159ccc" + }, + "viewer.camera.orbitLeft": { + "message": "左に回転", + "sourceHash": "8bf25afa3cb8baea" + }, + "viewer.camera.orbitRight": { + "message": "右に回転", + "sourceHash": "b9cc64c1d1bb0d46" + }, + "viewer.camera.topView": { + "message": "上面図", + "sourceHash": "1ab1eb3a14f4b0e0" + }, + "viewer.common.off": { + "message": "オフ", + "sourceHash": "50accd740fa2e736" + }, + "viewer.common.on": { + "message": "オン", + "sourceHash": "9cc01faad79c28da" + }, + "viewer.display.camera": { + "message": "カメラ", + "sourceHash": "14622745dbf93f4c" + }, + "viewer.display.colored": { + "message": "カラー表示", + "sourceHash": "d3d478fa6c6d7ddc" + }, + "viewer.display.colors": { + "message": "カラー", + "sourceHash": "82b74c4f5ef43336" + }, + "viewer.display.edges.off": { + "message": "オフ", + "sourceHash": "50accd740fa2e736" + }, + "viewer.display.edges.offDetail": { + "message": "エッジ線を表示しない", + "sourceHash": "4ff58913a4492199" + }, + "viewer.display.edges.soft": { + "message": "ソフト", + "sourceHash": "d266d83ec490df4e" + }, + "viewer.display.edges.softDetail": { + "message": "主要な稜線を薄く表示", + "sourceHash": "0c583f1d4201293b" + }, + "viewer.display.edges.strong": { + "message": "強調", + "sourceHash": "f82927a01d4c5e45" + }, + "viewer.display.edges.strongDetail": { + "message": "エッジ線をくっきり表示", + "sourceHash": "2416a48df28d6205" + }, + "viewer.display.edges.title": { + "message": "エッジ", + "sourceHash": "027bbc8b7287c06a" + }, + "viewer.display.monochrome": { + "message": "モノクロ", + "sourceHash": "ecde2f7c26914be3" + }, + "viewer.display.orthographic": { + "message": "平行投影", + "sourceHash": "ed59dd8ce7797a66" + }, + "viewer.display.perspective": { + "message": "透視投影", + "sourceHash": "316e25993ff72b88" + }, + "viewer.display.render.rendered": { + "message": "レンダリング", + "sourceHash": "f363702a10aedaeb" + }, + "viewer.display.render.renderedDetail": { + "message": "アンビエントオクルージョンを完全表示", + "sourceHash": "8bb9e54fc786ca77" + }, + "viewer.display.render.solid": { + "message": "ソリッド", + "sourceHash": "ef3e765e692faf4c" + }, + "viewer.display.render.solidDetail": { + "message": "軽量表示(アンビエントオクルージョンなし)", + "sourceHash": "8827df1cdeb02ed9" + }, + "viewer.display.render.title": { + "message": "レンダリング", + "sourceHash": "5e58af99d6d4fa6a" + }, + "viewer.display.shadows": { + "message": "影", + "sourceHash": "4c6bcb30b40047fc" + }, + "viewer.display.title": { + "message": "表示設定", + "sourceHash": "d0db46f206a88e2e" + }, + "viewer.floorplan.alignNorth": { + "message": "北を上に表示", + "sourceHash": "2bdf83801d4a40af" + }, + "viewer.floorplan.empty": { + "message": "このシーンには表示できる平面図がありません。", + "sourceHash": "286a757d8b52bb02" + }, + "viewer.floorplan.fit": { + "message": "全体を表示", + "sourceHash": "bf63c679ab94dbcd" + }, + "viewer.floorplan.floor": { + "message": "階", + "sourceHash": "d7148fa065bc709b" + }, + "viewer.floorplan.floorPlan": { + "message": "平面図", + "sourceHash": "fee6203bb9046929" + }, + "viewer.floorplan.viewLabel": { + "message": "{floor}の2Dビュー", + "sourceHash": "28b255b7bf5f1084" + }, + "viewer.floorplan.zoomIn": { + "message": "拡大", + "sourceHash": "d17804412f8dd081" + }, + "viewer.floorplan.zoomOut": { + "message": "縮小", + "sourceHash": "23b012a283b2c14a" + }, + "viewer.header.back": { + "message": "戻る", + "sourceHash": "7ceade8f25b58fe9" + }, + "viewer.header.levels": { + "message": "階層", + "sourceHash": "cc3d601d59e2e26c" + }, + "viewer.header.rooms": { + "message": "部屋", + "sourceHash": "3e7408ed44676176" + }, + "viewer.header.selection": { + "message": "選択", + "sourceHash": "170b2049ef0f9131" + }, + "viewer.header.units": { + "message": "住戸", + "sourceHash": "b404dded35241274" + }, + "viewer.header.untitled": { + "message": "名称未設定", + "sourceHash": "6547a4c0c368042a" + }, + "viewer.header.zones": { + "message": "ゾーン", + "sourceHash": "665db49a26f81425" + }, + "viewer.levels.exploded": { + "message": "分解", + "sourceHash": "be12f8788f1f0a05" + }, + "viewer.levels.label": { + "message": "階層:{mode}", + "sourceHash": "4bf4a6861d7d633e" + }, + "viewer.levels.manual": { + "message": "手動", + "sourceHash": "387082cd5f118bed" + }, + "viewer.levels.solo": { + "message": "単独", + "sourceHash": "e9f59b513f2f45ac" + }, + "viewer.levels.stacked": { + "message": "積層", + "sourceHash": "c203c84eee18c0f6" + }, + "viewer.stage.layout": { + "message": "ビュー配置", + "sourceHash": "9dd21ca47cffa3b8" + }, + "viewer.stage.split": { + "message": "分割", + "sourceHash": "e4cd64b1680ac0d2" + }, + "viewer.theme.blueprint": { + "message": "青図", + "sourceHash": "f14b89d713d2528a" + }, + "viewer.theme.mediterranean": { + "message": "地中海", + "sourceHash": "142bcb2841f423c5" + }, + "viewer.theme.night": { + "message": "夜景", + "sourceHash": "b399942ec5d7e1ba" + }, + "viewer.theme.overcast": { + "message": "曇天", + "sourceHash": "f512d1b947b8ae5a" + }, + "viewer.theme.paper": { + "message": "ペーパー", + "sourceHash": "583542b3bc837192" + }, + "viewer.theme.studio": { + "message": "スタジオ", + "sourceHash": "7abc2dc6e39b1747" + }, + "viewer.theme.sunset": { + "message": "夕景", + "sourceHash": "efe94d3ec8d8c44e" + }, + "viewer.theme.title": { + "message": "シーンテーマ", + "sourceHash": "82720864e5034398" + }, + "viewer.theme.twilight": { + "message": "薄暮", + "sourceHash": "4c10ea0aa0e2ea41" + }, + "viewer.theme.verdant": { + "message": "緑景", + "sourceHash": "f9592fb0672740c0" + }, + "viewer.visibility.guides": { + "message": "ガイド", + "sourceHash": "892d5dd34372028b" + }, + "viewer.visibility.scans": { + "message": "3Dスキャン", + "sourceHash": "3260bfa69cca50e5" + }, + "viewer.visibility.title": { + "message": "表示", + "sourceHash": "2cefbb554eadbb6e" + }, + "viewer.walkthrough.label": { + "message": "ウォークスルー:{state}", + "sourceHash": "31afd9dee47ba943" + }, + "viewer.walls.cutaway": { + "message": "カット表示", + "sourceHash": "4da8b3678ed77b45" + }, + "viewer.walls.fullHeight": { + "message": "全高", + "sourceHash": "131bac2467f4fdf6" + }, + "viewer.walls.label": { + "message": "壁:{mode}", + "sourceHash": "32f3d058cf2526d2" + }, + "viewer.walls.low": { + "message": "低く表示", + "sourceHash": "803532407fb5962b" + } + } +} diff --git a/packages/editor/src/i18n/catalogs/zh-CN.json b/packages/editor/src/i18n/catalogs/zh-CN.json new file mode 100644 index 0000000000..a2dd3aaaad --- /dev/null +++ b/packages/editor/src/i18n/catalogs/zh-CN.json @@ -0,0 +1,345 @@ +{ + "locale": "zh-CN", + "messages": { + "core.level.basement": { + "message": "地下层", + "sourceHash": "8caa959c0ae800c3" + }, + "core.level.basementNumber": { + "message": "地下 {number} 层", + "sourceHash": "14927085ee91ba05" + }, + "core.level.floorNumber": { + "message": "{number} 层", + "sourceHash": "e59074a7b9b3f56e" + }, + "core.level.groundFloor": { + "message": "首层", + "sourceHash": "cde3efc9b7ddfa41" + }, + "editor.buildPanel.advanced": { + "message": "高级", + "sourceHash": "f571cb2d06ae01ae" + }, + "editor.buildPanel.rooms.hint": { + "message": "墙体 · 楼板 · 吊顶", + "sourceHash": "fb40866c2d6db5fb" + }, + "editor.buildPanel.rooms.idle": { + "message": "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和吊顶。", + "sourceHash": "271039bd919dc4c1" + }, + "editor.buildPanel.rooms.polygon.label": { + "message": "多边形", + "sourceHash": "4e186fc057733162" + }, + "editor.buildPanel.rooms.polygon.lead": { + "message": "依次点击各个角点,", + "sourceHash": "345e3aca3879b5c5" + }, + "editor.buildPanel.rooms.polygon.rest": { + "message": "然后再次点击起点闭合。适用于 L 形和斜角房间。", + "sourceHash": "4b869bde0632749b" + }, + "editor.buildPanel.rooms.polygon.title": { + "message": "多边形房间", + "sourceHash": "8ec6152e139b0e3f" + }, + "editor.buildPanel.rooms.rectangle.label": { + "message": "矩形", + "sourceHash": "2c2c20272b0370cc" + }, + "editor.buildPanel.rooms.rectangle.lead": { + "message": "点击两个对角点", + "sourceHash": "060faa48e299d7c9" + }, + "editor.buildPanel.rooms.rectangle.rest": { + "message": "在网格上。墙体、楼板和吊顶将一并创建。", + "sourceHash": "5c868318eb6e0952" + }, + "editor.buildPanel.rooms.rectangle.title": { + "message": "矩形房间", + "sourceHash": "83a45603ac7f41e8" + }, + "editor.buildPanel.rooms.title": { + "message": "房间", + "sourceHash": "3e7408ed44676176" + }, + "editor.buildPanel.rooms.walls.label": { + "message": "墙体", + "sourceHash": "aac9a732d2159ccc" + }, + "editor.buildPanel.rooms.walls.lead": { + "message": "逐面绘制墙体。", + "sourceHash": "9843edd68426fa2d" + }, + "editor.buildPanel.rooms.walls.rest": { + "message": "闭合轮廓后会形成房间;保持开口可绘制隔墙。", + "sourceHash": "e3c4f3c4a5a9d8a6" + }, + "editor.buildPanel.rooms.walls.title": { + "message": "墙体", + "sourceHash": "aac9a732d2159ccc" + }, + "viewer.camera.orbitLeft": { + "message": "向左旋转", + "sourceHash": "8bf25afa3cb8baea" + }, + "viewer.camera.orbitRight": { + "message": "向右旋转", + "sourceHash": "b9cc64c1d1bb0d46" + }, + "viewer.camera.topView": { + "message": "俯视图", + "sourceHash": "1ab1eb3a14f4b0e0" + }, + "viewer.common.off": { + "message": "关闭", + "sourceHash": "50accd740fa2e736" + }, + "viewer.common.on": { + "message": "开启", + "sourceHash": "9cc01faad79c28da" + }, + "viewer.display.camera": { + "message": "相机", + "sourceHash": "14622745dbf93f4c" + }, + "viewer.display.colored": { + "message": "彩色", + "sourceHash": "d3d478fa6c6d7ddc" + }, + "viewer.display.colors": { + "message": "颜色", + "sourceHash": "82b74c4f5ef43336" + }, + "viewer.display.edges.off": { + "message": "关闭", + "sourceHash": "50accd740fa2e736" + }, + "viewer.display.edges.offDetail": { + "message": "不显示边线", + "sourceHash": "4ff58913a4492199" + }, + "viewer.display.edges.soft": { + "message": "柔和", + "sourceHash": "d266d83ec490df4e" + }, + "viewer.display.edges.softDetail": { + "message": "淡化显示主要折线", + "sourceHash": "0c583f1d4201293b" + }, + "viewer.display.edges.strong": { + "message": "清晰", + "sourceHash": "f82927a01d4c5e45" + }, + "viewer.display.edges.strongDetail": { + "message": "清晰、不透明的边线", + "sourceHash": "2416a48df28d6205" + }, + "viewer.display.edges.title": { + "message": "边线", + "sourceHash": "027bbc8b7287c06a" + }, + "viewer.display.monochrome": { + "message": "单色", + "sourceHash": "ecde2f7c26914be3" + }, + "viewer.display.orthographic": { + "message": "正交", + "sourceHash": "ed59dd8ce7797a66" + }, + "viewer.display.perspective": { + "message": "透视", + "sourceHash": "316e25993ff72b88" + }, + "viewer.display.render.rendered": { + "message": "渲染效果", + "sourceHash": "f363702a10aedaeb" + }, + "viewer.display.render.renderedDetail": { + "message": "完整环境光遮蔽", + "sourceHash": "8bb9e54fc786ca77" + }, + "viewer.display.render.solid": { + "message": "实体", + "sourceHash": "ef3e765e692faf4c" + }, + "viewer.display.render.solidDetail": { + "message": "平整快速,不启用环境光遮蔽", + "sourceHash": "8827df1cdeb02ed9" + }, + "viewer.display.render.title": { + "message": "渲染", + "sourceHash": "5e58af99d6d4fa6a" + }, + "viewer.display.shadows": { + "message": "阴影", + "sourceHash": "4c6bcb30b40047fc" + }, + "viewer.display.title": { + "message": "显示设置", + "sourceHash": "d0db46f206a88e2e" + }, + "viewer.floorplan.alignNorth": { + "message": "视图对齐北向", + "sourceHash": "2bdf83801d4a40af" + }, + "viewer.floorplan.empty": { + "message": "此场景没有可用的平面图。", + "sourceHash": "286a757d8b52bb02" + }, + "viewer.floorplan.fit": { + "message": "适合窗口", + "sourceHash": "bf63c679ab94dbcd" + }, + "viewer.floorplan.floor": { + "message": "楼层", + "sourceHash": "d7148fa065bc709b" + }, + "viewer.floorplan.floorPlan": { + "message": "平面图", + "sourceHash": "fee6203bb9046929" + }, + "viewer.floorplan.viewLabel": { + "message": "{floor}二维视图", + "sourceHash": "28b255b7bf5f1084" + }, + "viewer.floorplan.zoomIn": { + "message": "放大", + "sourceHash": "d17804412f8dd081" + }, + "viewer.floorplan.zoomOut": { + "message": "缩小", + "sourceHash": "23b012a283b2c14a" + }, + "viewer.header.back": { + "message": "返回", + "sourceHash": "7ceade8f25b58fe9" + }, + "viewer.header.levels": { + "message": "楼层", + "sourceHash": "cc3d601d59e2e26c" + }, + "viewer.header.rooms": { + "message": "房间", + "sourceHash": "3e7408ed44676176" + }, + "viewer.header.selection": { + "message": "当前选择", + "sourceHash": "170b2049ef0f9131" + }, + "viewer.header.units": { + "message": "单元", + "sourceHash": "b404dded35241274" + }, + "viewer.header.untitled": { + "message": "未命名", + "sourceHash": "6547a4c0c368042a" + }, + "viewer.header.zones": { + "message": "分区", + "sourceHash": "665db49a26f81425" + }, + "viewer.levels.exploded": { + "message": "分解", + "sourceHash": "be12f8788f1f0a05" + }, + "viewer.levels.label": { + "message": "楼层:{mode}", + "sourceHash": "4bf4a6861d7d633e" + }, + "viewer.levels.manual": { + "message": "手动", + "sourceHash": "387082cd5f118bed" + }, + "viewer.levels.solo": { + "message": "单层", + "sourceHash": "e9f59b513f2f45ac" + }, + "viewer.levels.stacked": { + "message": "叠层", + "sourceHash": "c203c84eee18c0f6" + }, + "viewer.stage.layout": { + "message": "视图布局", + "sourceHash": "9dd21ca47cffa3b8" + }, + "viewer.stage.split": { + "message": "分屏", + "sourceHash": "e4cd64b1680ac0d2" + }, + "viewer.theme.blueprint": { + "message": "蓝图", + "sourceHash": "f14b89d713d2528a" + }, + "viewer.theme.mediterranean": { + "message": "地中海", + "sourceHash": "142bcb2841f423c5" + }, + "viewer.theme.night": { + "message": "夜景", + "sourceHash": "b399942ec5d7e1ba" + }, + "viewer.theme.overcast": { + "message": "阴天", + "sourceHash": "f512d1b947b8ae5a" + }, + "viewer.theme.paper": { + "message": "纸面", + "sourceHash": "583542b3bc837192" + }, + "viewer.theme.studio": { + "message": "影棚", + "sourceHash": "7abc2dc6e39b1747" + }, + "viewer.theme.sunset": { + "message": "日落", + "sourceHash": "efe94d3ec8d8c44e" + }, + "viewer.theme.title": { + "message": "场景主题", + "sourceHash": "82720864e5034398" + }, + "viewer.theme.twilight": { + "message": "暮色", + "sourceHash": "4c10ea0aa0e2ea41" + }, + "viewer.theme.verdant": { + "message": "绿意", + "sourceHash": "f9592fb0672740c0" + }, + "viewer.visibility.guides": { + "message": "参考图", + "sourceHash": "892d5dd34372028b" + }, + "viewer.visibility.scans": { + "message": "扫描模型", + "sourceHash": "3260bfa69cca50e5" + }, + "viewer.visibility.title": { + "message": "可见性", + "sourceHash": "2cefbb554eadbb6e" + }, + "viewer.walkthrough.label": { + "message": "漫游:{state}", + "sourceHash": "31afd9dee47ba943" + }, + "viewer.walls.cutaway": { + "message": "剖切墙", + "sourceHash": "4da8b3678ed77b45" + }, + "viewer.walls.fullHeight": { + "message": "全高墙", + "sourceHash": "131bac2467f4fdf6" + }, + "viewer.walls.label": { + "message": "墙体:{mode}", + "sourceHash": "32f3d058cf2526d2" + }, + "viewer.walls.low": { + "message": "矮墙", + "sourceHash": "803532407fb5962b" + } + } +} diff --git a/packages/editor/src/i18n/locales.test.ts b/packages/editor/src/i18n/locales.test.ts new file mode 100644 index 0000000000..0588e11bef --- /dev/null +++ b/packages/editor/src/i18n/locales.test.ts @@ -0,0 +1,22 @@ +import { afterEach, expect, test } from 'bun:test' +import { setLocale, translate } from '@pascal-app/core' +import { registerBundledLocales } from './locales' + +afterEach(() => setLocale('en')) + +test('bundled locale packs cover the converted viewer and editor surfaces', () => { + registerBundledLocales() + + const expectations = { + 'zh-CN': ['显示设置', '房间'], + fr: ['Paramètres d’affichage', 'Pièces'], + es: ['Ajustes de visualización', 'Estancias'], + ja: ['表示設定', '部屋'], + } as const + + for (const [locale, [display, rooms]] of Object.entries(expectations)) { + setLocale(locale) + expect(translate('viewer.display.title', 'Display settings')).toBe(display) + expect(translate('editor.buildPanel.rooms.title', 'Rooms')).toBe(rooms) + } +}) diff --git a/packages/editor/src/i18n/locales.ts b/packages/editor/src/i18n/locales.ts index 31f928467c..b2ca56e536 100644 --- a/packages/editor/src/i18n/locales.ts +++ b/packages/editor/src/i18n/locales.ts @@ -1,397 +1,25 @@ -import { registerMessages, type TranslationMessages } from '@pascal-app/core' +import { + registerMessages, + type TranslationMessage, + type TranslationMessages, +} from '@pascal-app/core' +import es from './catalogs/es.json' +import fr from './catalogs/fr.json' +import ja from './catalogs/ja.json' +import zhCN from './catalogs/zh-CN.json' export type BundledLocale = 'zh-CN' | 'fr' | 'es' | 'ja' -type LocalePack = { - core: TranslationMessages - editor: TranslationMessages - viewer: TranslationMessages +type LocaleCatalog = { + locale: BundledLocale + messages: Record } -export const bundledLocalePacks: Record = { - 'zh-CN': { - core: { - 'level.groundFloor': '首层', - 'level.basement': '地下层', - 'level.basementNumber': '地下 {number} 层', - 'level.floorNumber': '{number} 层', - }, - editor: { - 'buildPanel.advanced': '高级', - 'buildPanel.rooms.hint': '墙体 · 楼板 · 吊顶', - 'buildPanel.rooms.title': '房间', - 'buildPanel.rooms.rectangle.label': '矩形', - 'buildPanel.rooms.rectangle.title': '矩形房间', - 'buildPanel.rooms.rectangle.lead': '点击两个对角点', - 'buildPanel.rooms.rectangle.rest': '在网格上。墙体、楼板和吊顶将一并创建。', - 'buildPanel.rooms.polygon.label': '多边形', - 'buildPanel.rooms.polygon.title': '多边形房间', - 'buildPanel.rooms.polygon.lead': '依次点击各个角点,', - 'buildPanel.rooms.polygon.rest': '然后再次点击起点闭合。适用于 L 形和斜角房间。', - 'buildPanel.rooms.walls.label': '墙体', - 'buildPanel.rooms.walls.title': '墙体', - 'buildPanel.rooms.walls.lead': '逐面绘制墙体。', - 'buildPanel.rooms.walls.rest': '闭合轮廓后会形成房间;保持开口可绘制隔墙。', - 'buildPanel.rooms.idle': '选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和吊顶。', - }, - viewer: { - 'common.on': '开启', - 'common.off': '关闭', - 'levels.stacked': '叠层', - 'levels.exploded': '分解', - 'levels.solo': '单层', - 'levels.manual': '手动', - 'levels.label': '楼层:{mode}', - 'walls.fullHeight': '全高墙', - 'walls.cutaway': '剖切墙', - 'walls.low': '矮墙', - 'walls.label': '墙体:{mode}', - 'theme.title': '场景主题', - 'theme.studio': '影棚', - 'theme.paper': '纸面', - 'theme.sunset': '日落', - 'theme.overcast': '阴天', - 'theme.blueprint': '蓝图', - 'theme.mediterranean': '地中海', - 'theme.twilight': '暮色', - 'theme.night': '夜景', - 'theme.verdant': '绿意', - 'visibility.title': '可见性', - 'visibility.scans': '扫描模型', - 'visibility.guides': '参考图', - 'display.title': '显示设置', - 'display.shadows': '阴影', - 'display.camera': '相机', - 'display.perspective': '透视', - 'display.orthographic': '正交', - 'display.colors': '颜色', - 'display.colored': '彩色', - 'display.monochrome': '单色', - 'display.render.title': '渲染', - 'display.render.solid': '实体', - 'display.render.solidDetail': '平整快速,不启用环境光遮蔽', - 'display.render.rendered': '渲染效果', - 'display.render.renderedDetail': '完整环境光遮蔽', - 'display.edges.title': '边线', - 'display.edges.off': '关闭', - 'display.edges.offDetail': '不显示边线', - 'display.edges.soft': '柔和', - 'display.edges.softDetail': '淡化显示主要折线', - 'display.edges.strong': '清晰', - 'display.edges.strongDetail': '清晰、不透明的边线', - 'walkthrough.label': '漫游:{state}', - 'camera.orbitLeft': '向左旋转', - 'camera.orbitRight': '向右旋转', - 'camera.topView': '俯视图', - 'floorplan.alignNorth': '视图对齐北向', - 'floorplan.empty': '此场景没有可用的平面图。', - 'floorplan.viewLabel': '{floor}二维视图', - 'floorplan.floorPlan': '平面图', - 'floorplan.floor': '楼层', - 'floorplan.zoomOut': '缩小', - 'floorplan.fit': '适合窗口', - 'floorplan.zoomIn': '放大', - 'header.units': '单元', - 'header.rooms': '房间', - 'header.zones': '分区', - 'header.back': '返回', - 'header.untitled': '未命名', - 'header.selection': '当前选择', - 'header.levels': '楼层', - 'stage.layout': '视图布局', - 'stage.split': '分屏', - }, - }, - fr: { - core: { - 'level.groundFloor': 'Rez-de-chaussée', - 'level.basement': 'Sous-sol', - 'level.basementNumber': 'Sous-sol {number}', - 'level.floorNumber': 'Étage {number}', - }, - editor: { - 'buildPanel.advanced': 'Avancé', - 'buildPanel.rooms.hint': 'murs · sol · plafond', - 'buildPanel.rooms.title': 'Pièces', - 'buildPanel.rooms.rectangle.label': 'Rectangle', - 'buildPanel.rooms.rectangle.title': 'Pièce rectangulaire', - 'buildPanel.rooms.rectangle.lead': 'Cliquez sur deux angles', - 'buildPanel.rooms.rectangle.rest': - 'de la grille. Les murs, le sol et le plafond sont créés ensemble.', - 'buildPanel.rooms.polygon.label': 'Polygone', - 'buildPanel.rooms.polygon.title': 'Pièce polygonale', - 'buildPanel.rooms.polygon.lead': 'Cliquez sur chaque angle,', - 'buildPanel.rooms.polygon.rest': - 'puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.', - 'buildPanel.rooms.walls.label': 'Murs', - 'buildPanel.rooms.walls.title': 'Murs', - 'buildPanel.rooms.walls.lead': 'Tracez un mur à la fois.', - 'buildPanel.rooms.walls.rest': - 'Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.', - 'buildPanel.rooms.idle': - 'Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.', - }, - viewer: { - 'common.on': 'Activé', - 'common.off': 'Désactivé', - 'levels.stacked': 'Superposés', - 'levels.exploded': 'Éclatés', - 'levels.solo': 'Isolé', - 'levels.manual': 'Manuel', - 'levels.label': 'Niveaux : {mode}', - 'walls.fullHeight': 'Pleine hauteur', - 'walls.cutaway': 'Vue en coupe', - 'walls.low': 'Bas', - 'walls.label': 'Murs : {mode}', - 'theme.title': 'Ambiance', - 'theme.studio': 'Studio', - 'theme.paper': 'Papier', - 'theme.sunset': 'Coucher de soleil', - 'theme.overcast': 'Ciel couvert', - 'theme.blueprint': 'Plan', - 'theme.mediterranean': 'Méditerranéen', - 'theme.twilight': 'Crépuscule', - 'theme.night': 'Nuit', - 'theme.verdant': 'Verdoyant', - 'visibility.title': 'Visibilité', - 'visibility.scans': 'Relevés 3D', - 'visibility.guides': 'Repères', - 'display.title': 'Paramètres d’affichage', - 'display.shadows': 'Ombres', - 'display.camera': 'Caméra', - 'display.perspective': 'Perspective', - 'display.orthographic': 'Orthographique', - 'display.colors': 'Couleurs', - 'display.colored': 'En couleur', - 'display.monochrome': 'Monochrome', - 'display.render.title': 'Rendu', - 'display.render.solid': 'Solide', - 'display.render.solidDetail': 'Rapide et uniforme, sans occlusion ambiante', - 'display.render.rendered': 'Réaliste', - 'display.render.renderedDetail': 'Occlusion ambiante complète', - 'display.edges.title': 'Arêtes', - 'display.edges.off': 'Désactivées', - 'display.edges.offDetail': 'Aucune ligne d’arête', - 'display.edges.soft': 'Douces', - 'display.edges.softDetail': 'Contour léger des plis principaux', - 'display.edges.strong': 'Marquées', - 'display.edges.strongDetail': 'Lignes d’arête nettes et opaques', - 'walkthrough.label': 'Visite : {state}', - 'camera.orbitLeft': 'Pivoter à gauche', - 'camera.orbitRight': 'Pivoter à droite', - 'camera.topView': 'Vue de dessus', - 'floorplan.alignNorth': 'Orienter la vue vers le nord', - 'floorplan.empty': 'Aucun plan d’étage n’est disponible pour cette scène.', - 'floorplan.viewLabel': 'Vue 2D — {floor}', - 'floorplan.floorPlan': 'Plan d’étage', - 'floorplan.floor': 'Niveau', - 'floorplan.zoomOut': 'Zoom arrière', - 'floorplan.fit': 'Ajuster le plan', - 'floorplan.zoomIn': 'Zoom avant', - 'header.units': 'Unités', - 'header.rooms': 'Pièces', - 'header.zones': 'Zones', - 'header.back': 'Retour', - 'header.untitled': 'Sans titre', - 'header.selection': 'Sélection', - 'header.levels': 'Niveaux', - 'stage.layout': 'Disposition des vues', - 'stage.split': 'Partagé', - }, - }, - es: { - core: { - 'level.groundFloor': 'Planta baja', - 'level.basement': 'Sótano', - 'level.basementNumber': 'Sótano {number}', - 'level.floorNumber': 'Planta {number}', - }, - editor: { - 'buildPanel.advanced': 'Avanzado', - 'buildPanel.rooms.hint': 'muros · suelo · techo', - 'buildPanel.rooms.title': 'Estancias', - 'buildPanel.rooms.rectangle.label': 'Rectángulo', - 'buildPanel.rooms.rectangle.title': 'Estancia rectangular', - 'buildPanel.rooms.rectangle.lead': 'Haz clic en dos esquinas', - 'buildPanel.rooms.rectangle.rest': - 'de la cuadrícula. Los muros, el suelo y el techo se crean a la vez.', - 'buildPanel.rooms.polygon.label': 'Polígono', - 'buildPanel.rooms.polygon.title': 'Estancia poligonal', - 'buildPanel.rooms.polygon.lead': 'Haz clic en cada esquina', - 'buildPanel.rooms.polygon.rest': - 'y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.', - 'buildPanel.rooms.walls.label': 'Muros', - 'buildPanel.rooms.walls.title': 'Muros', - 'buildPanel.rooms.walls.lead': 'Dibuja un muro cada vez.', - 'buildPanel.rooms.walls.rest': - 'Cierra el contorno para crear una estancia o déjalo abierto para tabiques.', - 'buildPanel.rooms.idle': - 'Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.', - }, - viewer: { - 'common.on': 'Activado', - 'common.off': 'Desactivado', - 'levels.stacked': 'Apiladas', - 'levels.exploded': 'Separadas', - 'levels.solo': 'Aislada', - 'levels.manual': 'Manual', - 'levels.label': 'Plantas: {mode}', - 'walls.fullHeight': 'Altura completa', - 'walls.cutaway': 'Vista seccionada', - 'walls.low': 'Bajos', - 'walls.label': 'Muros: {mode}', - 'theme.title': 'Ambiente', - 'theme.studio': 'Estudio', - 'theme.paper': 'Papel', - 'theme.sunset': 'Atardecer', - 'theme.overcast': 'Nublado', - 'theme.blueprint': 'Plano técnico', - 'theme.mediterranean': 'Mediterráneo', - 'theme.twilight': 'Crepúsculo', - 'theme.night': 'Noche', - 'theme.verdant': 'Verde', - 'visibility.title': 'Visibilidad', - 'visibility.scans': 'Escaneos 3D', - 'visibility.guides': 'Guías', - 'display.title': 'Ajustes de visualización', - 'display.shadows': 'Sombras', - 'display.camera': 'Cámara', - 'display.perspective': 'Perspectiva', - 'display.orthographic': 'Ortográfica', - 'display.colors': 'Colores', - 'display.colored': 'A color', - 'display.monochrome': 'Monocromo', - 'display.render.title': 'Renderizado', - 'display.render.solid': 'Sólido', - 'display.render.solidDetail': 'Plano y rápido, sin oclusión ambiental', - 'display.render.rendered': 'Realista', - 'display.render.renderedDetail': 'Oclusión ambiental completa', - 'display.edges.title': 'Aristas', - 'display.edges.off': 'Desactivadas', - 'display.edges.offDetail': 'Sin líneas de arista', - 'display.edges.soft': 'Suaves', - 'display.edges.softDetail': 'Contorno tenue de los pliegues principales', - 'display.edges.strong': 'Marcadas', - 'display.edges.strongDetail': 'Líneas de arista nítidas y opacas', - 'walkthrough.label': 'Recorrido: {state}', - 'camera.orbitLeft': 'Orbitar a la izquierda', - 'camera.orbitRight': 'Orbitar a la derecha', - 'camera.topView': 'Vista superior', - 'floorplan.alignNorth': 'Orientar la vista al norte', - 'floorplan.empty': 'No hay planos de planta disponibles para esta escena.', - 'floorplan.viewLabel': 'Vista 2D de {floor}', - 'floorplan.floorPlan': 'Plano de planta', - 'floorplan.floor': 'Planta', - 'floorplan.zoomOut': 'Alejar', - 'floorplan.fit': 'Ajustar plano', - 'floorplan.zoomIn': 'Acercar', - 'header.units': 'Unidades', - 'header.rooms': 'Estancias', - 'header.zones': 'Zonas', - 'header.back': 'Volver', - 'header.untitled': 'Sin título', - 'header.selection': 'Selección', - 'header.levels': 'Plantas', - 'stage.layout': 'Disposición de vistas', - 'stage.split': 'Dividida', - }, - }, - ja: { - core: { - 'level.groundFloor': '1階', - 'level.basement': '地下1階', - 'level.basementNumber': '地下{number}階', - 'level.floorNumber': '{number}階', - }, - editor: { - 'buildPanel.advanced': '詳細', - 'buildPanel.rooms.hint': '壁 · 床 · 天井', - 'buildPanel.rooms.title': '部屋', - 'buildPanel.rooms.rectangle.label': '長方形', - 'buildPanel.rooms.rectangle.title': '長方形の部屋', - 'buildPanel.rooms.rectangle.lead': '対角の2点をクリック', - 'buildPanel.rooms.rectangle.rest': - 'グリッド上で指定します。壁・床・天井をまとめて作成します。', - 'buildPanel.rooms.polygon.label': '多角形', - 'buildPanel.rooms.polygon.title': '多角形の部屋', - 'buildPanel.rooms.polygon.lead': '各頂点をクリックし、', - 'buildPanel.rooms.polygon.rest': - '最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。', - 'buildPanel.rooms.walls.label': '壁', - 'buildPanel.rooms.walls.title': '壁', - 'buildPanel.rooms.walls.lead': '壁を1面ずつ描画します。', - 'buildPanel.rooms.walls.rest': - '輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。', - 'buildPanel.rooms.idle': '形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。', - }, - viewer: { - 'common.on': 'オン', - 'common.off': 'オフ', - 'levels.stacked': '積層', - 'levels.exploded': '分解', - 'levels.solo': '単独', - 'levels.manual': '手動', - 'levels.label': '階層:{mode}', - 'walls.fullHeight': '全高', - 'walls.cutaway': 'カット表示', - 'walls.low': '低く表示', - 'walls.label': '壁:{mode}', - 'theme.title': 'シーンテーマ', - 'theme.studio': 'スタジオ', - 'theme.paper': 'ペーパー', - 'theme.sunset': '夕景', - 'theme.overcast': '曇天', - 'theme.blueprint': '青図', - 'theme.mediterranean': '地中海', - 'theme.twilight': '薄暮', - 'theme.night': '夜景', - 'theme.verdant': '緑景', - 'visibility.title': '表示', - 'visibility.scans': '3Dスキャン', - 'visibility.guides': 'ガイド', - 'display.title': '表示設定', - 'display.shadows': '影', - 'display.camera': 'カメラ', - 'display.perspective': '透視投影', - 'display.orthographic': '平行投影', - 'display.colors': 'カラー', - 'display.colored': 'カラー表示', - 'display.monochrome': 'モノクロ', - 'display.render.title': 'レンダリング', - 'display.render.solid': 'ソリッド', - 'display.render.solidDetail': '軽量表示(アンビエントオクルージョンなし)', - 'display.render.rendered': 'レンダリング', - 'display.render.renderedDetail': 'アンビエントオクルージョンを完全表示', - 'display.edges.title': 'エッジ', - 'display.edges.off': 'オフ', - 'display.edges.offDetail': 'エッジ線を表示しない', - 'display.edges.soft': 'ソフト', - 'display.edges.softDetail': '主要な稜線を薄く表示', - 'display.edges.strong': '強調', - 'display.edges.strongDetail': 'エッジ線をくっきり表示', - 'walkthrough.label': 'ウォークスルー:{state}', - 'camera.orbitLeft': '左に回転', - 'camera.orbitRight': '右に回転', - 'camera.topView': '上面図', - 'floorplan.alignNorth': '北を上に表示', - 'floorplan.empty': 'このシーンには表示できる平面図がありません。', - 'floorplan.viewLabel': '{floor}の2Dビュー', - 'floorplan.floorPlan': '平面図', - 'floorplan.floor': '階', - 'floorplan.zoomOut': '縮小', - 'floorplan.fit': '全体を表示', - 'floorplan.zoomIn': '拡大', - 'header.units': '住戸', - 'header.rooms': '部屋', - 'header.zones': 'ゾーン', - 'header.back': '戻る', - 'header.untitled': '名称未設定', - 'header.selection': '選択', - 'header.levels': '階層', - 'stage.layout': 'ビュー配置', - 'stage.split': '分割', - }, - }, +export const bundledLocalePacks: Record = { + 'zh-CN': zhCN as LocaleCatalog, + fr: fr as LocaleCatalog, + es: es as LocaleCatalog, + ja: ja as LocaleCatalog, } let registered = false @@ -399,9 +27,20 @@ let registered = false export function registerBundledLocales(): void { if (registered) return registered = true - for (const [locale, namespaces] of Object.entries(bundledLocalePacks)) { - registerMessages(locale, 'core', namespaces.core) - registerMessages(locale, 'editor', namespaces.editor) - registerMessages(locale, 'viewer', namespaces.viewer) + + for (const catalog of Object.values(bundledLocalePacks)) { + const namespaces = new Map() + for (const [fullKey, entry] of Object.entries(catalog.messages)) { + const separator = fullKey.indexOf('.') + if (separator < 1) continue + const namespace = fullKey.slice(0, separator) + const key = fullKey.slice(separator + 1) + const messages = namespaces.get(namespace) ?? {} + messages[key] = entry.message + namespaces.set(namespace, messages) + } + for (const [namespace, messages] of namespaces) { + registerMessages(catalog.locale, namespace, messages) + } } } diff --git a/packages/editor/src/i18n/source-catalog.json b/packages/editor/src/i18n/source-catalog.json new file mode 100644 index 0000000000..8e706d8f94 --- /dev/null +++ b/packages/editor/src/i18n/source-catalog.json @@ -0,0 +1,469 @@ +{ + "core.level.basement": { + "default": "Basement", + "hash": "8caa959c0ae800c3", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:158", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:57" + ] + }, + "core.level.basementNumber": { + "default": "Basement {number}", + "hash": "14927085ee91ba05", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:160", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:59" + ] + }, + "core.level.floorNumber": { + "default": "Floor {number}", + "hash": "e59074a7b9b3f56e", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:162", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:61" + ] + }, + "core.level.groundFloor": { + "default": "Ground floor", + "hash": "cde3efc9b7ddfa41", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:157", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:56" + ] + }, + "editor.buildPanel.advanced": { + "default": "Advanced", + "hash": "f571cb2d06ae01ae", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:99"] + }, + "editor.buildPanel.rooms.hint": { + "default": "walls · floor · ceiling", + "hash": "fb40866c2d6db5fb", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:225"] + }, + "editor.buildPanel.rooms.idle": { + "default": "Pick a shape to start. Every room brings its own walls, floor and ceiling.", + "hash": "271039bd919dc4c1", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:252"] + }, + "editor.buildPanel.rooms.polygon.label": { + "default": "Polygon", + "hash": "4e186fc057733162", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:286"] + }, + "editor.buildPanel.rooms.polygon.lead": { + "default": "Click each corner,", + "hash": "345e3aca3879b5c5", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:288"] + }, + "editor.buildPanel.rooms.polygon.rest": { + "default": "then the first one again to close. For L-shapes and angled rooms.", + "hash": "4b869bde0632749b", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:289"] + }, + "editor.buildPanel.rooms.polygon.title": { + "default": "Polygon room", + "hash": "8ec6152e139b0e3f", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:287"] + }, + "editor.buildPanel.rooms.rectangle.label": { + "default": "Rectangle", + "hash": "2c2c20272b0370cc", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:265"] + }, + "editor.buildPanel.rooms.rectangle.lead": { + "default": "Click two corners", + "hash": "060faa48e299d7c9", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:267"] + }, + "editor.buildPanel.rooms.rectangle.rest": { + "default": "on the grid. Walls, floor and ceiling are created together.", + "hash": "5c868318eb6e0952", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:268"] + }, + "editor.buildPanel.rooms.rectangle.title": { + "default": "Rectangle room", + "hash": "83a45603ac7f41e8", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:266"] + }, + "editor.buildPanel.rooms.title": { + "default": "Rooms", + "hash": "3e7408ed44676176", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:227"] + }, + "editor.buildPanel.rooms.walls.label": { + "default": "Walls", + "hash": "aac9a732d2159ccc", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:276"] + }, + "editor.buildPanel.rooms.walls.lead": { + "default": "Draw one wall at a time.", + "hash": "9843edd68426fa2d", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:278"] + }, + "editor.buildPanel.rooms.walls.rest": { + "default": "Close a loop and it becomes a room; leave it open for partitions.", + "hash": "e3c4f3c4a5a9d8a6", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:279"] + }, + "editor.buildPanel.rooms.walls.title": { + "default": "Walls", + "hash": "aac9a732d2159ccc", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:277"] + }, + "viewer.camera.orbitLeft": { + "default": "Orbit left", + "hash": "8bf25afa3cb8baea", + "context": [ + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:482", + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:490" + ] + }, + "viewer.camera.orbitRight": { + "default": "Orbit right", + "hash": "b9cc64c1d1bb0d46", + "context": [ + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:498", + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:506" + ] + }, + "viewer.camera.topView": { + "default": "Top view", + "hash": "1ab1eb3a14f4b0e0", + "context": [ + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:514", + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:522" + ] + }, + "viewer.common.off": { + "default": "Off", + "hash": "50accd740fa2e736", + "context": [ + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:212", + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:463" + ] + }, + "viewer.common.on": { + "default": "On", + "hash": "9cc01faad79c28da", + "context": [ + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:212", + "packages/editor/src/components/viewer/viewer-controls-bar.tsx:462" + ] + }, + "viewer.display.camera": { + "default": "Camera", + "hash": "14622745dbf93f4c", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:225"] + }, + "viewer.display.colored": { + "default": "Colored", + "hash": "d3d478fa6c6d7ddc", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:239"] + }, + "viewer.display.colors": { + "default": "Colors", + "hash": "82b74c4f5ef43336", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:236"] + }, + "viewer.display.edges.off": { + "default": "Off", + "hash": "50accd740fa2e736", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:174"] + }, + "viewer.display.edges.offDetail": { + "default": "No edge lines", + "hash": "4ff58913a4492199", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:175"] + }, + "viewer.display.edges.soft": { + "default": "Soft", + "hash": "d266d83ec490df4e", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:179"] + }, + "viewer.display.edges.softDetail": { + "default": "Faint outline of major creases", + "hash": "0c583f1d4201293b", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:180"] + }, + "viewer.display.edges.strong": { + "default": "Strong", + "hash": "f82927a01d4c5e45", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:184"] + }, + "viewer.display.edges.strongDetail": { + "default": "Crisp, opaque edge lines", + "hash": "2416a48df28d6205", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:185"] + }, + "viewer.display.edges.title": { + "default": "Edges", + "hash": "027bbc8b7287c06a", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:312"] + }, + "viewer.display.monochrome": { + "default": "Monochrome", + "hash": "ecde2f7c26914be3", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:240"] + }, + "viewer.display.orthographic": { + "default": "Orthographic", + "hash": "ed59dd8ce7797a66", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:229"] + }, + "viewer.display.perspective": { + "default": "Perspective", + "hash": "316e25993ff72b88", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:228"] + }, + "viewer.display.render.rendered": { + "default": "Rendered", + "hash": "f363702a10aedaeb", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:166"] + }, + "viewer.display.render.renderedDetail": { + "default": "Full ambient occlusion", + "hash": "8bb9e54fc786ca77", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:167"] + }, + "viewer.display.render.solid": { + "default": "Solid", + "hash": "ef3e765e692faf4c", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:160"] + }, + "viewer.display.render.solidDetail": { + "default": "Flat and fast — no ambient occlusion", + "hash": "8827df1cdeb02ed9", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:161"] + }, + "viewer.display.render.title": { + "default": "Render", + "hash": "5e58af99d6d4fa6a", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:249"] + }, + "viewer.display.shadows": { + "default": "Shadows", + "hash": "4c6bcb30b40047fc", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:210"] + }, + "viewer.display.title": { + "default": "Display settings", + "hash": "d0db46f206a88e2e", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:196"] + }, + "viewer.floorplan.alignNorth": { + "default": "Align view to north", + "hash": "2bdf83801d4a40af", + "context": ["packages/editor/src/components/viewer/floorplan-compass-button.tsx:23"] + }, + "viewer.floorplan.empty": { + "default": "No floor plans are available for this scene.", + "hash": "286a757d8b52bb02", + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:886"] + }, + "viewer.floorplan.fit": { + "default": "Fit floor plan", + "hash": "bf63c679ab94dbcd", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:1043", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1046" + ] + }, + "viewer.floorplan.floor": { + "default": "Floor", + "hash": "d7148fa065bc709b", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:990", + "packages/editor/src/components/viewer/floorplan-preview.tsx:992" + ] + }, + "viewer.floorplan.floorPlan": { + "default": "Floor plan", + "hash": "fee6203bb9046929", + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:902"] + }, + "viewer.floorplan.viewLabel": { + "default": "{floor} 2D view", + "hash": "28b255b7bf5f1084", + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:899"] + }, + "viewer.floorplan.zoomIn": { + "default": "Zoom in", + "hash": "d17804412f8dd081", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:1052", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1055" + ] + }, + "viewer.floorplan.zoomOut": { + "default": "Zoom out", + "hash": "23b012a283b2c14a", + "context": [ + "packages/editor/src/components/viewer/floorplan-preview.tsx:1034", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1037" + ] + }, + "viewer.header.back": { + "default": "Back", + "hash": "7ceade8f25b58fe9", + "context": [ + "packages/editor/src/components/viewer/viewer-scene-header.tsx:137", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:146" + ] + }, + "viewer.header.levels": { + "default": "Levels", + "hash": "cc3d601d59e2e26c", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:210"] + }, + "viewer.header.rooms": { + "default": "Rooms", + "hash": "3e7408ed44676176", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:48"] + }, + "viewer.header.selection": { + "default": "Selection", + "hash": "170b2049ef0f9131", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:175"] + }, + "viewer.header.units": { + "default": "Units", + "hash": "b404dded35241274", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:47"] + }, + "viewer.header.untitled": { + "default": "Untitled", + "hash": "6547a4c0c368042a", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:156"] + }, + "viewer.header.zones": { + "default": "Zones", + "hash": "665db49a26f81425", + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:49"] + }, + "viewer.levels.exploded": { + "default": "Exploded", + "hash": "be12f8788f1f0a05", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:58"] + }, + "viewer.levels.label": { + "default": "Levels: {mode}", + "hash": "4bf4a6861d7d633e", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:397"] + }, + "viewer.levels.manual": { + "default": "Manual", + "hash": "387082cd5f118bed", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:60"] + }, + "viewer.levels.solo": { + "default": "Solo", + "hash": "e9f59b513f2f45ac", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:59"] + }, + "viewer.levels.stacked": { + "default": "Stacked", + "hash": "c203c84eee18c0f6", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:57"] + }, + "viewer.stage.layout": { + "default": "Viewer layout", + "hash": "9dd21ca47cffa3b8", + "context": ["packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31"] + }, + "viewer.stage.split": { + "default": "Split", + "hash": "e4cd64b1680ac0d2", + "context": ["packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59"] + }, + "viewer.theme.blueprint": { + "default": "Blueprint", + "hash": "f14b89d713d2528a", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:74"] + }, + "viewer.theme.mediterranean": { + "default": "Mediterranean", + "hash": "142bcb2841f423c5", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:75"] + }, + "viewer.theme.night": { + "default": "Night", + "hash": "b399942ec5d7e1ba", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:77"] + }, + "viewer.theme.overcast": { + "default": "Overcast", + "hash": "f512d1b947b8ae5a", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:73"] + }, + "viewer.theme.paper": { + "default": "Paper", + "hash": "583542b3bc837192", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:71"] + }, + "viewer.theme.studio": { + "default": "Studio", + "hash": "7abc2dc6e39b1747", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:70"] + }, + "viewer.theme.sunset": { + "default": "Sunset", + "hash": "efe94d3ec8d8c44e", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:72"] + }, + "viewer.theme.title": { + "default": "Scene theme", + "hash": "82720864e5034398", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:277"] + }, + "viewer.theme.twilight": { + "default": "Twilight", + "hash": "4c10ea0aa0e2ea41", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:76"] + }, + "viewer.theme.verdant": { + "default": "Verdant", + "hash": "f9592fb0672740c0", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:78"] + }, + "viewer.visibility.guides": { + "default": "Guides", + "hash": "892d5dd34372028b", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:134"] + }, + "viewer.visibility.scans": { + "default": "Scans", + "hash": "3260bfa69cca50e5", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:121"] + }, + "viewer.visibility.title": { + "default": "Visibility", + "hash": "2cefbb554eadbb6e", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:106"] + }, + "viewer.walkthrough.label": { + "default": "Walkthrough: {state}", + "hash": "31afd9dee47ba943", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:460"] + }, + "viewer.walls.cutaway": { + "default": "Cutaway", + "hash": "4da8b3678ed77b45", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:66"] + }, + "viewer.walls.fullHeight": { + "default": "Full height", + "hash": "131bac2467f4fdf6", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:64"] + }, + "viewer.walls.label": { + "default": "Walls: {mode}", + "hash": "32f3d058cf2526d2", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:424"] + }, + "viewer.walls.low": { + "default": "Low", + "hash": "803532407fb5962b", + "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:65"] + } +} diff --git a/scripts/i18n.test.ts b/scripts/i18n.test.ts new file mode 100644 index 0000000000..94fcda390c --- /dev/null +++ b/scripts/i18n.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from 'bun:test' +import { + type LocaleCatalog, + refreshLocaleCatalog, + type SourceCatalog, + type TranslationProvider, +} from './i18n' + +test('AI translation refreshes only missing and stale entries through a mocked provider', async () => { + const source: SourceCatalog = { + 'viewer.current': { + default: 'Current', + hash: 'hash-current', + context: ['current.tsx:1'], + }, + 'viewer.missing': { + default: 'Hello {name}', + hash: 'hash-missing', + context: ['missing.tsx:2'], + }, + 'viewer.stale': { + default: 'Changed', + hash: 'hash-new', + context: ['stale.tsx:3'], + }, + } + const catalog: LocaleCatalog = { + locale: 'fr', + messages: { + 'viewer.current': { message: 'Actuel', sourceHash: 'hash-current' }, + 'viewer.stale': { message: 'Ancien', sourceHash: 'hash-old' }, + }, + } + const requests: string[][] = [] + const provider: TranslationProvider = { + async translate(request) { + requests.push(request.items.map((item) => item.key)) + return { + 'viewer.missing': 'Bonjour {name}', + 'viewer.stale': 'Modifié', + } + }, + } + + const refreshed = await refreshLocaleCatalog(source, catalog, provider, {}) + + expect(requests).toEqual([['viewer.missing', 'viewer.stale']]) + expect(refreshed.messages['viewer.current']).toEqual(catalog.messages['viewer.current']) + expect(refreshed.messages['viewer.missing']).toEqual({ + message: 'Bonjour {name}', + sourceHash: 'hash-missing', + }) + expect(refreshed.messages['viewer.stale']).toEqual({ + message: 'Modifié', + sourceHash: 'hash-new', + }) +}) diff --git a/scripts/i18n.ts b/scripts/i18n.ts new file mode 100644 index 0000000000..5d4fc424b3 --- /dev/null +++ b/scripts/i18n.ts @@ -0,0 +1,463 @@ +import { createHash } from 'node:crypto' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import ts from 'typescript' + +export type Message = + | string + | { + zero?: string + one?: string + two?: string + few?: string + many?: string + other: string + } + +export type SourceEntry = { + default: Message + hash: string + context: string[] +} + +export type SourceCatalog = Record + +export type LocaleEntry = { + message: Message + sourceHash: string +} + +export type LocaleCatalog = { + locale: string + messages: Record +} + +export type TranslationRequest = { + locale: string + items: Array<{ key: string; source: Message; context: string[] }> + glossary: Record +} + +export interface TranslationProvider { + translate(request: TranslationRequest): Promise> +} + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const SOURCE_CATALOG_PATH = join(ROOT, 'packages/editor/src/i18n/source-catalog.json') +const LOCALE_CATALOG_DIR = join(ROOT, 'packages/editor/src/i18n/catalogs') +const GLOSSARY_PATH = join(ROOT, 'i18n/glossary.json') +const BUNDLED_LOCALES = ['zh-CN', 'fr', 'es', 'ja'] as const +const SOURCE_GLOBS = [ + 'packages/core/src/**/*.{ts,tsx}', + 'packages/viewer/src/**/*.{ts,tsx}', + 'packages/editor/src/**/*.{ts,tsx}', + 'packages/nodes/src/**/*.{ts,tsx}', + 'apps/editor/**/*.{ts,tsx}', +] +const CONVERTED_SURFACES = [ + 'packages/editor/src/components/viewer', + 'packages/editor/src/components/ui/build-panel/build-panel.tsx', +] +const PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'] as const + +function hashMessage(message: Message): string { + return createHash('sha256').update(JSON.stringify(message)).digest('hex').slice(0, 16) +} + +function messageFromNode(node: ts.Expression): Message | null { + if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text + if (!ts.isObjectLiteralExpression(node)) return null + + const message: Partial> = {} + for (const property of node.properties) { + if (!ts.isPropertyAssignment(property)) return null + const name = + ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) + ? property.name.text + : null + if (!(name && PLURAL_KEYS.includes(name as (typeof PLURAL_KEYS)[number]))) return null + if (!ts.isStringLiteralLike(property.initializer)) return null + message[name as (typeof PLURAL_KEYS)[number]] = property.initializer.text + } + return typeof message.other === 'string' ? (message as Message) : null +} + +function sourceFiles(): string[] { + const files = new Set() + for (const pattern of SOURCE_GLOBS) { + const glob = new Bun.Glob(pattern) + for (const path of glob.scanSync({ cwd: ROOT, absolute: true, onlyFiles: true })) { + if ( + path.includes('/node_modules/') || + /\.test\.[tj]sx?$/.test(path) || + path.includes('/__fixtures__/') + ) { + continue + } + files.add(path) + } + } + return [...files].sort() +} + +export function extractSourceCatalog(root = ROOT): SourceCatalog { + const found = new Map }>() + + for (const path of sourceFiles()) { + const sourceText = readFileSync(path, 'utf8') + const source = ts.createSourceFile( + path, + sourceText, + ts.ScriptTarget.Latest, + true, + path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ) + + function visit(node: ts.Node): void { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 't' && + node.arguments.length >= 2 + ) { + const keyNode = node.arguments[0] + const defaultNode = node.arguments[1] + if (!(keyNode && defaultNode && ts.isStringLiteralLike(keyNode))) { + const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 + throw new Error( + `${relative(root, path)}:${line}: t() keys and English defaults must be static literals`, + ) + } + const message = messageFromNode(defaultNode) + if (message === null) { + const line = source.getLineAndCharacterOfPosition(defaultNode.getStart()).line + 1 + throw new Error( + `${relative(root, path)}:${line}: unsupported English message in t("${keyNode.text}")`, + ) + } + const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 + const context = `${relative(root, path)}:${line}` + const existing = found.get(keyNode.text) + if (existing && JSON.stringify(existing.default) !== JSON.stringify(message)) { + throw new Error(`Conflicting English defaults for "${keyNode.text}"`) + } + if (existing) existing.context.add(context) + else found.set(keyNode.text, { default: message, context: new Set([context]) }) + } + ts.forEachChild(node, visit) + } + visit(source) + } + + return Object.fromEntries( + [...found.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [ + key, + { + default: entry.default, + hash: hashMessage(entry.default), + context: [...entry.context].sort(), + }, + ]), + ) +} + +function readJson(path: string): T { + return JSON.parse(readFileSync(path, 'utf8')) as T +} + +function writeJson(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) +} + +function localeCatalogPath(locale: string): string { + return join(LOCALE_CATALOG_DIR, `${locale}.json`) +} + +function messageStrings(message: Message): string[] { + return typeof message === 'string' ? [message] : Object.values(message).filter(Boolean) +} + +function placeholders(message: Message): string[] { + const names = new Set() + for (const value of messageStrings(message)) { + for (const match of value.matchAll(/\{([A-Za-z_][\w.-]*)\}/g)) names.add(match[1]!) + } + return [...names].sort() +} + +function samePlaceholders(source: Message, translated: Message): boolean { + return JSON.stringify(placeholders(source)) === JSON.stringify(placeholders(translated)) +} + +function isMessage(value: unknown): value is Message { + if (typeof value === 'string') return true + if (!(value && typeof value === 'object' && !Array.isArray(value))) return false + const record = value as Record + return ( + typeof record.other === 'string' && + Object.entries(record).every( + ([key, entry]) => + PLURAL_KEYS.includes(key as (typeof PLURAL_KEYS)[number]) && typeof entry === 'string', + ) + ) +} + +function currentSourceMatches(expected: SourceCatalog, actual: SourceCatalog): string[] { + const errors: string[] = [] + for (const [key, entry] of Object.entries(actual)) { + const stored = expected[key] + if (!stored) errors.push(`source catalog is missing "${key}"`) + else if ( + stored.hash !== entry.hash || + JSON.stringify(stored.default) !== JSON.stringify(entry.default) + ) { + errors.push(`source catalog is stale for "${key}"`) + } + } + for (const key of Object.keys(expected)) { + if (!actual[key]) errors.push(`source catalog has removed key "${key}"`) + } + return errors +} + +function bypassedLiterals(): string[] { + const warnings: string[] = [] + const paths = sourceFiles().filter((path) => + CONVERTED_SURFACES.some((surface) => { + const absolute = join(ROOT, surface) + return path === absolute || path.startsWith(`${absolute}/`) + }), + ) + + for (const path of paths) { + const sourceText = readFileSync(path, 'utf8') + const source = ts.createSourceFile( + path, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ) + function warning(node: ts.Node, text: string): void { + if (!/[A-Za-z]{2}/.test(text)) return + const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 + warnings.push(`${relative(ROOT, path)}:${line}: "${text.trim()}"`) + } + function visit(node: ts.Node): void { + if (ts.isJsxText(node)) warning(node, node.text) + if ( + ts.isJsxAttribute(node) && + ['aria-label', 'title', 'placeholder', 'label', 'alt'].includes(node.name.text) && + node.initializer && + ts.isStringLiteral(node.initializer) + ) { + warning(node, node.initializer.text) + } + ts.forEachChild(node, visit) + } + visit(source) + } + return warnings +} + +export function checkCatalogs(root = ROOT): string[] { + const errors: string[] = [] + const actual = extractSourceCatalog(root) + const stored = readJson(SOURCE_CATALOG_PATH) + errors.push(...currentSourceMatches(stored, actual)) + + for (const locale of BUNDLED_LOCALES) { + const catalog = readJson(localeCatalogPath(locale)) + for (const [key, source] of Object.entries(actual)) { + const translation = catalog.messages[key] + if (!translation) { + errors.push(`${locale}: missing "${key}"`) + continue + } + if (translation.sourceHash !== source.hash) errors.push(`${locale}: stale "${key}"`) + if (!samePlaceholders(source.default, translation.message)) { + errors.push(`${locale}: broken placeholders in "${key}"`) + } + if ( + typeof source.default === 'object' && + (typeof translation.message !== 'object' || + !Object.keys(source.default).every((category) => category in translation.message)) + ) { + errors.push(`${locale}: missing plural forms in "${key}"`) + } + } + } + return errors +} + +function promptFor(request: TranslationRequest): string { + return [ + `Translate these Pascal architecture/CAD interface messages into ${request.locale}.`, + 'Return one JSON object mapping every input key to its translated string or plural-form object.', + 'Preserve {placeholders} exactly. Keep labels concise and use a consistent professional CAD glossary.', + 'Do not add commentary or markdown. Human reviewers will approve the resulting diff.', + `Glossary: ${JSON.stringify(request.glossary)}`, + `Messages: ${JSON.stringify(request.items)}`, + ].join('\n\n') +} + +function parseProviderResponse(text: string, request: TranslationRequest): Record { + const start = text.indexOf('{') + const end = text.lastIndexOf('}') + if (start < 0 || end < start) throw new Error('Translation provider did not return JSON') + const parsed = JSON.parse(text.slice(start, end + 1)) as Record + const messages: Record = {} + for (const item of request.items) { + const message = parsed[item.key] + if (!isMessage(message)) throw new Error(`Translation provider omitted "${item.key}"`) + if (!samePlaceholders(item.source, message)) { + throw new Error(`Translation provider changed placeholders in "${item.key}"`) + } + messages[item.key] = message + } + return messages +} + +function configuredProvider(): TranslationProvider { + const provider = + process.env.I18N_PROVIDER ?? + (process.env.OPENAI_API_KEY ? 'openai' : process.env.ANTHROPIC_API_KEY ? 'anthropic' : '') + const model = process.env.I18N_MODEL + if (!model) throw new Error('Set I18N_MODEL to the translation model') + + if (provider === 'openai') { + const key = process.env.OPENAI_API_KEY + if (!key) throw new Error('Set OPENAI_API_KEY for the OpenAI translation provider') + return { + async translate(request) { + const response = await fetch( + `${process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, + { + method: 'POST', + headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ + model, + response_format: { type: 'json_object' }, + messages: [{ role: 'user', content: promptFor(request) }], + }), + }, + ) + if (!response.ok) throw new Error(`OpenAI translation failed (${response.status})`) + const body = (await response.json()) as { + choices?: Array<{ message?: { content?: string } }> + } + return parseProviderResponse(body.choices?.[0]?.message?.content ?? '', request) + }, + } + } + + if (provider === 'anthropic') { + const key = process.env.ANTHROPIC_API_KEY + if (!key) throw new Error('Set ANTHROPIC_API_KEY for the Anthropic translation provider') + return { + async translate(request) { + const response = await fetch( + `${process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'}/v1/messages`, + { + method: 'POST', + headers: { + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + 'x-api-key': key, + }, + body: JSON.stringify({ + model, + max_tokens: 8192, + messages: [{ role: 'user', content: promptFor(request) }], + }), + }, + ) + if (!response.ok) throw new Error(`Anthropic translation failed (${response.status})`) + const body = (await response.json()) as { + content?: Array<{ type?: string; text?: string }> + } + const text = body.content?.find((block) => block.type === 'text')?.text ?? '' + return parseProviderResponse(text, request) + }, + } + } + + throw new Error('Set I18N_PROVIDER to "openai" or "anthropic" and provide its API key') +} + +export async function refreshLocaleCatalog( + source: SourceCatalog, + catalog: LocaleCatalog, + provider: TranslationProvider, + glossary: Record, +): Promise { + const stale = Object.entries(source) + .filter(([key, entry]) => catalog.messages[key]?.sourceHash !== entry.hash) + .map(([key, entry]) => ({ key, source: entry.default, context: entry.context })) + if (stale.length === 0) return catalog + + const messages = { ...catalog.messages } + for (let offset = 0; offset < stale.length; offset += 40) { + const items = stale.slice(offset, offset + 40) + const translated = await provider.translate({ locale: catalog.locale, items, glossary }) + for (const item of items) { + const message = translated[item.key] + if (!message) throw new Error(`Translation provider omitted "${item.key}"`) + messages[item.key] = { message, sourceHash: source[item.key]!.hash } + } + } + return { ...catalog, messages: Object.fromEntries(Object.entries(messages).sort()) } +} + +async function main(): Promise { + const command = process.argv[2] + if (command === 'extract') { + const catalog = extractSourceCatalog() + writeJson(SOURCE_CATALOG_PATH, catalog) + console.log( + `Extracted ${Object.keys(catalog).length} messages to ${relative(ROOT, SOURCE_CATALOG_PATH)}`, + ) + return + } + + if (command === 'check') { + const errors = checkCatalogs() + for (const literal of bypassedLiterals()) { + console.warn(`[i18n warning] user-facing literal bypasses t(): ${literal}`) + } + if (errors.length > 0) { + console.error(errors.map((error) => `[i18n] ${error}`).join('\n')) + process.exitCode = 1 + } else { + console.log(`Checked ${BUNDLED_LOCALES.length} bundled locale catalogs`) + } + return + } + + if (command === 'translate') { + const requested = process.argv.slice(3) + const locales = requested.length > 0 ? requested : [...BUNDLED_LOCALES] + const source = readJson(SOURCE_CATALOG_PATH) + const glossary = readJson>(GLOSSARY_PATH) + const provider = configuredProvider() + for (const locale of locales) { + if (!BUNDLED_LOCALES.includes(locale as (typeof BUNDLED_LOCALES)[number])) { + throw new Error(`Unknown bundled locale "${locale}"`) + } + const path = localeCatalogPath(locale) + const catalog = readJson(path) + const refreshed = await refreshLocaleCatalog(source, catalog, provider, glossary) + writeJson(path, refreshed) + console.log(`Updated ${relative(ROOT, path)}`) + } + return + } + + throw new Error('Usage: bun scripts/i18n.ts [locale...]') +} + +if (import.meta.main) await main() From 4de32bde5b6c9b803b485402ab5e28238d7a8793 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:32:59 +0000 Subject: [PATCH 05/23] fix(i18n): format generated catalogs --- scripts/i18n.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/i18n.ts b/scripts/i18n.ts index 5d4fc424b3..60086f91a2 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -171,6 +171,11 @@ function readJson(path: string): T { function writeJson(path: string, value: unknown): void { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) + const formatted = Bun.spawnSync( + [process.execPath, 'x', 'biome', 'format', '--write', relative(ROOT, path)], + { cwd: ROOT, stdout: 'ignore', stderr: 'inherit' }, + ) + if (formatted.exitCode !== 0) throw new Error(`Could not format ${relative(ROOT, path)}`) } function localeCatalogPath(locale: string): string { From 11e5d6119502a73f04f1a1854ae1fd0e8b3196f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:33:42 +0000 Subject: [PATCH 06/23] test(i18n): preserve ungrouped length formatting --- packages/core/src/i18n/i18n.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 243cad8b97..689b81cb35 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -88,7 +88,7 @@ describe('locale formatting', () => { setLocale('fr') expect(formatNumber(2.75, { max: 2 })).toBe('2,75') expect(formatLength(2.75, 'm')).toBe('2,75\u00a0m') - expect(formatLength(2.75, 'mm')).toBe('2 750\u00a0mm') + expect(formatLength(2.75, 'mm')).toBe('2750\u00a0mm') setLocale('en') expect(formatLength(2.75, 'm')).toBe('2.75\u00a0m') From 18554d8ee3adaf179ede3cd208897adc95fa17af Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:37:42 +0000 Subject: [PATCH 07/23] fix(i18n): preserve formatting and runtime updates --- packages/core/src/i18n/index.ts | 2 +- packages/core/src/i18n/use-translate.ts | 13 ++++++++---- .../editor/src/components/editor/index.tsx | 3 +++ .../src/components/editor/room-labels.tsx | 5 ++++- packages/editor/src/index.tsx | 1 + packages/editor/src/lib/measurements.ts | 2 ++ scripts/i18n.test.ts | 20 +++++++++++++++++++ scripts/i18n.ts | 18 ++++++++++++++++- 8 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts index f179664638..671aa65ab7 100644 --- a/packages/core/src/i18n/index.ts +++ b/packages/core/src/i18n/index.ts @@ -17,4 +17,4 @@ export { type TranslationParams, translate, } from './registry' -export { type Translate, useTranslate } from './use-translate' +export { type Translate, useLocale, useTranslate } from './use-translate' diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts index 4687b45a11..9140df218e 100644 --- a/packages/core/src/i18n/use-translate.ts +++ b/packages/core/src/i18n/use-translate.ts @@ -3,6 +3,7 @@ import { useCallback, useSyncExternalStore } from 'react' import { getI18nVersion, + getLocale, subscribe, type TranslationMessage, type TranslationParams, @@ -17,17 +18,21 @@ export type Translate = ( params?: TranslationParams, ) => string +export function useLocale(): string { + const version = useSyncExternalStore(subscribe, getI18nVersion, () => SERVER_SNAPSHOT) + return version === SERVER_SNAPSHOT ? 'en' : getLocale() +} + /** * The server and hydration snapshot always uses the in-code English message. * Once the client subscribes, the active locale is read from the registry and * locale or message registrations re-render the component. */ export function useTranslate(): Translate { - const version = useSyncExternalStore(subscribe, getI18nVersion, () => SERVER_SNAPSHOT) + const locale = useLocale() return useCallback( - (key, englishDefault, params) => - translate(key, englishDefault, params, version === SERVER_SNAPSHOT ? 'en' : undefined), - [version], + (key, englishDefault, params) => translate(key, englishDefault, params, locale), + [locale], ) } diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 5483314863..de89493285 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -10,6 +10,7 @@ import { initSpatialGridSync, setLocale, spatialGridManager, + useLocale, useScene, } from '@pascal-app/core' import { @@ -1964,6 +1965,8 @@ function EditorContent({ } export default function Editor(props: EditorProps) { + useLocale() + useClientLayoutEffect(() => { if (props.locale) setLocale(props.locale) }, [props.locale]) diff --git a/packages/editor/src/components/editor/room-labels.tsx b/packages/editor/src/components/editor/room-labels.tsx index 96bdb1652f..167fa33d22 100644 --- a/packages/editor/src/components/editor/room-labels.tsx +++ b/packages/editor/src/components/editor/room-labels.tsx @@ -94,7 +94,10 @@ export function roomLabelsVisible(state: RoomLabelVisibilityState): boolean { } export function formatRoomArea(squareMeters: number, unit: LinearUnit): string { - return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { max: 1 })} ${getAreaUnitLabel(unit)}` + return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { + min: 1, + max: 1, + })} ${getAreaUnitLabel(unit)}` } function useRoomLabelsVisible() { diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index abbd50c16e..edcba4e241 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -43,6 +43,7 @@ export { type TranslationMessages, type TranslationParams, translate, + useLocale, useScene, useTranslate, } from '@pascal-app/core' diff --git a/packages/editor/src/lib/measurements.ts b/packages/editor/src/lib/measurements.ts index 3485345fb5..8296316b7f 100644 --- a/packages/editor/src/lib/measurements.ts +++ b/packages/editor/src/lib/measurements.ts @@ -154,6 +154,7 @@ export function formatAreaLabel( if (!Number.isFinite(squareMeters)) return '--' return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { + min: fractionDigits, max: fractionDigits, })}${getAreaUnitLabel(unit)}` } @@ -174,6 +175,7 @@ export function formatVolumeLabel( if (!Number.isFinite(cubicMeters)) return '--' return `${formatNumber(cubicMetersToVolumeUnit(cubicMeters, unit), { + min: fractionDigits, max: fractionDigits, })}${getVolumeUnitLabel(unit)}` } diff --git a/scripts/i18n.test.ts b/scripts/i18n.test.ts index 94fcda390c..7e3449c434 100644 --- a/scripts/i18n.test.ts +++ b/scripts/i18n.test.ts @@ -55,3 +55,23 @@ test('AI translation refreshes only missing and stale entries through a mocked p sourceHash: 'hash-new', }) }) + +test('AI translation rejects broken placeholders from a provider', async () => { + const source: SourceCatalog = { + 'viewer.greeting': { + default: 'Hello {name}', + hash: 'hash-greeting', + context: ['greeting.tsx:1'], + }, + } + const catalog: LocaleCatalog = { locale: 'fr', messages: {} } + const provider: TranslationProvider = { + async translate() { + return { 'viewer.greeting': 'Bonjour' } + }, + } + + expect(refreshLocaleCatalog(source, catalog, provider, {})).rejects.toThrow( + 'Translation provider changed placeholders in "viewer.greeting"', + ) +}) diff --git a/scripts/i18n.ts b/scripts/i18n.ts index 60086f91a2..b30517e3df 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -195,7 +195,18 @@ function placeholders(message: Message): string[] { } function samePlaceholders(source: Message, translated: Message): boolean { - return JSON.stringify(placeholders(source)) === JSON.stringify(placeholders(translated)) + if (typeof source === 'string' || typeof translated === 'string') { + return ( + typeof source === 'string' && + typeof translated === 'string' && + JSON.stringify(placeholders(source)) === JSON.stringify(placeholders(translated)) + ) + } + + return Object.entries(translated).every(([category, value]) => { + const sourceValue = source[category as keyof typeof source] ?? source.other + return JSON.stringify(placeholders(sourceValue)) === JSON.stringify(placeholders(value)) + }) } function isMessage(value: unknown): value is Message { @@ -412,6 +423,11 @@ export async function refreshLocaleCatalog( for (const item of items) { const message = translated[item.key] if (!message) throw new Error(`Translation provider omitted "${item.key}"`) + if (!isMessage(message)) + throw new Error(`Translation provider returned an invalid message for "${item.key}"`) + if (!samePlaceholders(item.source, message)) { + throw new Error(`Translation provider changed placeholders in "${item.key}"`) + } messages[item.key] = { message, sourceHash: source[item.key]!.hash } } } From d931282e35a7cc8110f7b7d04bbe83d83b3fb05c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 20:39:55 +0000 Subject: [PATCH 08/23] fix(i18n): preserve locale and numeric fallbacks --- packages/core/src/i18n/format.ts | 2 +- packages/core/src/i18n/i18n.test.tsx | 9 +++++++++ packages/core/src/i18n/registry.ts | 8 ++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/src/i18n/format.ts b/packages/core/src/i18n/format.ts index d1104ab71b..4414c29c8a 100644 --- a/packages/core/src/i18n/format.ts +++ b/packages/core/src/i18n/format.ts @@ -91,7 +91,7 @@ export function normalizeDecimalInput(input: string, locale = getLocale()): stri export function parseLocalizedNumber(input: string, locale = getLocale()): number | null { const normalized = normalizeDecimalInput(input.trim(), locale).replace('−', '-') - if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) return null const value = Number(normalized) return Number.isFinite(value) ? value : null } diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 689b81cb35..b3c9d82b3e 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -34,6 +34,14 @@ describe('message registry', () => { expect(translate('viewer.controls.unknown', 'Unknown')).toBe('Unknown') }) + test('falls back through script before the base language', () => { + registerMessages('zh', 'viewer', { greeting: '你好' }) + registerMessages('zh-Hant', 'viewer', { greeting: '您好' }) + setLocale('zh-Hant-HK') + + expect(translate('viewer.greeting', 'Hello')).toBe('您好') + }) + test('interpolates values and selects plurals with Intl.PluralRules', () => { registerMessages('fr', 'editor', { selection: { @@ -97,6 +105,7 @@ describe('locale formatting', () => { test('accepts the locale decimal separator as well as a dot', () => { expect(parseLocalizedNumber('2,75', 'fr')).toBe(2.75) expect(parseLocalizedNumber('2.75', 'fr')).toBe(2.75) + expect(parseLocalizedNumber('1,25e2', 'fr')).toBe(125) expect(parseLocalizedNumber('2,75', 'en')).toBeNull() }) }) diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index 842d40c8f6..183cc66760 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -32,8 +32,12 @@ function canonicalLocale(value: string): string { function localeFallbacks(value: string): string[] { const exact = canonicalLocale(value) try { - const language = new Intl.Locale(exact).language - return language === exact ? [exact] : [exact, language] + const parsed = new Intl.Locale(exact) + const candidates = [exact] + if (parsed.baseName !== exact) candidates.push(parsed.baseName) + if (parsed.script) candidates.push(`${parsed.language}-${parsed.script}`) + candidates.push(parsed.language) + return [...new Set(candidates)] } catch { return [exact] } From c14412824486c3f5698afb32aeca185b2dca5598 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 21:28:35 +0000 Subject: [PATCH 09/23] fix(i18n): use source plural rules for fallbacks --- packages/core/src/i18n/i18n.test.tsx | 19 +++++++++++++++++++ packages/core/src/i18n/registry.ts | 10 ++++------ packages/editor/src/i18n/locales.ts | 4 +++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index b3c9d82b3e..85550df4cb 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -42,6 +42,13 @@ describe('message registry', () => { expect(translate('viewer.greeting', 'Hello')).toBe('您好') }) + test('allows host overrides for English while keeping in-code English as the fallback', () => { + registerMessages('en', 'viewer', { greeting: 'Welcome' }) + + expect(translate('viewer.greeting', 'Hello')).toBe('Welcome') + expect(translate('viewer.unregistered', 'In-code fallback')).toBe('In-code fallback') + }) + test('interpolates values and selects plurals with Intl.PluralRules', () => { registerMessages('fr', 'editor', { selection: { @@ -61,6 +68,18 @@ describe('message registry', () => { expect(translate('editor.welcome', 'Hello {name}', { name: 'Ada' })).toBe('Bonjour Ada') }) + test('uses English plural rules when a translation is missing', () => { + setLocale('ru') + + expect( + translate( + 'viewer.test.english-plural-fallback', + { one: '{count} item', other: '{count} items' }, + { count: 21 }, + ), + ).toBe('21 items') + }) + test('notifies subscribers for locale and active-catalog changes', () => { let changes = 0 const unsubscribe = subscribe(() => { diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index 183cc66760..644d94b875 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -117,7 +117,6 @@ function warnMissing(key: string, requestedLocale: string): void { function selectPlural( message: TranslationMessage, - fallback: TranslationMessage, count: number | undefined, requestedLocale: string, ): string { @@ -132,9 +131,7 @@ function selectPlural( } const translated = message[category] ?? message.other - if (translated !== undefined) return translated - if (typeof fallback === 'string') return fallback - return fallback[category] ?? fallback.other + return translated } function interpolate(message: string, params: TranslationParams | undefined): string { @@ -152,9 +149,10 @@ export function translate( requestedLocale = locale, ): string { const normalizedLocale = canonicalLocale(requestedLocale) - const translated = normalizedLocale === 'en' ? undefined : messageFor(key, normalizedLocale) + const translated = messageFor(key, normalizedLocale) if (translated === undefined) warnMissing(key, normalizedLocale) const message = translated ?? englishDefault const count = typeof params?.count === 'number' ? params.count : undefined - return interpolate(selectPlural(message, englishDefault, count, normalizedLocale), params) + const pluralLocale = translated === undefined ? 'en' : normalizedLocale + return interpolate(selectPlural(message, count, pluralLocale), params) } diff --git a/packages/editor/src/i18n/locales.ts b/packages/editor/src/i18n/locales.ts index b2ca56e536..831c5a48da 100644 --- a/packages/editor/src/i18n/locales.ts +++ b/packages/editor/src/i18n/locales.ts @@ -32,7 +32,9 @@ export function registerBundledLocales(): void { const namespaces = new Map() for (const [fullKey, entry] of Object.entries(catalog.messages)) { const separator = fullKey.indexOf('.') - if (separator < 1) continue + if (separator < 1) { + throw new Error(`Invalid translation key "${fullKey}" in ${catalog.locale}`) + } const namespace = fullKey.slice(0, separator) const key = fullKey.slice(separator + 1) const messages = namespaces.get(namespace) ?? {} From 2fc1547484f48e46c122ad5d6044401ea7176448 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 21:29:53 +0000 Subject: [PATCH 10/23] test(i18n): isolate English host override --- packages/core/src/i18n/i18n.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 85550df4cb..8f7245d420 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -43,9 +43,9 @@ describe('message registry', () => { }) test('allows host overrides for English while keeping in-code English as the fallback', () => { - registerMessages('en', 'viewer', { greeting: 'Welcome' }) + registerMessages('en', 'viewer', { hostGreeting: 'Welcome' }) - expect(translate('viewer.greeting', 'Hello')).toBe('Welcome') + expect(translate('viewer.hostGreeting', 'Hello')).toBe('Welcome') expect(translate('viewer.unregistered', 'In-code fallback')).toBe('In-code fallback') }) From abfdd4a32f8cd756f15aa54b3db7a90809c80304 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 21:31:23 +0000 Subject: [PATCH 11/23] fix(i18n): keep canonical English in code --- packages/core/src/i18n/i18n.test.tsx | 4 ++-- packages/core/src/i18n/registry.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 8f7245d420..17ccbb598d 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -42,10 +42,10 @@ describe('message registry', () => { expect(translate('viewer.greeting', 'Hello')).toBe('您好') }) - test('allows host overrides for English while keeping in-code English as the fallback', () => { + test('keeps canonical English in code', () => { registerMessages('en', 'viewer', { hostGreeting: 'Welcome' }) - expect(translate('viewer.hostGreeting', 'Hello')).toBe('Welcome') + expect(translate('viewer.hostGreeting', 'Hello')).toBe('Hello') expect(translate('viewer.unregistered', 'In-code fallback')).toBe('In-code fallback') }) diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index 644d94b875..bb441869fc 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -149,7 +149,7 @@ export function translate( requestedLocale = locale, ): string { const normalizedLocale = canonicalLocale(requestedLocale) - const translated = messageFor(key, normalizedLocale) + const translated = normalizedLocale === 'en' ? undefined : messageFor(key, normalizedLocale) if (translated === undefined) warnMissing(key, normalizedLocale) const message = translated ?? englishDefault const count = typeof params?.count === 'number' ? params.count : undefined From 9356b30cc82ec4b103ad125a186a3d1870831a23 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 21:43:03 +0000 Subject: [PATCH 12/23] ci: rerun flaky quality job From 2d381ce6482c86cf089a0e2732e72d4d80847655 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 22:01:37 +0000 Subject: [PATCH 13/23] fix(i18n): address locale review findings --- i18n/glossary.json | 7 ++ packages/core/src/i18n/format.ts | 27 ++++- packages/core/src/i18n/i18n.test.tsx | 51 ++++++++- packages/core/src/i18n/index.ts | 1 + packages/core/src/i18n/registry.ts | 1 + .../src/components/editor/room-labels.tsx | 12 +- .../components/ui/build-panel/build-panel.tsx | 44 ++++--- .../components/viewer/floorplan-preview.tsx | 15 ++- .../components/viewer/viewer-scene-header.tsx | 15 ++- packages/editor/src/i18n/catalogs/es.json | 24 ++-- packages/editor/src/i18n/catalogs/fr.json | 24 ++-- packages/editor/src/i18n/catalogs/ja.json | 24 ++-- packages/editor/src/i18n/catalogs/zh-CN.json | 28 ++--- packages/editor/src/i18n/locales.test.ts | 16 +++ packages/editor/src/i18n/source-catalog.json | 108 +++++++++--------- packages/editor/src/lib/measurement-parser.ts | 7 +- packages/editor/src/lib/measurements.test.ts | 17 ++- scripts/i18n.test.ts | 10 ++ scripts/i18n.ts | 31 ++++- 19 files changed, 317 insertions(+), 145 deletions(-) diff --git a/i18n/glossary.json b/i18n/glossary.json index 1b93c3701e..8a7bd9ec58 100644 --- a/i18n/glossary.json +++ b/i18n/glossary.json @@ -1,6 +1,13 @@ { "productContext": "Pascal is an architecture and building-design editor. Use concise professional UI language familiar to architects, CAD users, builders, and interior designers.", "terms": { + "ceiling": { + "zh-CN": "天花板", + "fr": "plafond", + "es": "techo", + "ja": "天井", + "note": "A generic overhead room surface, not necessarily a suspended ceiling." + }, "floor plan": { "zh-CN": "平面图", "fr": "plan d’étage", diff --git a/packages/core/src/i18n/format.ts b/packages/core/src/i18n/format.ts index 4414c29c8a..5dc5894588 100644 --- a/packages/core/src/i18n/format.ts +++ b/packages/core/src/i18n/format.ts @@ -6,6 +6,10 @@ export interface NumberFormatOptions { /** Maximum fraction digits. */ max?: number useGrouping?: boolean + /** + * Locale used for formatting. React renderers must pass useLocale() so the + * server and hydration snapshots use the same locale. + */ locale?: string } @@ -43,6 +47,27 @@ function decimalSeparator(locale: string): string { } } +/** + * The persisted level is zero-based at grade. Japanese and Simplified Chinese + * label that same storey as floor 1, so floors above it display one higher. + */ +export function levelDisplayNumber(level: number, locale = getLocale()): number { + if (level <= 0) return level + try { + const parsed = new Intl.Locale(locale) + if (parsed.language === 'ja') return level + 1 + if ( + parsed.language === 'zh' && + (parsed.script === 'Hans' || (!parsed.script && parsed.region === 'CN')) + ) { + return level + 1 + } + } catch { + // Invalid locales use the stored English numbering. + } + return level +} + export function formatNumber(value: number, options: NumberFormatOptions = {}): string { if (!Number.isFinite(value)) return '--' const minimumFractionDigits = fractionDigits(options.min, 0) @@ -52,7 +77,7 @@ export function formatNumber(value: number, options: NumberFormatOptions = {}): ) try { - return new Intl.NumberFormat(options.locale ?? getLocale(), { + return new Intl.NumberFormat(options.locale ?? 'en', { minimumFractionDigits, maximumFractionDigits, useGrouping: options.useGrouping ?? false, diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 17ccbb598d..1bed5a4ede 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -5,12 +5,14 @@ import { formatLength, formatNumber, LevelNode, + levelDisplayNumber, parseLocalizedNumber, registerMessages, SiteNode, setLocale, subscribe, translate, + useLocale, useTranslate, } from '../index' @@ -42,6 +44,21 @@ describe('message registry', () => { expect(translate('viewer.greeting', 'Hello')).toBe('您好') }) + test('maps explicit Simplified Chinese scripts to zh-CN without crossing into Traditional', () => { + registerMessages('zh-CN', 'viewer', { 'test.simplified-alias': '简体中文' }) + + expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-Hans')).toBe( + '简体中文', + ) + expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-Hans-CN')).toBe( + '简体中文', + ) + expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-Hant')).toBe( + 'English', + ) + expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-TW')).toBe('English') + }) + test('keeps canonical English in code', () => { registerMessages('en', 'viewer', { hostGreeting: 'Welcome' }) @@ -113,12 +130,12 @@ describe('message registry', () => { describe('locale formatting', () => { test('formats numbers and lengths without coupling locale to the unit', () => { setLocale('fr') - expect(formatNumber(2.75, { max: 2 })).toBe('2,75') - expect(formatLength(2.75, 'm')).toBe('2,75\u00a0m') - expect(formatLength(2.75, 'mm')).toBe('2750\u00a0mm') + expect(formatNumber(2.75, { locale: 'fr', max: 2 })).toBe('2,75') + expect(formatLength(2.75, 'm', { locale: 'fr' })).toBe('2,75\u00a0m') + expect(formatLength(2.75, 'mm', { locale: 'fr' })).toBe('2750\u00a0mm') setLocale('en') - expect(formatLength(2.75, 'm')).toBe('2.75\u00a0m') + expect(formatLength(2.75, 'm', { locale: 'en' })).toBe('2.75\u00a0m') }) test('accepts the locale decimal separator as well as a dot', () => { @@ -127,6 +144,16 @@ describe('locale formatting', () => { expect(parseLocalizedNumber('1,25e2', 'fr')).toBe(125) expect(parseLocalizedNumber('2,75', 'en')).toBeNull() }) + + test('uses local floor numbering without changing persisted level indices', () => { + expect(levelDisplayNumber(1, 'en')).toBe(1) + expect(levelDisplayNumber(1, 'ja')).toBe(2) + expect(levelDisplayNumber(1, 'zh-CN')).toBe(2) + expect(levelDisplayNumber(1, 'zh-Hans')).toBe(2) + expect(levelDisplayNumber(1, 'zh-Hant')).toBe(1) + expect(levelDisplayNumber(1, 'zh-TW')).toBe(1) + expect(levelDisplayNumber(0, 'ja')).toBe(0) + }) }) test('useTranslate renders the English snapshot on the server', () => { @@ -141,6 +168,22 @@ test('useTranslate renders the English snapshot on the server', () => { expect(renderToString()).toContain('Hello') }) +test('rendered number formatting defaults to English and can use the hydration-safe snapshot', () => { + setLocale('fr') + + function Measurement() { + const locale = useLocale() + return ( + <> + {formatNumber(2.75, { max: 2 })} + {formatNumber(2.75, { locale, max: 2 })} + + ) + } + + expect(renderToString()).not.toContain('2,75') +}) + test('building and serializing a scene is locale-neutral', () => { function serializeFixture() { const level = LevelNode.parse({ id: 'level_i18n', parentId: 'building_i18n', children: [] }) diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts index 671aa65ab7..0253c4ac90 100644 --- a/packages/core/src/i18n/index.ts +++ b/packages/core/src/i18n/index.ts @@ -2,6 +2,7 @@ export { formatLength, formatNumber, type LengthUnit, + levelDisplayNumber, type NumberFormatOptions, normalizeDecimalInput, parseLocalizedNumber, diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index bb441869fc..fba2c320a8 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -36,6 +36,7 @@ function localeFallbacks(value: string): string[] { const candidates = [exact] if (parsed.baseName !== exact) candidates.push(parsed.baseName) if (parsed.script) candidates.push(`${parsed.language}-${parsed.script}`) + if (parsed.language === 'zh' && parsed.script === 'Hans') candidates.push('zh-CN') candidates.push(parsed.language) return [...new Set(candidates)] } catch { diff --git a/packages/editor/src/components/editor/room-labels.tsx b/packages/editor/src/components/editor/room-labels.tsx index 167fa33d22..489a4eb6c1 100644 --- a/packages/editor/src/components/editor/room-labels.tsx +++ b/packages/editor/src/components/editor/room-labels.tsx @@ -12,6 +12,7 @@ import { polygonInteriorPoint, type Ring, sceneRegistry, + useLocale, useScene, type ZoneNode, } from '@pascal-app/core' @@ -93,10 +94,11 @@ export function roomLabelsVisible(state: RoomLabelVisibilityState): boolean { ) } -export function formatRoomArea(squareMeters: number, unit: LinearUnit): string { +export function formatRoomArea(squareMeters: number, unit: LinearUnit, locale?: string): string { return `${formatNumber(squareMetersToAreaUnit(squareMeters, unit), { min: 1, max: 1, + locale, })} ${getAreaUnitLabel(unit)}` } @@ -1173,11 +1175,12 @@ export function RoomPillChip({ onLeave: () => void }) { const unit = useViewer((s) => s.unit) - const areaLabel = formatRoomArea(areaM2, unit) + const locale = useLocale() + const areaLabel = formatRoomArea(areaM2, unit, locale) // biome-ignore lint/correctness/useExhaustiveDependencies: A dot's border box does not resize when its hidden text changes. useLayoutEffect(() => { if (elements instanceof PillElements) elements.measure(zoneId) - }, [areaM2, elements, name, unit, zoneId]) + }, [areaM2, elements, locale, name, unit, zoneId]) const register = useCallback( (element: HTMLElement | null) => { if (element) elements.set(zoneId, element) @@ -1231,6 +1234,7 @@ export function RoomPillDetail({ areaM2: number }) { const unit = useViewer((s) => s.unit) + const locale = useLocale() return (
- +
) } diff --git a/packages/editor/src/components/ui/build-panel/build-panel.tsx b/packages/editor/src/components/ui/build-panel/build-panel.tsx index aa727530e5..ae8d20f484 100644 --- a/packages/editor/src/components/ui/build-panel/build-panel.tsx +++ b/packages/editor/src/components/ui/build-panel/build-panel.tsx @@ -191,12 +191,23 @@ export function BuildToolTile({ ) } +const CAPTION_LEAD_MARKER = '__PASCAL_CAPTION_LEAD__' + /** The line under the Rooms tiles: what the armed shape does, lead in bold. */ -export function BuildPanelCaption({ lead, rest }: { lead?: string; rest: string }) { +export function BuildPanelCaption({ caption, lead }: { caption: string; lead?: string }) { + const markerIndex = caption.indexOf(CAPTION_LEAD_MARKER) + const hasMarkedLead = lead && markerIndex >= 0 return (

- {lead ? {lead} : null} - {rest} + {hasMarkedLead ? ( + <> + {caption.slice(0, markerIndex)} + {lead} + {caption.slice(markerIndex + CAPTION_LEAD_MARKER.length)} + + ) : ( + caption + )}

) } @@ -246,14 +257,14 @@ export function BuildPanelRoomsSection({ })} ) @@ -265,9 +276,10 @@ function roomVariantMessages(variant: WallDrawVariant, t: Translate) { label: t('editor.buildPanel.rooms.rectangle.label', 'Rectangle'), title: t('editor.buildPanel.rooms.rectangle.title', 'Rectangle room'), lead: t('editor.buildPanel.rooms.rectangle.lead', 'Click two corners'), - rest: t( - 'editor.buildPanel.rooms.rectangle.rest', - 'on the grid. Walls, floor and ceiling are created together.', + caption: t( + 'editor.buildPanel.rooms.rectangle.caption', + '{lead} on the grid. Walls, floor and ceiling are created together.', + { lead: CAPTION_LEAD_MARKER }, ), } } @@ -276,9 +288,10 @@ function roomVariantMessages(variant: WallDrawVariant, t: Translate) { label: t('editor.buildPanel.rooms.walls.label', 'Walls'), title: t('editor.buildPanel.rooms.walls.title', 'Walls'), lead: t('editor.buildPanel.rooms.walls.lead', 'Draw one wall at a time.'), - rest: t( - 'editor.buildPanel.rooms.walls.rest', - 'Close a loop and it becomes a room; leave it open for partitions.', + caption: t( + 'editor.buildPanel.rooms.walls.caption', + '{lead} Close a loop and it becomes a room; leave it open for partitions.', + { lead: CAPTION_LEAD_MARKER }, ), } } @@ -286,9 +299,10 @@ function roomVariantMessages(variant: WallDrawVariant, t: Translate) { label: t('editor.buildPanel.rooms.polygon.label', 'Polygon'), title: t('editor.buildPanel.rooms.polygon.title', 'Polygon room'), lead: t('editor.buildPanel.rooms.polygon.lead', 'Click each corner,'), - rest: t( - 'editor.buildPanel.rooms.polygon.rest', - 'then the first one again to close. For L-shapes and angled rooms.', + caption: t( + 'editor.buildPanel.rooms.polygon.caption', + '{lead} then the first one again to close. For L-shapes and angled rooms.', + { lead: CAPTION_LEAD_MARKER }, ), } } diff --git a/packages/editor/src/components/viewer/floorplan-preview.tsx b/packages/editor/src/components/viewer/floorplan-preview.tsx index a907d16fb9..2443c2107d 100644 --- a/packages/editor/src/components/viewer/floorplan-preview.tsx +++ b/packages/editor/src/components/viewer/floorplan-preview.tsx @@ -8,8 +8,10 @@ import { type GeometryContext, getLevelDisplayName, isNodeKindEnabled, + levelDisplayNumber, nodeRegistry, type Translate, + useLocale, useScene, useTranslate, } from '@pascal-app/core' @@ -145,7 +147,7 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox { } } -function levelLabel(level: AnyNode, t: Translate): string { +function levelLabel(level: AnyNode, t: Translate, locale: string): string { const levelNumber = (level as { level?: number }).level ?? 0 const levelName = (level as { name?: string }).name const english = getLevelDisplayName({ @@ -159,7 +161,11 @@ function levelLabel(level: AnyNode, t: Translate): string { if (levelNumber < -1) { return t('core.level.basementNumber', 'Basement {number}', { number: -levelNumber }) } - if (levelNumber > 0) return t('core.level.floorNumber', 'Floor {number}', { number: levelNumber }) + if (levelNumber > 0) { + return t('core.level.floorNumber', 'Floor {number}', { + number: levelDisplayNumber(levelNumber, locale), + }) + } return english } @@ -323,6 +329,7 @@ export function FloorplanPreview({ synchronizeNavigation = false, }: FloorplanPreviewProps) { const t = useTranslate() + const locale = useLocale() const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes)) const storeInstalledPlugins = useScene((state) => scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins, @@ -898,7 +905,7 @@ export function FloorplanPreview({ aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight + - 0 F" aria-label={t('viewer.floorplan.viewLabel', '{floor} 2D view', { floor: activeLevel - ? levelLabel(activeLevel, t) + ? levelLabel(activeLevel, t, locale) : t('viewer.floorplan.floorPlan', 'Floor plan'), })} onKeyDown={onKeyDown} @@ -1003,7 +1010,7 @@ export function FloorplanPreview({ > {levels.map((level) => ( ))} diff --git a/packages/editor/src/components/viewer/viewer-scene-header.tsx b/packages/editor/src/components/viewer/viewer-scene-header.tsx index afde6161ab..0f76e9069e 100644 --- a/packages/editor/src/components/viewer/viewer-scene-header.tsx +++ b/packages/editor/src/components/viewer/viewer-scene-header.tsx @@ -3,8 +3,10 @@ import { getLevelDisplayName, type LevelNode, + levelDisplayNumber, sceneRegistry, type Translate, + useLocale, useScene, useTranslate, } from '@pascal-app/core' @@ -49,7 +51,7 @@ function sectionTitle(kind: ViewerSidebarSection['kind'], t: Translate): string return t('viewer.header.zones', 'Zones') } -function translatedLevelName(level: LevelNode, t: Translate): string { +function translatedLevelName(level: LevelNode, t: Translate, locale: string): string { const english = getLevelDisplayName(level) const authoredName = level.name?.trim() if (authoredName && authoredName !== `Level ${level.level}`) return authoredName @@ -58,7 +60,11 @@ function translatedLevelName(level: LevelNode, t: Translate): string { if (level.level < -1) { return t('core.level.basementNumber', 'Basement {number}', { number: -level.level }) } - if (level.level > 0) return t('core.level.floorNumber', 'Floor {number}', { number: level.level }) + if (level.level > 0) { + return t('core.level.floorNumber', 'Floor {number}', { + number: levelDisplayNumber(level.level, locale), + }) + } return english } @@ -79,6 +85,7 @@ export const ViewerSceneHeader = ({ source: sourceProp, }: ViewerSceneHeaderProps) => { const t = useTranslate() + const locale = useLocale() const sceneNodes = useScene((s) => s.nodes) const source = useMemo( () => sourceProp ?? { nodes: sceneNodes, objectFor: parametricObject }, @@ -237,7 +244,7 @@ export const ViewerSceneHeader = ({
- {translatedLevelName(lvl, t)} + {translatedLevelName(lvl, t, locale)}
@@ -278,7 +285,7 @@ export const ViewerSceneHeader = ({ /> {row.label} - {formatRoomArea(row.areaM2, unit)} + {formatRoomArea(row.areaM2, unit, locale)} ) diff --git a/packages/editor/src/i18n/catalogs/es.json b/packages/editor/src/i18n/catalogs/es.json index b17c7aafe7..28d9718994 100644 --- a/packages/editor/src/i18n/catalogs/es.json +++ b/packages/editor/src/i18n/catalogs/es.json @@ -29,6 +29,10 @@ "message": "Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.", "sourceHash": "271039bd919dc4c1" }, + "editor.buildPanel.rooms.polygon.caption": { + "message": "{lead} y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.", + "sourceHash": "ae2800b6c9e2b6ed" + }, "editor.buildPanel.rooms.polygon.label": { "message": "Polígono", "sourceHash": "4e186fc057733162" @@ -37,14 +41,14 @@ "message": "Haz clic en cada esquina", "sourceHash": "345e3aca3879b5c5" }, - "editor.buildPanel.rooms.polygon.rest": { - "message": "y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.", - "sourceHash": "4b869bde0632749b" - }, "editor.buildPanel.rooms.polygon.title": { "message": "Estancia poligonal", "sourceHash": "8ec6152e139b0e3f" }, + "editor.buildPanel.rooms.rectangle.caption": { + "message": "{lead} en la cuadrícula. Los muros, el suelo y el techo se crean a la vez.", + "sourceHash": "9b46295eedcbedae" + }, "editor.buildPanel.rooms.rectangle.label": { "message": "Rectángulo", "sourceHash": "2c2c20272b0370cc" @@ -53,10 +57,6 @@ "message": "Haz clic en dos esquinas", "sourceHash": "060faa48e299d7c9" }, - "editor.buildPanel.rooms.rectangle.rest": { - "message": "de la cuadrícula. Los muros, el suelo y el techo se crean a la vez.", - "sourceHash": "5c868318eb6e0952" - }, "editor.buildPanel.rooms.rectangle.title": { "message": "Estancia rectangular", "sourceHash": "83a45603ac7f41e8" @@ -65,6 +65,10 @@ "message": "Estancias", "sourceHash": "3e7408ed44676176" }, + "editor.buildPanel.rooms.walls.caption": { + "message": "{lead} Cierra el contorno para crear una estancia o déjalo abierto para tabiques.", + "sourceHash": "8a21a03c4acf396b" + }, "editor.buildPanel.rooms.walls.label": { "message": "Muros", "sourceHash": "aac9a732d2159ccc" @@ -73,10 +77,6 @@ "message": "Dibuja un muro cada vez.", "sourceHash": "9843edd68426fa2d" }, - "editor.buildPanel.rooms.walls.rest": { - "message": "Cierra el contorno para crear una estancia o déjalo abierto para tabiques.", - "sourceHash": "e3c4f3c4a5a9d8a6" - }, "editor.buildPanel.rooms.walls.title": { "message": "Muros", "sourceHash": "aac9a732d2159ccc" diff --git a/packages/editor/src/i18n/catalogs/fr.json b/packages/editor/src/i18n/catalogs/fr.json index 0d31ca9fe6..86889813fc 100644 --- a/packages/editor/src/i18n/catalogs/fr.json +++ b/packages/editor/src/i18n/catalogs/fr.json @@ -29,6 +29,10 @@ "message": "Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.", "sourceHash": "271039bd919dc4c1" }, + "editor.buildPanel.rooms.polygon.caption": { + "message": "{lead} puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.", + "sourceHash": "ae2800b6c9e2b6ed" + }, "editor.buildPanel.rooms.polygon.label": { "message": "Polygone", "sourceHash": "4e186fc057733162" @@ -37,14 +41,14 @@ "message": "Cliquez sur chaque angle,", "sourceHash": "345e3aca3879b5c5" }, - "editor.buildPanel.rooms.polygon.rest": { - "message": "puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.", - "sourceHash": "4b869bde0632749b" - }, "editor.buildPanel.rooms.polygon.title": { "message": "Pièce polygonale", "sourceHash": "8ec6152e139b0e3f" }, + "editor.buildPanel.rooms.rectangle.caption": { + "message": "{lead} sur la grille. Les murs, le sol et le plafond sont créés ensemble.", + "sourceHash": "9b46295eedcbedae" + }, "editor.buildPanel.rooms.rectangle.label": { "message": "Rectangle", "sourceHash": "2c2c20272b0370cc" @@ -53,10 +57,6 @@ "message": "Cliquez sur deux angles", "sourceHash": "060faa48e299d7c9" }, - "editor.buildPanel.rooms.rectangle.rest": { - "message": "de la grille. Les murs, le sol et le plafond sont créés ensemble.", - "sourceHash": "5c868318eb6e0952" - }, "editor.buildPanel.rooms.rectangle.title": { "message": "Pièce rectangulaire", "sourceHash": "83a45603ac7f41e8" @@ -65,6 +65,10 @@ "message": "Pièces", "sourceHash": "3e7408ed44676176" }, + "editor.buildPanel.rooms.walls.caption": { + "message": "{lead} Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.", + "sourceHash": "8a21a03c4acf396b" + }, "editor.buildPanel.rooms.walls.label": { "message": "Murs", "sourceHash": "aac9a732d2159ccc" @@ -73,10 +77,6 @@ "message": "Tracez un mur à la fois.", "sourceHash": "9843edd68426fa2d" }, - "editor.buildPanel.rooms.walls.rest": { - "message": "Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.", - "sourceHash": "e3c4f3c4a5a9d8a6" - }, "editor.buildPanel.rooms.walls.title": { "message": "Murs", "sourceHash": "aac9a732d2159ccc" diff --git a/packages/editor/src/i18n/catalogs/ja.json b/packages/editor/src/i18n/catalogs/ja.json index 1f6d1bea20..912f35ab91 100644 --- a/packages/editor/src/i18n/catalogs/ja.json +++ b/packages/editor/src/i18n/catalogs/ja.json @@ -29,6 +29,10 @@ "message": "形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。", "sourceHash": "271039bd919dc4c1" }, + "editor.buildPanel.rooms.polygon.caption": { + "message": "{lead}最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。", + "sourceHash": "ae2800b6c9e2b6ed" + }, "editor.buildPanel.rooms.polygon.label": { "message": "多角形", "sourceHash": "4e186fc057733162" @@ -37,14 +41,14 @@ "message": "各頂点をクリックし、", "sourceHash": "345e3aca3879b5c5" }, - "editor.buildPanel.rooms.polygon.rest": { - "message": "最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。", - "sourceHash": "4b869bde0632749b" - }, "editor.buildPanel.rooms.polygon.title": { "message": "多角形の部屋", "sourceHash": "8ec6152e139b0e3f" }, + "editor.buildPanel.rooms.rectangle.caption": { + "message": "グリッド上で{lead}します。壁・床・天井をまとめて作成します。", + "sourceHash": "9b46295eedcbedae" + }, "editor.buildPanel.rooms.rectangle.label": { "message": "長方形", "sourceHash": "2c2c20272b0370cc" @@ -53,10 +57,6 @@ "message": "対角の2点をクリック", "sourceHash": "060faa48e299d7c9" }, - "editor.buildPanel.rooms.rectangle.rest": { - "message": "グリッド上で指定します。壁・床・天井をまとめて作成します。", - "sourceHash": "5c868318eb6e0952" - }, "editor.buildPanel.rooms.rectangle.title": { "message": "長方形の部屋", "sourceHash": "83a45603ac7f41e8" @@ -65,6 +65,10 @@ "message": "部屋", "sourceHash": "3e7408ed44676176" }, + "editor.buildPanel.rooms.walls.caption": { + "message": "{lead}輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。", + "sourceHash": "8a21a03c4acf396b" + }, "editor.buildPanel.rooms.walls.label": { "message": "壁", "sourceHash": "aac9a732d2159ccc" @@ -73,10 +77,6 @@ "message": "壁を1面ずつ描画します。", "sourceHash": "9843edd68426fa2d" }, - "editor.buildPanel.rooms.walls.rest": { - "message": "輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。", - "sourceHash": "e3c4f3c4a5a9d8a6" - }, "editor.buildPanel.rooms.walls.title": { "message": "壁", "sourceHash": "aac9a732d2159ccc" diff --git a/packages/editor/src/i18n/catalogs/zh-CN.json b/packages/editor/src/i18n/catalogs/zh-CN.json index a2dd3aaaad..d2fe5dacfb 100644 --- a/packages/editor/src/i18n/catalogs/zh-CN.json +++ b/packages/editor/src/i18n/catalogs/zh-CN.json @@ -22,13 +22,17 @@ "sourceHash": "f571cb2d06ae01ae" }, "editor.buildPanel.rooms.hint": { - "message": "墙体 · 楼板 · 吊顶", + "message": "墙体 · 楼板 · 天花板", "sourceHash": "fb40866c2d6db5fb" }, "editor.buildPanel.rooms.idle": { - "message": "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和吊顶。", + "message": "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和天花板。", "sourceHash": "271039bd919dc4c1" }, + "editor.buildPanel.rooms.polygon.caption": { + "message": "{lead}然后再次点击起点闭合。适用于 L 形和斜角房间。", + "sourceHash": "ae2800b6c9e2b6ed" + }, "editor.buildPanel.rooms.polygon.label": { "message": "多边形", "sourceHash": "4e186fc057733162" @@ -37,14 +41,14 @@ "message": "依次点击各个角点,", "sourceHash": "345e3aca3879b5c5" }, - "editor.buildPanel.rooms.polygon.rest": { - "message": "然后再次点击起点闭合。适用于 L 形和斜角房间。", - "sourceHash": "4b869bde0632749b" - }, "editor.buildPanel.rooms.polygon.title": { "message": "多边形房间", "sourceHash": "8ec6152e139b0e3f" }, + "editor.buildPanel.rooms.rectangle.caption": { + "message": "在网格上{lead}。墙体、楼板和天花板将一并创建。", + "sourceHash": "9b46295eedcbedae" + }, "editor.buildPanel.rooms.rectangle.label": { "message": "矩形", "sourceHash": "2c2c20272b0370cc" @@ -53,10 +57,6 @@ "message": "点击两个对角点", "sourceHash": "060faa48e299d7c9" }, - "editor.buildPanel.rooms.rectangle.rest": { - "message": "在网格上。墙体、楼板和吊顶将一并创建。", - "sourceHash": "5c868318eb6e0952" - }, "editor.buildPanel.rooms.rectangle.title": { "message": "矩形房间", "sourceHash": "83a45603ac7f41e8" @@ -65,6 +65,10 @@ "message": "房间", "sourceHash": "3e7408ed44676176" }, + "editor.buildPanel.rooms.walls.caption": { + "message": "{lead}闭合轮廓后会形成房间;保持开口可绘制隔墙。", + "sourceHash": "8a21a03c4acf396b" + }, "editor.buildPanel.rooms.walls.label": { "message": "墙体", "sourceHash": "aac9a732d2159ccc" @@ -73,10 +77,6 @@ "message": "逐面绘制墙体。", "sourceHash": "9843edd68426fa2d" }, - "editor.buildPanel.rooms.walls.rest": { - "message": "闭合轮廓后会形成房间;保持开口可绘制隔墙。", - "sourceHash": "e3c4f3c4a5a9d8a6" - }, "editor.buildPanel.rooms.walls.title": { "message": "墙体", "sourceHash": "aac9a732d2159ccc" diff --git a/packages/editor/src/i18n/locales.test.ts b/packages/editor/src/i18n/locales.test.ts index 0588e11bef..e9f8bdd84d 100644 --- a/packages/editor/src/i18n/locales.test.ts +++ b/packages/editor/src/i18n/locales.test.ts @@ -20,3 +20,19 @@ test('bundled locale packs cover the converted viewer and editor surfaces', () = expect(translate('editor.buildPanel.rooms.title', 'Rooms')).toBe(rooms) } }) + +test('Simplified Chinese aliases use zh-CN without affecting Traditional Chinese', () => { + registerBundledLocales() + + setLocale('zh-Hans') + expect(translate('viewer.display.title', 'Display settings')).toBe('显示设置') + + setLocale('zh-Hans-CN') + expect(translate('viewer.display.title', 'Display settings')).toBe('显示设置') + + setLocale('zh-Hant') + expect(translate('viewer.display.title', 'Display settings')).toBe('Display settings') + + setLocale('zh-TW') + expect(translate('viewer.display.title', 'Display settings')).toBe('Display settings') +}) diff --git a/packages/editor/src/i18n/source-catalog.json b/packages/editor/src/i18n/source-catalog.json index 8e706d8f94..6e3c6486f6 100644 --- a/packages/editor/src/i18n/source-catalog.json +++ b/packages/editor/src/i18n/source-catalog.json @@ -3,32 +3,32 @@ "default": "Basement", "hash": "8caa959c0ae800c3", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:158", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:57" + "packages/editor/src/components/viewer/floorplan-preview.tsx:160", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:59" ] }, "core.level.basementNumber": { "default": "Basement {number}", "hash": "14927085ee91ba05", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:160", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:59" + "packages/editor/src/components/viewer/floorplan-preview.tsx:162", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:61" ] }, "core.level.floorNumber": { "default": "Floor {number}", "hash": "e59074a7b9b3f56e", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:162", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:61" + "packages/editor/src/components/viewer/floorplan-preview.tsx:165", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:64" ] }, "core.level.groundFloor": { "default": "Ground floor", "hash": "cde3efc9b7ddfa41", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:157", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:56" + "packages/editor/src/components/viewer/floorplan-preview.tsx:159", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:58" ] }, "editor.buildPanel.advanced": { @@ -39,77 +39,77 @@ "editor.buildPanel.rooms.hint": { "default": "walls · floor · ceiling", "hash": "fb40866c2d6db5fb", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:225"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:236"] }, "editor.buildPanel.rooms.idle": { "default": "Pick a shape to start. Every room brings its own walls, floor and ceiling.", "hash": "271039bd919dc4c1", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:252"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:262"] + }, + "editor.buildPanel.rooms.polygon.caption": { + "default": "{lead} then the first one again to close. For L-shapes and angled rooms.", + "hash": "ae2800b6c9e2b6ed", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:302"] }, "editor.buildPanel.rooms.polygon.label": { "default": "Polygon", "hash": "4e186fc057733162", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:286"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:299"] }, "editor.buildPanel.rooms.polygon.lead": { "default": "Click each corner,", "hash": "345e3aca3879b5c5", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:288"] - }, - "editor.buildPanel.rooms.polygon.rest": { - "default": "then the first one again to close. For L-shapes and angled rooms.", - "hash": "4b869bde0632749b", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:289"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:301"] }, "editor.buildPanel.rooms.polygon.title": { "default": "Polygon room", "hash": "8ec6152e139b0e3f", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:287"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:300"] + }, + "editor.buildPanel.rooms.rectangle.caption": { + "default": "{lead} on the grid. Walls, floor and ceiling are created together.", + "hash": "9b46295eedcbedae", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:279"] }, "editor.buildPanel.rooms.rectangle.label": { "default": "Rectangle", "hash": "2c2c20272b0370cc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:265"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:276"] }, "editor.buildPanel.rooms.rectangle.lead": { "default": "Click two corners", "hash": "060faa48e299d7c9", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:267"] - }, - "editor.buildPanel.rooms.rectangle.rest": { - "default": "on the grid. Walls, floor and ceiling are created together.", - "hash": "5c868318eb6e0952", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:268"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:278"] }, "editor.buildPanel.rooms.rectangle.title": { "default": "Rectangle room", "hash": "83a45603ac7f41e8", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:266"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:277"] }, "editor.buildPanel.rooms.title": { "default": "Rooms", "hash": "3e7408ed44676176", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:227"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:238"] + }, + "editor.buildPanel.rooms.walls.caption": { + "default": "{lead} Close a loop and it becomes a room; leave it open for partitions.", + "hash": "8a21a03c4acf396b", + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:291"] }, "editor.buildPanel.rooms.walls.label": { "default": "Walls", "hash": "aac9a732d2159ccc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:276"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:288"] }, "editor.buildPanel.rooms.walls.lead": { "default": "Draw one wall at a time.", "hash": "9843edd68426fa2d", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:278"] - }, - "editor.buildPanel.rooms.walls.rest": { - "default": "Close a loop and it becomes a room; leave it open for partitions.", - "hash": "e3c4f3c4a5a9d8a6", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:279"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:290"] }, "editor.buildPanel.rooms.walls.title": { "default": "Walls", "hash": "aac9a732d2159ccc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:277"] + "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:289"] }, "viewer.camera.orbitLeft": { "default": "Orbit left", @@ -259,87 +259,87 @@ "viewer.floorplan.empty": { "default": "No floor plans are available for this scene.", "hash": "286a757d8b52bb02", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:886"] + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:893"] }, "viewer.floorplan.fit": { "default": "Fit floor plan", "hash": "bf63c679ab94dbcd", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1043", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1046" + "packages/editor/src/components/viewer/floorplan-preview.tsx:1050", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1053" ] }, "viewer.floorplan.floor": { "default": "Floor", "hash": "d7148fa065bc709b", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:990", - "packages/editor/src/components/viewer/floorplan-preview.tsx:992" + "packages/editor/src/components/viewer/floorplan-preview.tsx:997", + "packages/editor/src/components/viewer/floorplan-preview.tsx:999" ] }, "viewer.floorplan.floorPlan": { "default": "Floor plan", "hash": "fee6203bb9046929", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:902"] + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:909"] }, "viewer.floorplan.viewLabel": { "default": "{floor} 2D view", "hash": "28b255b7bf5f1084", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:899"] + "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:906"] }, "viewer.floorplan.zoomIn": { "default": "Zoom in", "hash": "d17804412f8dd081", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1052", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1055" + "packages/editor/src/components/viewer/floorplan-preview.tsx:1059", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1062" ] }, "viewer.floorplan.zoomOut": { "default": "Zoom out", "hash": "23b012a283b2c14a", "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1034", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1037" + "packages/editor/src/components/viewer/floorplan-preview.tsx:1041", + "packages/editor/src/components/viewer/floorplan-preview.tsx:1044" ] }, "viewer.header.back": { "default": "Back", "hash": "7ceade8f25b58fe9", "context": [ - "packages/editor/src/components/viewer/viewer-scene-header.tsx:137", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:146" + "packages/editor/src/components/viewer/viewer-scene-header.tsx:144", + "packages/editor/src/components/viewer/viewer-scene-header.tsx:153" ] }, "viewer.header.levels": { "default": "Levels", "hash": "cc3d601d59e2e26c", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:210"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:217"] }, "viewer.header.rooms": { "default": "Rooms", "hash": "3e7408ed44676176", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:48"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:50"] }, "viewer.header.selection": { "default": "Selection", "hash": "170b2049ef0f9131", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:175"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:182"] }, "viewer.header.units": { "default": "Units", "hash": "b404dded35241274", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:47"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:49"] }, "viewer.header.untitled": { "default": "Untitled", "hash": "6547a4c0c368042a", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:156"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:163"] }, "viewer.header.zones": { "default": "Zones", "hash": "665db49a26f81425", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:49"] + "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:51"] }, "viewer.levels.exploded": { "default": "Exploded", diff --git a/packages/editor/src/lib/measurement-parser.ts b/packages/editor/src/lib/measurement-parser.ts index d569b452f2..d0ed429bbb 100644 --- a/packages/editor/src/lib/measurement-parser.ts +++ b/packages/editor/src/lib/measurement-parser.ts @@ -92,7 +92,7 @@ export interface MeasurementHintOptions extends ParseMeasurementOptions { clamp?: (stored: number) => number } -const PLAIN_NUMBER = /^[+-]?\d*(?:[.,]\d*)?$/ +const PLAIN_NUMBER = /^[+-]?\d*\.?\d*$/ /** * A faint "= 1.83 m" preview of the value currently being typed, shown only @@ -106,9 +106,10 @@ export function measurementHint( options: MeasurementHintOptions = {}, ): string | null { const trimmed = raw.trim() - if (!trimmed || PLAIN_NUMBER.test(trimmed)) return null + const normalized = normalizeDecimalInput(trimmed) + if (!normalized || PLAIN_NUMBER.test(normalized)) return null - const parsed = parseMeasurement(trimmed, spec, options) + const parsed = parseMeasurement(normalized, spec, options) if (parsed === null) return null const stored = options.clamp ? options.clamp(parsed) : parsed diff --git a/packages/editor/src/lib/measurements.test.ts b/packages/editor/src/lib/measurements.test.ts index f9fa358dc2..b3d4fd88de 100644 --- a/packages/editor/src/lib/measurements.test.ts +++ b/packages/editor/src/lib/measurements.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, test } from 'bun:test' +import { afterEach, describe, expect, test } from 'bun:test' +import { setLocale } from '@pascal-app/core' import { measurementHint, parseMeasurement } from '../index' import { buildMeasurementAngleArcPoints, @@ -193,6 +194,8 @@ describe('volume measurements', () => { }) describe('typed measurement fields', () => { + afterEach(() => setLocale('en')) + test('canonicalizes explicit, compound, and scientific dimensions', () => { const length = { kind: 'length', unitId: 'm' } as const expect(parseMeasurement('180cm', length)).toBeCloseTo(1.8) @@ -212,4 +215,16 @@ describe('typed measurement fields', () => { expect(measurementHint('180cm', length)).toBe('= 1.80 m') expect(measurementHint('6', length)).toBeNull() }) + + test('only treats the active locale decimal separator as a plain decimal', () => { + const length = { kind: 'length', unitId: 'm' } as const + + setLocale('en') + expect(measurementHint('1,000', length)).not.toBeNull() + expect(measurementHint('-1,000', length)).not.toBeNull() + expect(measurementHint('1,5', length)).not.toBeNull() + + setLocale('fr') + expect(measurementHint('1,5', length)).toBeNull() + }) }) diff --git a/scripts/i18n.test.ts b/scripts/i18n.test.ts index 7e3449c434..7e3aac07ba 100644 --- a/scripts/i18n.test.ts +++ b/scripts/i18n.test.ts @@ -1,11 +1,21 @@ import { expect, test } from 'bun:test' import { + hasRequiredPluralForms, type LocaleCatalog, refreshLocaleCatalog, type SourceCatalog, type TranslationProvider, } from './i18n' +test('plural validation follows the target locale categories', () => { + const english = { one: '{count} item', other: '{count} items' } + + expect(hasRequiredPluralForms(english, { other: '{count} 個' }, 'ja')).toBeTrue() + expect(hasRequiredPluralForms(english, { other: '{count} 个' }, 'zh-CN')).toBeTrue() + expect(hasRequiredPluralForms(english, { other: '{count} items' }, 'en')).toBeFalse() + expect(hasRequiredPluralForms(english, english, 'en')).toBeTrue() +}) + test('AI translation refreshes only missing and stale entries through a mocked provider', async () => { const source: SourceCatalog = { 'viewer.current': { diff --git a/scripts/i18n.ts b/scripts/i18n.ts index b30517e3df..28cc1a506f 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -209,6 +209,24 @@ function samePlaceholders(source: Message, translated: Message): boolean { }) } +function pluralCategories(locale: string): Intl.LDMLPluralRule[] { + try { + return new Intl.PluralRules(locale).resolvedOptions().pluralCategories + } catch { + return new Intl.PluralRules('en').resolvedOptions().pluralCategories + } +} + +export function hasRequiredPluralForms( + source: Message, + translated: Message, + locale: string, +): boolean { + if (typeof source === 'string') return typeof translated === 'string' + if (typeof translated === 'string') return false + return pluralCategories(locale).every((category) => category in translated) +} + function isMessage(value: unknown): value is Message { if (typeof value === 'string') return true if (!(value && typeof value === 'object' && !Array.isArray(value))) return false @@ -298,11 +316,7 @@ export function checkCatalogs(root = ROOT): string[] { if (!samePlaceholders(source.default, translation.message)) { errors.push(`${locale}: broken placeholders in "${key}"`) } - if ( - typeof source.default === 'object' && - (typeof translation.message !== 'object' || - !Object.keys(source.default).every((category) => category in translation.message)) - ) { + if (!hasRequiredPluralForms(source.default, translation.message, locale)) { errors.push(`${locale}: missing plural forms in "${key}"`) } } @@ -315,6 +329,7 @@ function promptFor(request: TranslationRequest): string { `Translate these Pascal architecture/CAD interface messages into ${request.locale}.`, 'Return one JSON object mapping every input key to its translated string or plural-form object.', 'Preserve {placeholders} exactly. Keep labels concise and use a consistent professional CAD glossary.', + `For plural objects, include every cardinal category required by Intl.PluralRules: ${pluralCategories(request.locale).join(', ')}.`, 'Do not add commentary or markdown. Human reviewers will approve the resulting diff.', `Glossary: ${JSON.stringify(request.glossary)}`, `Messages: ${JSON.stringify(request.items)}`, @@ -333,6 +348,9 @@ function parseProviderResponse(text: string, request: TranslationRequest): Recor if (!samePlaceholders(item.source, message)) { throw new Error(`Translation provider changed placeholders in "${item.key}"`) } + if (!hasRequiredPluralForms(item.source, message, request.locale)) { + throw new Error(`Translation provider omitted plural forms in "${item.key}"`) + } messages[item.key] = message } return messages @@ -428,6 +446,9 @@ export async function refreshLocaleCatalog( if (!samePlaceholders(item.source, message)) { throw new Error(`Translation provider changed placeholders in "${item.key}"`) } + if (!hasRequiredPluralForms(item.source, message, catalog.locale)) { + throw new Error(`Translation provider omitted plural forms in "${item.key}"`) + } messages[item.key] = { message, sourceHash: source[item.key]!.hash } } } From f91254cd6b2c5a358badf1f3a0db9b9b66062221 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 22:37:35 +0000 Subject: [PATCH 14/23] refactor(i18n): adopt Lingui runtime and PO catalogs --- apps/editor/app/client-bootstrap.tsx | 30 +- apps/editor/app/layout.tsx | 14 +- apps/editor/lib/ui-locale.ts | 11 + apps/editor/proxy.test.ts | 8 + apps/editor/proxy.ts | 8 +- biome.jsonc | 2 + bun.lock | 185 +++- i18n/README.md | 124 ++- lingui.config.ts | 30 + package.json | 12 +- packages/core/package.json | 3 + packages/core/src/i18n/i18n.test.tsx | 108 ++- packages/core/src/i18n/index.ts | 8 +- packages/core/src/i18n/registry.ts | 270 ++++-- packages/core/src/i18n/use-translate.ts | 60 +- packages/editor/package.json | 1 + .../editor/src/components/editor/index.tsx | 32 +- .../components/ui/build-panel/build-panel.tsx | 96 +- .../viewer/floorplan-compass-button.tsx | 9 +- .../components/viewer/floorplan-preview.tsx | 61 +- .../components/viewer/viewer-controls-bar.tsx | 156 ++-- .../components/viewer/viewer-scene-header.tsx | 48 +- .../viewer/viewer-stage-switcher.tsx | 8 +- packages/editor/src/i18n/catalogs/es.json | 345 ------- packages/editor/src/i18n/catalogs/fr.json | 345 ------- packages/editor/src/i18n/catalogs/ja.json | 345 ------- packages/editor/src/i18n/catalogs/zh-CN.json | 345 ------- packages/editor/src/i18n/locales.ts | 44 +- .../editor/src/i18n/locales/en/messages.po | 362 ++++++++ .../editor/src/i18n/locales/en/messages.ts | 1 + .../editor/src/i18n/locales/es/messages.po | 623 +++++++++++++ .../editor/src/i18n/locales/es/messages.ts | 1 + .../editor/src/i18n/locales/fr/messages.po | 623 +++++++++++++ .../editor/src/i18n/locales/fr/messages.ts | 1 + .../editor/src/i18n/locales/ja/messages.po | 623 +++++++++++++ .../editor/src/i18n/locales/ja/messages.ts | 1 + .../editor/src/i18n/locales/zh-CN/messages.po | 623 +++++++++++++ .../editor/src/i18n/locales/zh-CN/messages.ts | 1 + packages/editor/src/i18n/source-catalog.json | 469 ---------- packages/editor/src/index.tsx | 9 +- scripts/i18n.test.ts | 125 +-- scripts/i18n.ts | 846 ++++++++++-------- 42 files changed, 4338 insertions(+), 2678 deletions(-) create mode 100644 apps/editor/lib/ui-locale.ts create mode 100644 lingui.config.ts delete mode 100644 packages/editor/src/i18n/catalogs/es.json delete mode 100644 packages/editor/src/i18n/catalogs/fr.json delete mode 100644 packages/editor/src/i18n/catalogs/ja.json delete mode 100644 packages/editor/src/i18n/catalogs/zh-CN.json create mode 100644 packages/editor/src/i18n/locales/en/messages.po create mode 100644 packages/editor/src/i18n/locales/en/messages.ts create mode 100644 packages/editor/src/i18n/locales/es/messages.po create mode 100644 packages/editor/src/i18n/locales/es/messages.ts create mode 100644 packages/editor/src/i18n/locales/fr/messages.po create mode 100644 packages/editor/src/i18n/locales/fr/messages.ts create mode 100644 packages/editor/src/i18n/locales/ja/messages.po create mode 100644 packages/editor/src/i18n/locales/ja/messages.ts create mode 100644 packages/editor/src/i18n/locales/zh-CN/messages.po create mode 100644 packages/editor/src/i18n/locales/zh-CN/messages.ts delete mode 100644 packages/editor/src/i18n/source-catalog.json diff --git a/apps/editor/app/client-bootstrap.tsx b/apps/editor/app/client-bootstrap.tsx index 65d19e3594..ae89a75ba7 100644 --- a/apps/editor/app/client-bootstrap.tsx +++ b/apps/editor/app/client-bootstrap.tsx @@ -9,34 +9,42 @@ // `loaded` guard inside `../lib/bootstrap` keeps the side effect // idempotent under HMR. import '../lib/bootstrap' -import { getLocale, setLocale, subscribe } from '@pascal-app/core' +import { createI18n, defaultI18n, PascalI18nProvider } from '@pascal-app/core' import { registerBundledLocales } from '@pascal-app/editor/locales' -import { type ReactNode, useEffect } from 'react' +import { type ReactNode, useEffect, useMemo } from 'react' registerBundledLocales() -if (typeof window !== 'undefined') { - const queryLocale = new URLSearchParams(window.location.search).get('lang') - if (queryLocale) setLocale(queryLocale) -} - export function ClientBootstrap({ children, enableDevDiagnostics, + locale, }: { children: ReactNode enableDevDiagnostics: boolean + locale: string }) { + const i18n = useMemo(() => createI18n({ locale }), [locale]) + useEffect(() => { if (!enableDevDiagnostics) return import('react-scan').then(({ scan }) => scan({ enabled: true })) }, [enableDevDiagnostics]) + useEffect(() => { const syncDocumentLocale = () => { - document.documentElement.lang = getLocale() + document.documentElement.lang = i18n.getLocale() } syncDocumentLocale() - return subscribe(syncDocumentLocale) - }, []) - return children + return i18n.subscribe(syncDocumentLocale) + }, [i18n]) + + useEffect(() => { + const syncDefaultLocale = () => i18n.setLocale(defaultI18n.getLocale()) + return defaultI18n.subscribe(syncDefaultLocale) + }, [i18n]) + + useEffect(() => () => i18n.destroy(), [i18n]) + + return {children} } diff --git a/apps/editor/app/layout.tsx b/apps/editor/app/layout.tsx index 01965f4ba3..1107368272 100644 --- a/apps/editor/app/layout.tsx +++ b/apps/editor/app/layout.tsx @@ -1,10 +1,15 @@ +import { registerBundledLocales } from '@pascal-app/editor/locales' import { Agentation } from 'agentation' import { GeistPixelSquare } from 'geist/font/pixel' import { Barlow } from 'next/font/google' import localFont from 'next/font/local' +import { headers } from 'next/headers' +import { PASCAL_LOCALE_HEADER, resolveUiLocale } from '@/lib/ui-locale' import { ClientBootstrap } from './client-bootstrap' import './globals.css' +registerBundledLocales() + const geistSans = localFont({ src: './fonts/GeistVF.woff', variable: '--font-geist-sans', @@ -21,21 +26,24 @@ const barlow = Barlow({ display: 'swap', }) -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const enableDevDiagnostics = process.env.NODE_ENV === 'development' && process.env.PASCAL_DEV_DIAGNOSTICS === '1' + const locale = resolveUiLocale((await headers()).get(PASCAL_LOCALE_HEADER)) return ( - {children} + + {children} + {enableDevDiagnostics && } diff --git a/apps/editor/lib/ui-locale.ts b/apps/editor/lib/ui-locale.ts new file mode 100644 index 0000000000..135f1d1cf9 --- /dev/null +++ b/apps/editor/lib/ui-locale.ts @@ -0,0 +1,11 @@ +export const PASCAL_LOCALE_HEADER = 'x-pascal-locale' + +export function resolveUiLocale(value: string | null | undefined): string { + const requested = value?.trim() + if (!requested) return 'en' + try { + return Intl.getCanonicalLocales(requested)[0] ?? 'en' + } catch { + return 'en' + } +} diff --git a/apps/editor/proxy.test.ts b/apps/editor/proxy.test.ts index 1326543d23..59c62f2e61 100644 --- a/apps/editor/proxy.test.ts +++ b/apps/editor/proxy.test.ts @@ -1,5 +1,6 @@ import { afterEach, expect, test } from 'bun:test' import { issueSession, SESSION_COOKIE } from './lib/self-host-auth' +import { resolveUiLocale } from './lib/ui-locale' import { gateRequest } from './proxy' const OLD_ENV = { ...process.env } @@ -75,3 +76,10 @@ test('loopback and an upstream proxy are open', () => { process.env.PASCAL_AUTH = 'proxy' expect(gateRequest(request('/'))).toBeNull() }) + +test('UI locale canonicalization is deterministic for server rendering', () => { + expect(resolveUiLocale('fr')).toBe('fr') + expect(resolveUiLocale('zh-hans-cn')).toBe('zh-Hans-CN') + expect(resolveUiLocale('')).toBe('en') + expect(resolveUiLocale('not_a_locale')).toBe('en') +}) diff --git a/apps/editor/proxy.ts b/apps/editor/proxy.ts index 9fe7227207..855a50c7cd 100644 --- a/apps/editor/proxy.ts +++ b/apps/editor/proxy.ts @@ -1,5 +1,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { resolveAuthMode, sessionPrincipal } from '@/lib/self-host-auth' +import { PASCAL_LOCALE_HEADER, resolveUiLocale } from '@/lib/ui-locale' /** * Self-host gate. Pages redirect to `/login` (or `/setup` when nothing is @@ -17,7 +18,12 @@ const OPEN_PREFIXES = ['/embed/'] const SELF_GUARDED_API_PREFIXES = ['/api/scenes', '/api/artifacts'] export function proxy(request: NextRequest) { - return gateRequest(request) ?? NextResponse.next() + const gated = gateRequest(request) + if (gated) return gated + + const headers = new Headers(request.headers) + headers.set(PASCAL_LOCALE_HEADER, resolveUiLocale(request.nextUrl.searchParams.get('lang'))) + return NextResponse.next({ request: { headers } }) } export function gateRequest(request: Request): NextResponse | null { diff --git a/biome.jsonc b/biome.jsonc index 72ec2714aa..c54a71bea4 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -118,6 +118,8 @@ "!**/build", "!**/public", "!**/components/ui", + // Generated by `lingui compile`; source of truth is the adjacent PO file. + "!packages/editor/src/i18n/locales/**/messages.ts", // Recorded API responses; kept byte-exact. "!**/__fixtures__", "!**/next-env.d.ts" diff --git a/bun.lock b/bun.lock index 39548ae0ec..bd077dcc75 100644 --- a/bun.lock +++ b/bun.lock @@ -5,14 +5,23 @@ "": { "name": "editor", "devDependencies": { + "@ai-sdk/anthropic": "^4.0.89", + "@ai-sdk/openai": "^4.0.103", "@biomejs/biome": "^2.6.3", + "@lingui/cli": "^6.10.1", + "@lingui/conf": "6.10.1", + "@lingui/format-po": "^6.10.1", + "@lingui/message-utils": "6.10.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "@typescript/typescript6": "6.0.2", + "ai": "^7.0.154", "dotenv-cli": "^11.0.0", "fast-xml-parser": "^5.11.2", + "pofile-ts": "^4.0.3", "turbo": "^2.11.10", "typescript": "npm:@typescript/typescript6@6.0.2", "ultracite": "^8.1.0", + "zod": "^4.6.7", }, "optionalDependencies": { "@tailwindcss/oxide-darwin-arm64": "4.3.3", @@ -127,6 +136,9 @@ "name": "@pascal-app/core", "version": "1.0.6", "dependencies": { + "@lingui/core": "^6.10.1", + "@lingui/message-utils": "^6.10.1", + "@lingui/react": "^6.10.1", "@pascal-app/lingo": "^0.5.0", "dedent": "^1.7.2", "idb-keyval": "^6.3.0", @@ -161,6 +173,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.3", + "@lingui/core": "6.10.1", "@number-flow/react": "^0.6.2", "@pascal-app/geometry-script": "^1.0.6", "@pascal-app/ifc-converter": "^1.0.6", @@ -398,6 +411,16 @@ "three": "0.186.1", }, "packages": { + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.89", "", { "dependencies": { "@ai-sdk/provider": "4.0.34", "@ai-sdk/provider-utils": "5.0.69" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-PCXExlOB4P51rzJ9hGIcZ+6DfHdLpcbs0IzgbtBEubITa6f90+7K/sD38rXfdDzYdVtjFitdtSl3cr4KAY2wQw=="], + + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.127", "", { "dependencies": { "@ai-sdk/provider": "4.0.34", "@ai-sdk/provider-utils": "5.0.69", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ENcIQNrZxgFnNSx29X2o/mLKZRzMN91c53Qlp/UrF4RQGVSfaROjFQAq4eeJliRPWwjHvEhJYG42IrOwtWUGOQ=="], + + "@ai-sdk/openai": ["@ai-sdk/openai@4.0.103", "", { "dependencies": { "@ai-sdk/provider": "4.0.34", "@ai-sdk/provider-utils": "5.0.69" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZaMITn2RBzILNzrpXxJDMPRpNHGwZdq77GWqd9UBuz5VAA8EOu9JPy8hTk5OVhPMzDZbNgpb/D/1G+NbeVP22w=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.34", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Ip7ZQBq/FiGpM1mmOVseTtX9PBjnzo0bLC41WTgBSBBRUBz6LhNFGbYaNUzI6hPm6ll1oNSPOAZc/q/z+3QZEQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.69", "", { "dependencies": { "@ai-sdk/provider": "4.0.34", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oGCXIoBKLpNrrAzJvltFHdBl20FJMyT+7edD9D4nwxoI8Scbj3y1EgKzjoF2XFhNYkMTcv1oTFDgUzLxdjqSKg=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@astrojs/compiler": ["@astrojs/compiler@4.0.0", "", {}, "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA=="], @@ -468,6 +491,8 @@ "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@dgreenheck/ez-tree": ["@dgreenheck/ez-tree@1.1.0", "", { "peerDependencies": { "three": ">=0.167" } }, "sha512-6pvS6hD6B6h00dm0SnkgYeT4ABU5Y1Z9M44p1tXiV5C0eKrQy2sKECXshoaUv0qAOqYVL68w/PwadUxDFDiHUg=="], "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], @@ -606,6 +631,10 @@ "@iwer/sem": ["@iwer/sem@0.2.5", "", { "dependencies": { "three": "^0.165.0", "ts-proto": "^2.6.0" }, "peerDependencies": { "iwer": "^2.0.0" } }, "sha512-vMCfpu/7Qqc+hkBiGD9pxjeObgrhXOrL0KX94CA3yzJaU0dq0y49HXZT6fC+6X/jOmjaM3hjyE1m2h7ZmLzzyA=="], + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -622,6 +651,42 @@ "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@lingui/babel-plugin-extract-messages": ["@lingui/babel-plugin-extract-messages@6.10.1", "", { "dependencies": { "@lingui/conf": "6.10.1" } }, "sha512-geg2chO+ahysUT7RdPdOMHNVb2hpM7jDrQpro+IyZKG6NwZ6BlsroxXpJKC27k/Jw1g7uvMBrkw7zOPeGCfi5A=="], + + "@lingui/babel-plugin-lingui-macro": ["@lingui/babel-plugin-lingui-macro@6.10.1", "", { "dependencies": { "@lingui/conf": "6.10.1", "@lingui/message-utils": "6.10.1" }, "peerDependencies": { "@babel/core": "^7.20.12 || ^8.0.0" }, "optionalPeers": ["@babel/core"] }, "sha512-AQs7UsweJS+JMb1ySzETHFktdgwmdQaJM7B9WWVvuhJv0BtiI/S/XJcl2Nyh93mvWD3HArIFRWECKnZ0y5lIeQ=="], + + "@lingui/cli": ["@lingui/cli@6.10.1", "", { "dependencies": { "@babel/core": "^7.21.0", "@babel/generator": "^7.28.5", "@babel/parser": "^7.22.0", "@babel/types": "^7.21.2", "@lingui/babel-plugin-extract-messages": "6.10.1", "@lingui/babel-plugin-lingui-macro": "6.10.1", "@lingui/conf": "6.10.1", "@lingui/core": "6.10.1", "@lingui/format-po": "6.10.1", "@lingui/message-utils": "6.10.1", "@lingui/native-tools": "^0.1.6", "chokidar": "5.0.0", "cli-table3": "^0.6.5", "commander": "^14.0.2", "jiti": "^2.6.1", "micromatch": "^4.0.7", "ms": "^2.1.3", "normalize-path": "^3.0.0", "ora": "^9.1.0", "pseudolocale": "^3.1.0", "source-map": "^0.7.6", "tinypool": "^2.1.0" }, "peerDependencies": { "esbuild": "^0.28.1", "rolldown": "^1.0.0" }, "optionalPeers": ["esbuild", "rolldown"], "bin": { "lingui": "dist/lingui.js" } }, "sha512-C80aWjnGutoPFkw7F995yt3APK32+7X5d36DdyL5MKSrAHf8irBOG2wjy2nhm6gjDWOS0+s6zCTh8HX2HDC+CQ=="], + + "@lingui/conf": ["@lingui/conf@6.10.1", "", { "dependencies": { "jest-validate": "^29.4.3", "jiti": "^2.5.1", "lilconfig": "^3.1.3", "normalize-path": "^3.0.0" } }, "sha512-utgrZY0AUj08dLi/9jSrR0chwBlvPKjGGDCPHR1UHCVZb/EIxskbITiCVITUfVuxp5+FMSPMm0V1lzF1Q86qlg=="], + + "@lingui/core": ["@lingui/core@6.10.1", "", { "dependencies": { "@lingui/babel-plugin-lingui-macro": "6.10.1", "@lingui/message-utils": "6.10.1" }, "peerDependencies": { "babel-plugin-macros": "2 || 3" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-s2b2yyXBwNFdKz5r3S9zaGLlJpSTH3+k+q0RGQr/TbjoTDsuTCMbHHT6stTWj377/4p/E3UTOrIy7r7J5jTGUA=="], + + "@lingui/format-po": ["@lingui/format-po@6.10.1", "", { "dependencies": { "@lingui/conf": "6.10.1", "@lingui/message-utils": "6.10.1", "pofile-ts": "^4.0.3" } }, "sha512-C+PjhCV4ERBCTFFymcIpBNfZaQfMcwh97+YENvmqgaUNQ95WcMVV9dk+qV+V+QG4g82a6ZCNbce1zSAxoy7rdw=="], + + "@lingui/message-utils": ["@lingui/message-utils@6.10.1", "", { "dependencies": { "@messageformat/date-skeleton": "^1.1.0", "@messageformat/parser": "^5.0.0", "js-sha256": "^0.10.1" } }, "sha512-IzCtKG+wtC0uMFs7pONVAFscHzQN0FjW6RG7DULVJcHaLmH/hOGc7QpGX7Jzm7d8QguDfUM0QmSwFwLkYDmNvQ=="], + + "@lingui/native-tools": ["@lingui/native-tools@0.1.6", "", { "dependencies": { "@swc/types": "^0.1.25" }, "optionalDependencies": { "@lingui/native-tools-android-arm64": "0.1.6", "@lingui/native-tools-darwin-arm64": "0.1.6", "@lingui/native-tools-darwin-x64": "0.1.6", "@lingui/native-tools-linux-arm64-gnu": "0.1.6", "@lingui/native-tools-linux-arm64-musl": "0.1.6", "@lingui/native-tools-linux-x64-gnu": "0.1.6", "@lingui/native-tools-linux-x64-musl": "0.1.6", "@lingui/native-tools-win32-arm64-msvc": "0.1.6", "@lingui/native-tools-win32-x64-msvc": "0.1.6" }, "peerDependencies": { "@lingui/conf": "^6.0.1" } }, "sha512-xsn2R9YvWL9WuemM0ivSlXy14ORlfvPYLxFP3kbcFU3Pg5T9aRnmYOTfwszwGnjt4QsORO1mHHZT6k/Wr7V9pA=="], + + "@lingui/native-tools-android-arm64": ["@lingui/native-tools-android-arm64@0.1.6", "", { "os": "android", "cpu": "arm64" }, "sha512-kAwVqJlKv868dGJOhqXXqHwhI3bOlvdHSZz8rozlEEidvBuw6Srr0Es5hwisLlwybzcTYstjOeVGxmvu3QAV4g=="], + + "@lingui/native-tools-darwin-arm64": ["@lingui/native-tools-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0KEoSob+pYS1jyRB+2bJp78L2L2i/VODFIk5Gs8LmKV58xZ+uN8MOhf1dYBBsi6+0tiqPartSCk3WA4i+lKPUQ=="], + + "@lingui/native-tools-darwin-x64": ["@lingui/native-tools-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-e7liIjkzbJ8w3F9PHzZPmpYpeiv7XO4Np9J1tEpUamKYEdYBNt4AKr/UmWAQ7AjxbZSV9N8lj1ulUl3pdFUuaQ=="], + + "@lingui/native-tools-linux-arm64-gnu": ["@lingui/native-tools-linux-arm64-gnu@0.1.6", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-JKu/8CvLsYUiRbRSnuzmrQk24tfuizgnDYlMxE5/awUUrRE+GlpaMP76PhJpkl+tV0v7t6zVe3+I5lbq7LaqVw=="], + + "@lingui/native-tools-linux-arm64-musl": ["@lingui/native-tools-linux-arm64-musl@0.1.6", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-Aeexf/Ta0nZDZz3cDMAZ95GmFZ2lOxMwf1FK/ShK+U4WcNVSn3aI2KeuOeaLUfot9ZT8AiIIELPV4gO6b/GJgg=="], + + "@lingui/native-tools-linux-x64-gnu": ["@lingui/native-tools-linux-x64-gnu@0.1.6", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-SwX1dLcQE9pVcXwIxSGTE2+zBmaO6NbjD9XPqCiyoO8RFTkUTLjdnRDGHxFlFpIGMlVArQz5mMQ6coyUFl+AiA=="], + + "@lingui/native-tools-linux-x64-musl": ["@lingui/native-tools-linux-x64-musl@0.1.6", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-aWI8e10I6i7K9BG77LJDHREozx/VT8j8onf0wxd43ACor2S1VwS0ZNlIfpNWQTWovmU/CUliGO/VWQq2jGa+Sg=="], + + "@lingui/native-tools-win32-arm64-msvc": ["@lingui/native-tools-win32-arm64-msvc@0.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-M2TZP4gKgLaLbiboXIrY5+8GsaQLHvZ8E6cUFgAuSaBa8AMa73+pZO1eay/St9t8yDh6EL6hOlqN0HedUHMwyQ=="], + + "@lingui/native-tools-win32-x64-msvc": ["@lingui/native-tools-win32-x64-msvc@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-5KvuSTFtDB8UGHS3Rxd+juYZgc51isQSx0hPukDObre6UxZGuUVCBmJ9YwoGVzXedViiNGmsQvrnCTBbVqI75g=="], + + "@lingui/react": ["@lingui/react@6.10.1", "", { "dependencies": { "@lingui/babel-plugin-lingui-macro": "6.10.1", "@lingui/core": "6.10.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "babel-plugin-macros": "2 || 3", "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-r0e4T7WpkeKeVxT+qt56u7i8z1ykRIotpsCXaA6Vm2K0iVkE0SebcWpBaEFpBX8FyHEzXSA5nshSH6dm50pL1Q=="], + "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.3", "", {}, "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg=="], "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], @@ -642,6 +707,10 @@ "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="], + "@messageformat/date-skeleton": ["@messageformat/date-skeleton@1.1.0", "", {}, "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A=="], + + "@messageformat/parser": ["@messageformat/parser@5.2.0", "", { "dependencies": { "moo": "^0.5.1" } }, "sha512-sc3hNZ0JtC32GhL5nUgPbh2ilcwji4G+Vmj/vhMd8hZaH//NDOvkfwKK1mFnRWG5h2DpksN8xr/HB5/+vMg3iw=="], + "@mint/pascal-plugin": ["@mint/pascal-plugin@github:mintdotgg/mint-pascal-plugin#902c546", { "peerDependencies": { "@pascal-app/core": ">=0.9.2 <1.0.0 || >=1.0.0-beta.1 <1.0.0", "@pascal-app/editor": ">=0.9.2 <1.0.0 || >=1.0.0-beta.1 <1.0.0", "@pascal-app/viewer": ">=0.9.2 <1.0.0 || >=1.0.0-beta.1 <1.0.0", "react": "^18 || ^19", "three": "^0.185" } }, "mintdotgg-mint-pascal-plugin-902c546", "sha512-/itUH9r9OIP8ZPrklW8iWe6B2SDOtlL1m8r9hlVM4Rw5josYtDdkDKQ37FbanvxxuUKcQnukHbtxWe3svRaCrQ=="], "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.4.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.4.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.8", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-LgrD2OphHuG12vfKFKz6SOnq7bKHQmhGJIAYdD+MZBn2npHEyry6uD8zvktodtl5CQM85MO0anUyD9JwmUmxww=="], @@ -974,10 +1043,18 @@ "@shaderfrog/glsl-parser": ["@shaderfrog/glsl-parser@7.0.1", "", {}, "sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + "@swc/types": ["@swc/types@0.1.28", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], @@ -1044,6 +1121,12 @@ "@types/howler": ["@types/howler@2.2.13", "", {}, "sha512-40+EBjqIHHrC4VShlz/7i0lBUsE3QkgzZinQQji74Hd8sBkJZUBaT7LWFLK6rcabsDOOQpoMbEJvtaFQwxOu/g=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/ndarray": ["@types/ndarray@1.0.14", "", {}, "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg=="], @@ -1066,6 +1149,10 @@ "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.72.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/type-utils": "8.72.0", "@typescript-eslint/utils": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.72.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.72.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw=="], @@ -1150,12 +1237,16 @@ "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@visual-json/core": ["@visual-json/core@0.4.0", "", {}, "sha512-T/HvRIp7r6SehxVnO+akV72NLv2OMBEoQdYTFbpwMi6uU2TaXQOpIOHBG4r9XBz7OqC2e8aedOwal/NtqtzyCg=="], "@visual-json/react": ["@visual-json/react@0.4.0", "", { "dependencies": { "@visual-json/core": "0.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-qiDrXI7wu9VoTE0TaLoJp7kwRj8SQTpkS/wkts5sBPA8078zdrIKc+0u0Y8wAXGYd3udw9dNfD4kmpO48OEwXQ=="], "@webxr/plugin": ["@webxr/plugin@github:sudhir9297/webxr-pascal-plugin#9cd9045", { "dependencies": { "@iwer/devui": "2.3.0", "iwer": "2.3.0", "lucide-react": "^1.7.0" }, "peerDependencies": { "@pascal-app/core": "^1.0.0-beta.5", "@pascal-app/editor": "^1.0.0-beta.5", "@pascal-app/viewer": "^1.0.0-beta.5", "@react-three/drei": "^10", "@react-three/fiber": "^9", "@react-three/xr": "^6", "react": "^18 || ^19", "react-dom": "^18 || ^19", "three": "^0.185 || ^0.186", "zustand": "^5" } }, "sudhir9297-webxr-pascal-plugin-9cd9045", "sha512-oICXf9DgdzLR+dVdAzp8z7knF+fZUUX2MJKZ/G2ljT5dXg6gmjPH16HWRy/Reb/hKILvcDGh5lw8RYQj5OBAuA=="], + "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -1166,6 +1257,8 @@ "agentation": ["agentation@3.1.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-ZZr6PRgX3ussGbvC9NVh5vFfKKFkQyHL6KuxOfBAAM/mJS3bM2XyhZ7wLPakDwU3pqkzq7u6vEfLikFsCOu5+Q=="], + "ai": ["ai@7.0.154", "", { "dependencies": { "@ai-sdk/gateway": "4.0.127", "@ai-sdk/provider": "4.0.34", "@ai-sdk/provider-utils": "5.0.69" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-vpvOZoV2Xcu6vWYM/X2XAFJG93T/ELF+qbslXxRnpqqhKZ6dtxfZf1KHFqYBz784uV/XVjXkNjgs9Zc239NY3Q=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -1174,7 +1267,7 @@ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], @@ -1238,6 +1331,8 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], "camera-controls": ["camera-controls@3.1.2", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA=="], @@ -1246,7 +1341,9 @@ "case-anything": ["case-anything@2.1.13", "", {}, "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], @@ -1258,6 +1355,8 @@ "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "cli-truncate": ["cli-truncate@6.1.1", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -1274,7 +1373,7 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -1346,6 +1445,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.464", "", {}, "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="], "enhanced-resolve": ["enhanced-resolve@5.26.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="], @@ -1570,7 +1671,7 @@ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], @@ -1624,10 +1725,16 @@ "iwer": ["iwer@2.3.0", "", { "dependencies": { "gl-matrix": "^3.4.4", "webxr-layers-polyfill": "^1.1.0" } }, "sha512-+aY/BXVIjztCtS4F1hAO2rQy0P1/0JbJ6Jq5QHVscUBBlv8xJlTkkyeg+uSiD9RQ7i5B6k1Blpcx84FQaI7Myw=="], + "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "js-sha256": ["js-sha256@0.10.1", "", {}, "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], @@ -1636,6 +1743,8 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -1658,6 +1767,8 @@ "ktx-parse": ["ktx-parse@1.1.0", "", {}, "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], @@ -1686,6 +1797,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@3.1.4", "", {}, "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w=="], + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -1734,6 +1847,8 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "moo": ["moo@0.5.3", "", {}, "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA=="], + "motion": ["motion@14.6.0", "", { "dependencies": { "framer-motion": "14.6.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-qqaZbyubgk7x3zxjPgP1l3Br/KQXol/IIV1uqb358SoN4ApcqNuV7+wyDPg/XLErmuyQL5lL8JVI/fOmDsmBKQ=="], "motion-dom": ["motion-dom@14.6.0", "", { "dependencies": { "motion-utils": "14.6.0" } }, "sha512-lIojTl9aYSPIkeOOSzBliy1nNvpdO3JtVzW2WgTXQlOP4ZMuJhtWO3fP7OD4TVFwlRucdvxlDSMkL+fEQnQ52A=="], @@ -1762,6 +1877,8 @@ "node-releases": ["node-releases@2.0.60", "", {}, "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "number-flow": ["number-flow@0.6.2", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw=="], @@ -1826,7 +1943,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], @@ -1836,6 +1953,8 @@ "png-js": ["png-js@2.0.0", "", { "dependencies": { "fflate": "^0.8.2" } }, "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA=="], + "pofile-ts": ["pofile-ts@4.0.3", "", {}, "sha512-sz1pnjgEfPyZ+QvaeX3NtCmbYnEvG01LZRLoN/uXoLtPZtxCIH5IctL7yXXc0fFyk/fqV6K8g3hlNfr6IJwupA=="], + "polygon-clipping": ["polygon-clipping@0.15.7", "", { "dependencies": { "robust-predicates": "^3.0.2", "splaytree": "^3.1.0" } }, "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1850,6 +1969,8 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="], "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], @@ -1862,6 +1983,8 @@ "protocol-buffers-schema": ["protocol-buffers-schema@3.6.1", "", {}, "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ=="], + "pseudolocale": ["pseudolocale@3.1.0", "", { "dependencies": { "commander": "^13.1.0" }, "bin": { "pseudolocale": "dist/cli.mjs" } }, "sha512-jBidehQu5WO8zbFUod0ETJYoVPxZ0ixsdI9B8ilkE0eA+YTydxHA7ujNdU4YhTexD5yH5Griv9r5CuXx+nwFUA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="], @@ -1892,6 +2015,8 @@ "react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="], + "readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], @@ -1954,6 +2079,8 @@ "slice-ansi": ["slice-ansi@9.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "splaytree": ["splaytree@3.2.3", "", {}, "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w=="], @@ -2024,6 +2151,8 @@ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="], + "tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -2074,6 +2203,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], @@ -2160,6 +2291,8 @@ "@iwer/devui/@pmndrs/pointer-events": ["@pmndrs/pointer-events@6.6.30", "", {}, "sha512-YD2jWdgEqqAWJNOOZ1WZMunUby8jwuQyOMk8zbeqC39R4nkZnGvu1Pa5EMGbV1zd2vZTxXDxYcAVxtQuhVwf3g=="], + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], @@ -2188,8 +2321,6 @@ "@pmndrs/xr/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - "@react-grab/cli/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "@react-grab/cli/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@react-grab/cli/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -2202,6 +2333,8 @@ "@repo/ui/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "@rollup/pluginutils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -2216,15 +2349,13 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "agent-install/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "agent-install/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001819", "", {}, "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg=="], - "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cmdk/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], @@ -2252,10 +2383,14 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "fdir/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "fontkit/@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "ifc-converter-app/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], "log-symbols/yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], @@ -2266,16 +2401,12 @@ "maplibre-gl/potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "ora/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], "oxlint-plugin-react-doctor/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], @@ -2284,6 +2415,12 @@ "postcss/nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "pseudolocale/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="], "react-doctor/agent-install": ["agent-install@0.0.5", "", { "dependencies": { "@iarna/toml": "^2.2.5", "commander": "^14.0.0", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "yaml": "^2.8.3" }, "bin": { "agent-install": "bin/agent-install.mjs" } }, "sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ=="], @@ -2292,12 +2429,22 @@ "react-scan/bippy": ["bippy@0.5.41", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-jCP2pXXLhXqPrAN+iSEFZmLI4uUM4fjSqajh0K+TmM062VehfDT3ZJNkrTGyN701Z5XMejs9qAudSqkMGhSMKg=="], - "react-scan/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "three-stdlib/fflate": ["fflate@0.6.11", "", {}, "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg=="], + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], + "ultracite/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + + "unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], @@ -2310,6 +2457,8 @@ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cmdk/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], @@ -2326,6 +2475,8 @@ "eslint-plugin-react/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "eslint-plugin-react/eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "eslint-plugin-react/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-plugin-react/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -2360,12 +2511,12 @@ "oxlint-plugin-react-doctor/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "react-doctor/agent-install/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "react-doctor/agent-install/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "eslint-plugin-react/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "eslint-plugin-react/eslint/file-entry-cache/flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], diff --git a/i18n/README.md b/i18n/README.md index 55549d1532..d19f78fcbb 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -1,61 +1,121 @@ # Localization -English UI copy stays beside the component that renders it: +> The localization API is experimental and unreleased. Its signatures may +> change before the first i18n-capable package release. + +Pascal uses Lingui 6 for ICU MessageFormat, React updates, extraction, and +catalog compilation. A thin Pascal registry keeps the ownership contract that +an embeddable editor needs: namespaced host and plugin packs, ordered +overrides, locale fallback, isolated request/editor scopes, and locale-aware +number formatting. + +Published packages use Lingui’s plain runtime call form, not macros: ```tsx -const t = useTranslate() -t('viewer.camera.topView', 'Top view') +const i18n = usePascalI18n() + +i18n._({ + id: 'viewer.camera.topView', + message: 'Top view', +}) ``` -The in-code text is the fallback and source of truth. Scene data, exports, -catalog content, MCP/agent text, and developer errors are not translated. +Hosts therefore need no Lingui compiler plugin or extra transform. English UI +copy stays beside the component that renders it and is also compiled into the +bundled English catalog. Scene data, exports, catalog content, MCP/agent text, +and developer errors are not translated. + +## Catalogs -## Add or adapt a language +The source and translated catalogs are PO files under +`packages/editor/src/i18n/locales//messages.po`. Compiled TypeScript +catalogs next to them are what the browser loads; PO files and the ICU compiler +are not part of the normal rendering path. -1. Run `bun run i18n:extract` after adding or changing `t()` calls. -2. Add a catalog in `packages/editor/src/i18n/catalogs/.json`, or run - `bun run i18n:translate -- ` for an existing bundled locale. -3. Review every translation in context. The AI command only prepares a diff; - it never runs in the app or CI. -4. Add the locale to `packages/editor/src/i18n/locales.ts` when it should ship - with the editor. -5. Run `bun run i18n:check`. +After adding or changing a runtime message: -Each translation records the hash of its English source. Changing the English -message makes the translation stale until a person updates or approves it. -`i18n:check` also rejects missing keys and changed placeholders. +1. Run `bun run i18n:extract`. Lingui updates the PO catalogs and compiled + modules. +2. Translate every new entry in each bundled locale. +3. Keep the `Source-Hash` comment in sync and mark a reviewed entry + `Review: human` without the `fuzzy` flag. +4. Review the copy in context, then run `bun run i18n:check`. -To adapt a bundled pack without forking, register an override after it: +`i18n:check` needs no API key. It fails for extraction/compiled-catalog drift, +missing or stale translations, unreviewed entries, malformed ICU, changed +placeholders or tags, invalid target-locale plural branches, and glossary +violations. It only warns when a new literal bypasses Lingui on the converted +viewer/build-panel surfaces. + +`bun run i18n:compile` runs Lingui’s strict compiler directly when only the +generated runtime catalogs need refreshing. + +To bundle another language, add its locale to `lingui.config.ts` and +`packages/editor/src/i18n/locales.ts`, extract, translate, compile, and add it +to the locale-pack test. + +## Hosts, plugins, and overrides + +Keep namespaces with their owner: `core.*`, `viewer.*`, `editor.*`, +`node..*`, or `plugin..*`. Raw community and host packs use ICU +strings and are compiled atomically through the lazy authoring path: ```ts import { registerMessages, setLocale } from '@pascal-app/core/i18n' import { registerBundledLocales } from '@pascal-app/editor/locales' registerBundledLocales() -registerMessages('fr', 'viewer', { + +await registerMessages('fr', 'viewer', { 'camera.topView': 'Vue en plan', }) +await registerMessages('en', 'viewer', { + 'camera.topView': 'Plan view', +}) + setLocale('fr') ``` -Later registrations win. Regional fallback is automatic (`pt-BR` → `pt` → -the English literal). An embedding host can set the locale before mount, pass -``, or use `?lang=fr` in the standalone app and iframe. -Calling `setLocale` later switches the mounted UI. +Later registrations win, including English overrides. Regional fallback is +automatic (`pt-BR` → `pt` → the English literal), while Simplified Chinese +script tags can use the `zh-CN` pack without crossing into Traditional Chinese. + +Pack authors that already compile with Lingui may call the synchronous +`registerCompiledMessages(locale, namespace, messages)` instead. Raw +`registerMessages` is intentionally asynchronous because it loads the ICU +compiler only for that authoring/host path. -Community packs use the same `registerMessages(locale, namespace, messages)` -contract. Keep namespaces with their owner: `core.*`, `viewer.*`, `editor.*`, -`node..*`, or `plugin..*`. +For server rendering, create one isolated scope per request and render the same +scope on the client: + +```tsx +const i18n = createI18n({ locale: requestedLocale }) + +return {children} +``` + +The standalone app resolves `?lang=` in its request proxy, so localized text +and numbers are present in the server response and match hydration. +`` creates the same kind of isolated scope for embeds. +Unscoped editors follow the default `setLocale`; scoped hosts switch at runtime +with `i18n.setLocale(...)`. ## AI-assisted fill -`bun run i18n:translate -- fr` refreshes only missing or stale entries. Set: +`bun run i18n:translate -- fr` sends only missing or source-stale PO entries +through the Vercel AI SDK. Configure one provider and model: - `I18N_PROVIDER=openai`, `OPENAI_API_KEY`, and `I18N_MODEL`; or - `I18N_PROVIDER=anthropic`, `ANTHROPIC_API_KEY`, and `I18N_MODEL`. -`OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` are optional. The command sends the -English copy, source context, and `i18n/glossary.json` to the selected provider, -validates placeholders, and writes JSON only. API keys are never required by -`i18n:check` and must not be committed. Human review remains mandatory, -especially for architecture/CAD terminology and short controls. +`OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` are optional. The structured-output +request includes the English ICU source, source locations, and +`i18n/glossary.json`. Every returned ID, ICU message, placeholder, tag, plural +branch, and applicable glossary term is validated before any PO file is +written. + +AI output is always marked `fuzzy`, `Review: required`, and records provider, +model, and review notes. It never becomes a shipping translation until a human +reviews it, removes `fuzzy`, and changes the review marker to `human`. +Translation never runs in the app or ordinary CI, and API keys must not be +committed. diff --git a/lingui.config.ts b/lingui.config.ts new file mode 100644 index 0000000000..9097895c79 --- /dev/null +++ b/lingui.config.ts @@ -0,0 +1,30 @@ +import type { LinguiConfig } from '@lingui/conf' +import { formatter } from '@lingui/format-po' + +const config: LinguiConfig = { + catalogs: [ + { + path: 'packages/editor/src/i18n/locales/{locale}/messages', + include: [ + 'packages/core/src', + 'packages/viewer/src', + 'packages/editor/src', + 'packages/nodes/src', + 'apps/editor', + ], + exclude: ['**/*.test.{ts,tsx}', '**/__fixtures__/**'], + }, + ], + compileNamespace: 'es', + fallbackLocales: false, + format: formatter({ + explicitIdAsDefault: true, + lineNumbers: true, + origins: true, + }), + locales: ['en', 'zh-CN', 'fr', 'es', 'ja'], + orderBy: 'messageId', + sourceLocale: 'en', +} + +export default config diff --git a/package.json b/package.json index 306dda9347..4e139d543a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "check:fix": "biome check --write", "check-types": "turbo run check-types", "i18n:check": "bun scripts/i18n.ts check", + "i18n:compile": "bun scripts/i18n.ts compile", "i18n:extract": "bun scripts/i18n.ts extract", "i18n:translate": "bun scripts/i18n.ts translate", "test": "turbo run test", @@ -29,14 +30,23 @@ "release:major": "gh workflow run release.yml -f bump=major" }, "devDependencies": { + "@ai-sdk/anthropic": "^4.0.89", + "@ai-sdk/openai": "^4.0.103", "@biomejs/biome": "^2.6.3", + "@lingui/cli": "^6.10.1", + "@lingui/conf": "6.10.1", + "@lingui/format-po": "^6.10.1", + "@lingui/message-utils": "6.10.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "@typescript/typescript6": "6.0.2", + "ai": "^7.0.154", "dotenv-cli": "^11.0.0", "fast-xml-parser": "^5.11.2", + "pofile-ts": "^4.0.3", "turbo": "^2.11.10", "typescript": "npm:@typescript/typescript6@6.0.2", - "ultracite": "^8.1.0" + "ultracite": "^8.1.0", + "zod": "^4.6.7" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/packages/core/package.json b/packages/core/package.json index c3aaf4f8f4..c35cb46613 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -158,6 +158,9 @@ "three": "^0.186" }, "dependencies": { + "@lingui/core": "^6.10.1", + "@lingui/message-utils": "^6.10.1", + "@lingui/react": "^6.10.1", "@pascal-app/lingo": "^0.5.0", "dedent": "^1.7.2", "idb-keyval": "^6.3.0", diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 1bed5a4ede..4d5cf7e483 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -2,10 +2,12 @@ import { afterEach, describe, expect, spyOn, test } from 'bun:test' import { renderToString } from 'react-dom/server' import { BuildingNode, + createI18n, formatLength, formatNumber, LevelNode, levelDisplayNumber, + PascalI18nProvider, parseLocalizedNumber, registerMessages, SiteNode, @@ -13,7 +15,7 @@ import { subscribe, translate, useLocale, - useTranslate, + usePascalI18n, } from '../index' afterEach(() => { @@ -21,13 +23,13 @@ afterEach(() => { }) describe('message registry', () => { - test('merges host overrides and falls back from a regional locale to its language', () => { - registerMessages('pt', 'viewer', { + test('merges host overrides and falls back from a regional locale to its language', async () => { + await registerMessages('pt', 'viewer', { 'controls.open': 'Abrir', 'controls.close': 'Fechar', }) - registerMessages('pt-BR', 'viewer', { 'controls.close': 'Encerrar' }) - registerMessages('pt-BR', 'viewer', { 'controls.close': 'Fechar agora' }) + await registerMessages('pt-BR', 'viewer', { 'controls.close': 'Encerrar' }) + await registerMessages('pt-BR', 'viewer', { 'controls.close': 'Fechar agora' }) setLocale('pt-BR') @@ -36,16 +38,16 @@ describe('message registry', () => { expect(translate('viewer.controls.unknown', 'Unknown')).toBe('Unknown') }) - test('falls back through script before the base language', () => { - registerMessages('zh', 'viewer', { greeting: '你好' }) - registerMessages('zh-Hant', 'viewer', { greeting: '您好' }) + test('falls back through script before the base language', async () => { + await registerMessages('zh', 'viewer', { greeting: '你好' }) + await registerMessages('zh-Hant', 'viewer', { greeting: '您好' }) setLocale('zh-Hant-HK') expect(translate('viewer.greeting', 'Hello')).toBe('您好') }) - test('maps explicit Simplified Chinese scripts to zh-CN without crossing into Traditional', () => { - registerMessages('zh-CN', 'viewer', { 'test.simplified-alias': '简体中文' }) + test('maps explicit Simplified Chinese scripts to zh-CN without crossing into Traditional', async () => { + await registerMessages('zh-CN', 'viewer', { 'test.simplified-alias': '简体中文' }) expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-Hans')).toBe( '简体中文', @@ -59,58 +61,74 @@ describe('message registry', () => { expect(translate('viewer.test.simplified-alias', 'English', undefined, 'zh-TW')).toBe('English') }) - test('keeps canonical English in code', () => { - registerMessages('en', 'viewer', { hostGreeting: 'Welcome' }) + test('allows later English registrations to override the in-code default', async () => { + await registerMessages('en', 'viewer', { hostGreeting: 'Welcome' }) - expect(translate('viewer.hostGreeting', 'Hello')).toBe('Hello') + expect(translate('viewer.hostGreeting', 'Hello')).toBe('Welcome') expect(translate('viewer.unregistered', 'In-code fallback')).toBe('In-code fallback') }) - test('interpolates values and selects plurals with Intl.PluralRules', () => { - registerMessages('fr', 'editor', { - selection: { - one: '{count} élément sélectionné', - other: '{count} éléments sélectionnés', - }, + test('interpolates values and selects ICU plurals', async () => { + await registerMessages('fr', 'editor', { + selection: '{count, plural, one {# élément sélectionné} other {# éléments sélectionnés}}', welcome: 'Bonjour {name}', }) setLocale('fr') expect( - translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 1 }), + translate('editor.selection', '{count, plural, one {# item} other {# items}}', { count: 1 }), ).toBe('1 élément sélectionné') expect( - translate('editor.selection', { one: '{count} item', other: '{count} items' }, { count: 3 }), + translate('editor.selection', '{count, plural, one {# item} other {# items}}', { count: 3 }), ).toBe('3 éléments sélectionnés') expect(translate('editor.welcome', 'Hello {name}', { name: 'Ada' })).toBe('Bonjour Ada') }) - test('uses English plural rules when a translation is missing', () => { + test('uses English plural rules when a translation is missing', async () => { + await registerMessages('en', 'viewer', { + 'test.english-plural-fallback': + '{count, plural, one {# fallback item} other {# fallback items}}', + }) setLocale('ru') expect( translate( 'viewer.test.english-plural-fallback', - { one: '{count} item', other: '{count} items' }, + '{count, plural, one {# item} other {# items}}', { count: 21 }, ), - ).toBe('21 items') + ).toBe('21 fallback items') }) - test('notifies subscribers for locale and active-catalog changes', () => { + test('notifies subscribers for locale and active-catalog changes', async () => { let changes = 0 const unsubscribe = subscribe(() => { changes += 1 }) setLocale('ja') - registerMessages('ja', 'viewer', { test: 'テスト' }) + await registerMessages('ja', 'viewer', { test: 'テスト' }) unsubscribe() setLocale('es') expect(changes).toBe(2) }) + test('keeps request-local scopes isolated', async () => { + await registerMessages('fr', 'viewer', { isolated: 'Français' }) + await registerMessages('ja', 'viewer', { isolated: '日本語' }) + const french = createI18n({ locale: 'fr' }) + const japanese = createI18n({ locale: 'ja' }) + + expect(french._({ id: 'viewer.isolated', message: 'English' })).toBe('Français') + expect(japanese._({ id: 'viewer.isolated', message: 'English' })).toBe('日本語') + french.setLocale('en') + expect(japanese.getLocale()).toBe('ja') + + french.destroy() + japanese.destroy() + }) + test('warns once in development and never exposes the missing key', () => { const previous = process.env.NODE_ENV process.env.NODE_ENV = 'development' @@ -129,12 +147,9 @@ describe('message registry', () => { describe('locale formatting', () => { test('formats numbers and lengths without coupling locale to the unit', () => { - setLocale('fr') expect(formatNumber(2.75, { locale: 'fr', max: 2 })).toBe('2,75') expect(formatLength(2.75, 'm', { locale: 'fr' })).toBe('2,75\u00a0m') expect(formatLength(2.75, 'mm', { locale: 'fr' })).toBe('2750\u00a0mm') - - setLocale('en') expect(formatLength(2.75, 'm', { locale: 'en' })).toBe('2.75\u00a0m') }) @@ -156,32 +171,29 @@ describe('locale formatting', () => { }) }) -test('useTranslate renders the English snapshot on the server', () => { - registerMessages('ja', 'viewer', { greeting: 'こんにちは' }) - setLocale('ja') +test('server rendering uses the selected locale for text and numbers', async () => { + await registerMessages('fr', 'viewer', { greeting: 'Bonjour' }) + const scope = createI18n({ locale: 'fr' }) function Greeting() { - const t = useTranslate() - return {t('viewer.greeting', 'Hello')} - } - - expect(renderToString()).toContain('Hello') -}) - -test('rendered number formatting defaults to English and can use the hydration-safe snapshot', () => { - setLocale('fr') - - function Measurement() { + const i18n = usePascalI18n() const locale = useLocale() return ( - <> - {formatNumber(2.75, { max: 2 })} - {formatNumber(2.75, { locale, max: 2 })} - + + {i18n._({ id: 'viewer.greeting', message: 'Hello' })}: + {formatNumber(2.75, { locale, max: 2 })} + ) } - expect(renderToString()).not.toContain('2,75') + const serverHtml = renderToString( + + + , + ) + + expect(serverHtml).toContain('Bonjour:2,75') + scope.destroy() }) test('building and serializing a scene is locale-neutral', () => { diff --git a/packages/core/src/i18n/index.ts b/packages/core/src/i18n/index.ts index 0253c4ac90..82f51409a7 100644 --- a/packages/core/src/i18n/index.ts +++ b/packages/core/src/i18n/index.ts @@ -8,8 +8,12 @@ export { parseLocalizedNumber, } from './format' export { + type CompiledTranslationMessages, + createI18n, + defaultI18n, getLocale, - type PluralMessage, + type PascalI18n, + registerCompiledMessages, registerMessages, setLocale, subscribe, @@ -18,4 +22,4 @@ export { type TranslationParams, translate, } from './registry' -export { type Translate, useLocale, useTranslate } from './use-translate' +export { PascalI18nProvider, useLocale, usePascalI18n } from './use-translate' diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index fba2c320a8..046a32a900 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -1,24 +1,25 @@ +import { + type I18n as LinguiI18n, + type MessageDescriptor, + type MessageOptions, + type Messages, + setupI18n, + type Values, +} from '@lingui/core' + +/** @experimental Unreleased API; signatures may change before the first i18n release. */ export type TranslationParams = Record - -export type PluralMessage = { - zero?: string - one?: string - two?: string - few?: string - many?: string - other: string -} - -export type TranslationMessage = string | PluralMessage +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export type TranslationMessage = string +/** @experimental Unreleased API; signatures may change before the first i18n release. */ export type TranslationMessages = Record +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export type CompiledTranslationMessages = Messages -const catalogs = new Map>() -const listeners = new Set<() => void>() +const catalogs = new Map() +const scopes = new Set() const warnedMissingKeys = new Set() -let locale = 'en' -let version = 0 - function canonicalLocale(value: string): string { const trimmed = value.trim() if (!trimmed) return 'en' @@ -54,106 +55,195 @@ function isDevelopment(): boolean { return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development' } -function emitChange(): void { - version += 1 - for (const listener of [...listeners]) listener() -} - -export function registerMessages( - messageLocale: string, - namespace: string, - messages: TranslationMessages, -): void { - const normalizedLocale = canonicalLocale(messageLocale) +function namespacedMessages(namespace: string, messages: Messages): Messages { const normalizedNamespace = namespace.replace(/^\.+|\.+$/g, '') - const catalog = catalogs.get(normalizedLocale) ?? new Map() + return Object.fromEntries( + Object.entries(messages).map(([key, message]) => { + const normalizedKey = key.replace(/^\.+/g, '') + return [ + normalizedNamespace ? `${normalizedNamespace}.${normalizedKey}` : normalizedKey, + message, + ] + }), + ) +} - for (const [key, message] of Object.entries(messages)) { - const normalizedKey = key.replace(/^\.+/g, '') - const fullKey = normalizedNamespace ? `${normalizedNamespace}.${normalizedKey}` : normalizedKey - catalog.set(fullKey, message) +function mergedMessages(requestedLocale: string): Messages { + const messages: Messages = {} + for (const candidate of localeFallbacks(requestedLocale).reverse()) { + Object.assign(messages, catalogs.get(candidate)) } - - catalogs.set(normalizedLocale, catalog) - emitChange() + return messages } -export function setLocale(nextLocale: string): void { - const normalized = canonicalLocale(nextLocale) - if (normalized === locale) return - locale = normalized - emitChange() +function warnMissing(key: string, requestedLocale: string): void { + if (!isDevelopment() || requestedLocale === 'en') return + const warningId = `${requestedLocale}:${key}` + if (warnedMissingKeys.has(warningId)) return + warnedMissingKeys.add(warningId) + console.warn(`[i18n] Missing "${key}" for locale "${requestedLocale}"; using English.`) } -export function getLocale(): string { - return locale -} +/** + * A request- or editor-local locale selection over Pascal's shared message + * registry. Message parsing, interpolation, and ICU selection are delegated to + * private Lingui instances. + * + * @experimental Unreleased API; signatures may change before the first i18n release. + */ +export class PascalI18n { + readonly i18n: LinguiI18n + private readonly englishI18n: LinguiI18n + private readonly listeners = new Set<() => void>() + private locale: string + private version = 0 + + constructor(initialLocale = 'en') { + this.locale = canonicalLocale(initialLocale) + this.i18n = setupI18n() + this.englishI18n = setupI18n() + scopes.add(this) + this.refresh(false) + } -export function subscribe(listener: () => void): () => void { - listeners.add(listener) - return () => { - listeners.delete(listener) + getLocale(): string { + return this.locale + } + + getVersion(): number { + return this.version + } + + setLocale(nextLocale: string): void { + const normalized = canonicalLocale(nextLocale) + if (normalized === this.locale) return + this.locale = normalized + this.refresh() + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } + + destroy(): void { + scopes.delete(this) + this.listeners.clear() + } + + _(descriptor: MessageDescriptor): string + _(id: string, values?: Values, options?: MessageOptions): string + _(idOrDescriptor: string | MessageDescriptor, values?: Values, options?: MessageOptions): string { + const id = typeof idOrDescriptor === 'string' ? idOrDescriptor : idOrDescriptor.id + const translated = + this.locale === 'en' || Object.prototype.hasOwnProperty.call(this.i18n.messages, id) + const runtime = translated ? this.i18n : this.englishI18n + if (!translated) warnMissing(id, this.locale) + return typeof idOrDescriptor === 'string' + ? runtime._(idOrDescriptor, values, options) + : runtime._(idOrDescriptor) + } + + refresh(notify = true): void { + this.i18n.loadAndActivate({ + locale: this.locale, + messages: mergedMessages(this.locale), + }) + this.englishI18n.loadAndActivate({ + locale: 'en', + messages: mergedMessages('en'), + }) + if (!notify) return + this.version += 1 + for (const listener of [...this.listeners]) listener() } } -export function getI18nVersion(): number { - return version +/** + * Create an isolated locale scope backed by the shared Pascal message packs. + * + * @experimental Unreleased API; signatures may change before the first i18n release. + */ +export function createI18n(options: { locale?: string } = {}): PascalI18n { + return new PascalI18n(options.locale) } -function messageFor(key: string, requestedLocale: string): TranslationMessage | undefined { - for (const candidate of localeFallbacks(requestedLocale)) { - const message = catalogs.get(candidate)?.get(key) - if (message !== undefined) return message - } - return undefined +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export const defaultI18n = createI18n() + +/** + * Register a raw ICU MessageFormat pack. The Lingui compiler is loaded only + * for this authoring/host path; bundled packs use registerCompiledMessages. + * The whole pack is validated before it becomes visible. + * + * @experimental Unreleased API; signatures may change before the first i18n release. + */ +export async function registerMessages( + messageLocale: string, + namespace: string, + messages: TranslationMessages, +): Promise { + const { compileMessageOrThrow } = await import('@lingui/message-utils/compileMessage') + const compiled = Object.fromEntries( + Object.entries(messages).map(([key, message]) => [key, compileMessageOrThrow(message)]), + ) + registerCompiledMessages(messageLocale, namespace, compiled) } -function warnMissing(key: string, requestedLocale: string): void { - if (!isDevelopment() || requestedLocale === 'en') return - const warningId = `${requestedLocale}:${key}` - if (warnedMissingKeys.has(warningId)) return - warnedMissingKeys.add(warningId) - console.warn(`[i18n] Missing "${key}" for locale "${requestedLocale}"; using English.`) +/** + * Register a catalog produced by `lingui compile`. Later registrations replace + * earlier messages with the same fully namespaced ID. + * + * @experimental Unreleased API; signatures may change before the first i18n release. + */ +export function registerCompiledMessages( + messageLocale: string, + namespace: string, + messages: CompiledTranslationMessages, +): void { + const normalizedLocale = canonicalLocale(messageLocale) + const catalog = catalogs.get(normalizedLocale) ?? {} + Object.assign(catalog, namespacedMessages(namespace, messages)) + catalogs.set(normalizedLocale, catalog) + for (const scope of [...scopes]) scope.refresh() } -function selectPlural( - message: TranslationMessage, - count: number | undefined, - requestedLocale: string, -): string { - if (typeof message === 'string') return message - if (count === undefined) return message.other +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function setLocale(nextLocale: string): void { + defaultI18n.setLocale(nextLocale) +} - let category: Intl.LDMLPluralRule = 'other' - try { - category = new Intl.PluralRules(requestedLocale).select(count) - } catch { - category = new Intl.PluralRules('en').select(count) - } +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function getLocale(): string { + return defaultI18n.getLocale() +} - const translated = message[category] ?? message.other - return translated +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function subscribe(listener: () => void): () => void { + return defaultI18n.subscribe(listener) } -function interpolate(message: string, params: TranslationParams | undefined): string { - if (!params) return message - return message.replace(/\{([A-Za-z_][\w.-]*)\}/g, (placeholder, name: string) => { - const value = params[name] - return value === undefined ? placeholder : String(value) - }) +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function getI18nVersion(): number { + return defaultI18n.getVersion() } +/** @experimental Unreleased API; signatures may change before the first i18n release. */ export function translate( key: string, englishDefault: TranslationMessage, params?: TranslationParams, - requestedLocale = locale, + requestedLocale = getLocale(), ): string { - const normalizedLocale = canonicalLocale(requestedLocale) - const translated = normalizedLocale === 'en' ? undefined : messageFor(key, normalizedLocale) - if (translated === undefined) warnMissing(key, normalizedLocale) - const message = translated ?? englishDefault - const count = typeof params?.count === 'number' ? params.count : undefined - const pluralLocale = translated === undefined ? 'en' : normalizedLocale - return interpolate(selectPlural(message, count, pluralLocale), params) + const scope = + canonicalLocale(requestedLocale) === defaultI18n.getLocale() + ? defaultI18n + : createI18n({ locale: requestedLocale }) + try { + return scope._({ id: key, message: englishDefault, values: params }) + } finally { + if (scope !== defaultI18n) scope.destroy() + } } diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts index 9140df218e..2b0452eadd 100644 --- a/packages/core/src/i18n/use-translate.ts +++ b/packages/core/src/i18n/use-translate.ts @@ -1,38 +1,42 @@ 'use client' -import { useCallback, useSyncExternalStore } from 'react' +import { I18nProvider as LinguiI18nProvider } from '@lingui/react' import { - getI18nVersion, - getLocale, - subscribe, - type TranslationMessage, - type TranslationParams, - translate, -} from './registry' + createContext, + createElement, + type ReactNode, + useContext, + useSyncExternalStore, +} from 'react' +import { defaultI18n, type PascalI18n } from './registry' -const SERVER_SNAPSHOT = -1 - -export type Translate = ( - key: string, - englishDefault: TranslationMessage, - params?: TranslationParams, -) => string - -export function useLocale(): string { - const version = useSyncExternalStore(subscribe, getI18nVersion, () => SERVER_SNAPSHOT) - return version === SERVER_SNAPSHOT ? 'en' : getLocale() -} +const PascalI18nContext = createContext(defaultI18n) /** - * The server and hydration snapshot always uses the in-code English message. - * Once the client subscribes, the active locale is read from the registry and - * locale or message registrations re-render the component. + * Bind a request- or editor-local Pascal scope to React and Lingui. + * + * @experimental Unreleased API; signatures may change before the first i18n release. */ -export function useTranslate(): Translate { - const locale = useLocale() +export function PascalI18nProvider({ children, i18n }: { children: ReactNode; i18n: PascalI18n }) { + return createElement( + PascalI18nContext.Provider, + { value: i18n }, + createElement(LinguiI18nProvider, { i18n: i18n.i18n }, children), + ) +} - return useCallback( - (key, englishDefault, params) => translate(key, englishDefault, params, locale), - [locale], +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function usePascalI18n(): PascalI18n { + const i18n = useContext(PascalI18nContext) + useSyncExternalStore( + i18n.subscribe.bind(i18n), + i18n.getVersion.bind(i18n), + i18n.getVersion.bind(i18n), ) + return i18n +} + +/** @experimental Unreleased API; signatures may change before the first i18n release. */ +export function useLocale(): string { + return usePascalI18n().getLocale() } diff --git a/packages/editor/package.json b/packages/editor/package.json index 91915cb8ca..97bf5e8909 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -31,6 +31,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.3", + "@lingui/core": "6.10.1", "@number-flow/react": "^0.6.2", "@pascal-app/geometry-script": "^1.0.6", "@pascal-app/ifc-converter": "^1.0.6", diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index de89493285..d6500dccc5 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -3,14 +3,14 @@ import { Icon } from '@iconify/react' import { acquireSceneReadOnlyLease, + createI18n, getCatalogMaterialById, getLibraryMaterialIdFromRef, getSceneMaterialIdFromRef, initSpaceDetectionSync, initSpatialGridSync, - setLocale, + PascalI18nProvider, spatialGridManager, - useLocale, useScene, } from '@pascal-app/core' import { @@ -34,6 +34,7 @@ import { useCallback, useEffect, useLayoutEffect, + useMemo, useRef, useState, } from 'react' @@ -1964,16 +1965,29 @@ function EditorContent({ ) } -export default function Editor(props: EditorProps) { - useLocale() - - useClientLayoutEffect(() => { - if (props.locale) setLocale(props.locale) - }, [props.locale]) - +function EditorFrame(props: EditorProps) { return ( ) } + +function LocalizedEditor(props: EditorProps & { locale: string }) { + const i18n = useMemo(() => createI18n({ locale: props.locale }), [props.locale]) + useEffect(() => () => i18n.destroy(), [i18n]) + + return ( + + + + ) +} + +export default function Editor(props: EditorProps) { + return props.locale ? ( + + ) : ( + + ) +} diff --git a/packages/editor/src/components/ui/build-panel/build-panel.tsx b/packages/editor/src/components/ui/build-panel/build-panel.tsx index ae8d20f484..4a6c3f77ed 100644 --- a/packages/editor/src/components/ui/build-panel/build-panel.tsx +++ b/packages/editor/src/components/ui/build-panel/build-panel.tsx @@ -1,6 +1,6 @@ 'use client' -import { type Translate, useTranslate } from '@pascal-app/core' +import { type PascalI18n, usePascalI18n } from '@pascal-app/core' import { ChevronRight } from 'lucide-react' import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react' import { cn } from '../../../lib/utils' @@ -70,7 +70,7 @@ export function BuildPanelAdvancedSection({ description?: string hint?: string }) { - const t = useTranslate() + const i18n = usePascalI18n() const [open, setOpen] = useState(false) useEffect(() => setOpen(readAdvancedOpen()), []) useEffect(() => { @@ -96,7 +96,7 @@ export function BuildPanelAdvancedSection({ )} />

- {t('editor.buildPanel.advanced', 'Advanced')} + {i18n._({ id: 'editor.buildPanel.advanced', message: 'Advanced' })}

{hint ? ( {hint} @@ -228,18 +228,21 @@ export function BuildPanelRoomsSection({ /** Extra attributes per tile (e.g. a host's guide-target hook). */ tileProps?: (variant: WallDrawVariant) => Record }) { - const t = useTranslate() + const i18n = usePascalI18n() const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant) - const activeMessages = active ? roomVariantMessages(active.id, t) : null + const activeMessages = active ? roomVariantMessages(active.id, i18n) : null return ( {WALL_DRAW_VARIANTS.map((variant) => { - const messages = roomVariantMessages(variant.id, t) + const messages = roomVariantMessages(variant.id, i18n) return ( @@ -270,39 +274,57 @@ export function BuildPanelRoomsSection({ ) } -function roomVariantMessages(variant: WallDrawVariant, t: Translate) { +function roomVariantMessages(variant: WallDrawVariant, i18n: PascalI18n) { if (variant === 'rectangle') { return { - label: t('editor.buildPanel.rooms.rectangle.label', 'Rectangle'), - title: t('editor.buildPanel.rooms.rectangle.title', 'Rectangle room'), - lead: t('editor.buildPanel.rooms.rectangle.lead', 'Click two corners'), - caption: t( - 'editor.buildPanel.rooms.rectangle.caption', - '{lead} on the grid. Walls, floor and ceiling are created together.', - { lead: CAPTION_LEAD_MARKER }, - ), + label: i18n._({ + id: 'editor.buildPanel.rooms.rectangle.label', + message: 'Rectangle', + }), + title: i18n._({ + id: 'editor.buildPanel.rooms.rectangle.title', + message: 'Rectangle room', + }), + lead: i18n._({ + id: 'editor.buildPanel.rooms.rectangle.lead', + message: 'Click two corners', + }), + caption: i18n._({ + id: 'editor.buildPanel.rooms.rectangle.caption', + message: '{lead} on the grid. Walls, floor and ceiling are created together.', + values: { lead: CAPTION_LEAD_MARKER }, + }), } } if (variant === 'walls') { return { - label: t('editor.buildPanel.rooms.walls.label', 'Walls'), - title: t('editor.buildPanel.rooms.walls.title', 'Walls'), - lead: t('editor.buildPanel.rooms.walls.lead', 'Draw one wall at a time.'), - caption: t( - 'editor.buildPanel.rooms.walls.caption', - '{lead} Close a loop and it becomes a room; leave it open for partitions.', - { lead: CAPTION_LEAD_MARKER }, - ), + label: i18n._({ id: 'editor.buildPanel.rooms.walls.label', message: 'Walls' }), + title: i18n._({ id: 'editor.buildPanel.rooms.walls.title', message: 'Walls' }), + lead: i18n._({ + id: 'editor.buildPanel.rooms.walls.lead', + message: 'Draw one wall at a time.', + }), + caption: i18n._({ + id: 'editor.buildPanel.rooms.walls.caption', + message: '{lead} Close a loop and it becomes a room; leave it open for partitions.', + values: { lead: CAPTION_LEAD_MARKER }, + }), } } return { - label: t('editor.buildPanel.rooms.polygon.label', 'Polygon'), - title: t('editor.buildPanel.rooms.polygon.title', 'Polygon room'), - lead: t('editor.buildPanel.rooms.polygon.lead', 'Click each corner,'), - caption: t( - 'editor.buildPanel.rooms.polygon.caption', - '{lead} then the first one again to close. For L-shapes and angled rooms.', - { lead: CAPTION_LEAD_MARKER }, - ), + label: i18n._({ id: 'editor.buildPanel.rooms.polygon.label', message: 'Polygon' }), + title: i18n._({ + id: 'editor.buildPanel.rooms.polygon.title', + message: 'Polygon room', + }), + lead: i18n._({ + id: 'editor.buildPanel.rooms.polygon.lead', + message: 'Click each corner,', + }), + caption: i18n._({ + id: 'editor.buildPanel.rooms.polygon.caption', + message: '{lead} then the first one again to close. For L-shapes and angled rooms.', + values: { lead: CAPTION_LEAD_MARKER }, + }), } } diff --git a/packages/editor/src/components/viewer/floorplan-compass-button.tsx b/packages/editor/src/components/viewer/floorplan-compass-button.tsx index 9bf29e709a..cc16511208 100644 --- a/packages/editor/src/components/viewer/floorplan-compass-button.tsx +++ b/packages/editor/src/components/viewer/floorplan-compass-button.tsx @@ -1,6 +1,6 @@ 'use client' -import { useTranslate } from '@pascal-app/core' +import { usePascalI18n } from '@pascal-app/core' import type React from 'react' import { cn } from '../../lib/utils' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip' @@ -19,8 +19,11 @@ export function FloorplanCompassButton({ needleRef, style, }: FloorplanCompassButtonProps) { - const t = useTranslate() - const label = t('viewer.floorplan.alignNorth', 'Align view to north') + const i18n = usePascalI18n() + const label = i18n._({ + id: 'viewer.floorplan.alignNorth', + message: 'Align view to north', + }) return ( diff --git a/packages/editor/src/components/viewer/floorplan-preview.tsx b/packages/editor/src/components/viewer/floorplan-preview.tsx index 2443c2107d..144755c29d 100644 --- a/packages/editor/src/components/viewer/floorplan-preview.tsx +++ b/packages/editor/src/components/viewer/floorplan-preview.tsx @@ -10,10 +10,10 @@ import { isNodeKindEnabled, levelDisplayNumber, nodeRegistry, - type Translate, + type PascalI18n, useLocale, + usePascalI18n, useScene, - useTranslate, } from '@pascal-app/core' import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema' import { useViewer } from '@pascal-app/viewer' @@ -147,7 +147,7 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox { } } -function levelLabel(level: AnyNode, t: Translate, locale: string): string { +function levelLabel(level: AnyNode, i18n: PascalI18n, locale: string): string { const levelNumber = (level as { level?: number }).level ?? 0 const levelName = (level as { name?: string }).name const english = getLevelDisplayName({ @@ -156,14 +156,22 @@ function levelLabel(level: AnyNode, t: Translate, locale: string): string { }) const authoredName = levelName?.trim() if (authoredName && authoredName !== `Level ${levelNumber}`) return authoredName - if (levelNumber === 0) return t('core.level.groundFloor', 'Ground floor') - if (levelNumber === -1) return t('core.level.basement', 'Basement') + if (levelNumber === 0) { + return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' }) + } + if (levelNumber === -1) return i18n._({ id: 'core.level.basement', message: 'Basement' }) if (levelNumber < -1) { - return t('core.level.basementNumber', 'Basement {number}', { number: -levelNumber }) + return i18n._({ + id: 'core.level.basementNumber', + message: 'Basement {number}', + values: { number: -levelNumber }, + }) } if (levelNumber > 0) { - return t('core.level.floorNumber', 'Floor {number}', { - number: levelDisplayNumber(levelNumber, locale), + return i18n._({ + id: 'core.level.floorNumber', + message: 'Floor {number}', + values: { number: levelDisplayNumber(levelNumber, locale) }, }) } return english @@ -328,7 +336,7 @@ export function FloorplanPreview({ showLevelSelector = true, synchronizeNavigation = false, }: FloorplanPreviewProps) { - const t = useTranslate() + const i18n = usePascalI18n() const locale = useLocale() const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes)) const storeInstalledPlugins = useScene((state) => @@ -890,7 +898,10 @@ export function FloorplanPreview({ className={className} style={{ display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }} > - {t('viewer.floorplan.empty', 'No floor plans are available for this scene.')} + {i18n._({ + id: 'viewer.floorplan.empty', + message: 'No floor plans are available for this scene.', + })}
) } @@ -903,10 +914,14 @@ export function FloorplanPreview({ {synchronizeNavigation ? : null}
- {t('viewer.floorplan.floor', 'Floor')} + {i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' })} @@ -1038,28 +1053,28 @@ export function FloorplanPreview({ }} > @@ -256,7 +264,7 @@ export const ViewerSceneHeader = ({ key={section.kind} > - {sectionTitle(section.kind, t)} + {sectionTitle(section.kind, i18n)} {section.rows.map((row) => { const active = diff --git a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx index 5dc4f39461..a7c7f3de81 100644 --- a/packages/editor/src/components/viewer/viewer-stage-switcher.tsx +++ b/packages/editor/src/components/viewer/viewer-stage-switcher.tsx @@ -1,6 +1,6 @@ 'use client' -import { useTranslate } from '@pascal-app/core' +import { usePascalI18n } from '@pascal-app/core' import { Box, Columns2, Map as MapIcon } from 'lucide-react' import type { ReactNode } from 'react' import { cn } from '../../lib/utils' @@ -23,12 +23,12 @@ export function ViewerStageSwitcher({ modes, onChange, }: ViewerStageSwitcherProps) { - const t = useTranslate() + const i18n = usePascalI18n() const enabledModes = normalizeViewerStageModes(modes) return (
1 ? 'hidden md:flex' : undefined} icon={} - label={t('viewer.stage.split', 'Split')} + label={i18n._({ id: 'viewer.stage.split', message: 'Split' })} onClick={() => onChange('split')} /> ) : null} diff --git a/packages/editor/src/i18n/catalogs/es.json b/packages/editor/src/i18n/catalogs/es.json deleted file mode 100644 index 28d9718994..0000000000 --- a/packages/editor/src/i18n/catalogs/es.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "locale": "es", - "messages": { - "core.level.basement": { - "message": "Sótano", - "sourceHash": "8caa959c0ae800c3" - }, - "core.level.basementNumber": { - "message": "Sótano {number}", - "sourceHash": "14927085ee91ba05" - }, - "core.level.floorNumber": { - "message": "Planta {number}", - "sourceHash": "e59074a7b9b3f56e" - }, - "core.level.groundFloor": { - "message": "Planta baja", - "sourceHash": "cde3efc9b7ddfa41" - }, - "editor.buildPanel.advanced": { - "message": "Avanzado", - "sourceHash": "f571cb2d06ae01ae" - }, - "editor.buildPanel.rooms.hint": { - "message": "muros · suelo · techo", - "sourceHash": "fb40866c2d6db5fb" - }, - "editor.buildPanel.rooms.idle": { - "message": "Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.", - "sourceHash": "271039bd919dc4c1" - }, - "editor.buildPanel.rooms.polygon.caption": { - "message": "{lead} y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.", - "sourceHash": "ae2800b6c9e2b6ed" - }, - "editor.buildPanel.rooms.polygon.label": { - "message": "Polígono", - "sourceHash": "4e186fc057733162" - }, - "editor.buildPanel.rooms.polygon.lead": { - "message": "Haz clic en cada esquina", - "sourceHash": "345e3aca3879b5c5" - }, - "editor.buildPanel.rooms.polygon.title": { - "message": "Estancia poligonal", - "sourceHash": "8ec6152e139b0e3f" - }, - "editor.buildPanel.rooms.rectangle.caption": { - "message": "{lead} en la cuadrícula. Los muros, el suelo y el techo se crean a la vez.", - "sourceHash": "9b46295eedcbedae" - }, - "editor.buildPanel.rooms.rectangle.label": { - "message": "Rectángulo", - "sourceHash": "2c2c20272b0370cc" - }, - "editor.buildPanel.rooms.rectangle.lead": { - "message": "Haz clic en dos esquinas", - "sourceHash": "060faa48e299d7c9" - }, - "editor.buildPanel.rooms.rectangle.title": { - "message": "Estancia rectangular", - "sourceHash": "83a45603ac7f41e8" - }, - "editor.buildPanel.rooms.title": { - "message": "Estancias", - "sourceHash": "3e7408ed44676176" - }, - "editor.buildPanel.rooms.walls.caption": { - "message": "{lead} Cierra el contorno para crear una estancia o déjalo abierto para tabiques.", - "sourceHash": "8a21a03c4acf396b" - }, - "editor.buildPanel.rooms.walls.label": { - "message": "Muros", - "sourceHash": "aac9a732d2159ccc" - }, - "editor.buildPanel.rooms.walls.lead": { - "message": "Dibuja un muro cada vez.", - "sourceHash": "9843edd68426fa2d" - }, - "editor.buildPanel.rooms.walls.title": { - "message": "Muros", - "sourceHash": "aac9a732d2159ccc" - }, - "viewer.camera.orbitLeft": { - "message": "Orbitar a la izquierda", - "sourceHash": "8bf25afa3cb8baea" - }, - "viewer.camera.orbitRight": { - "message": "Orbitar a la derecha", - "sourceHash": "b9cc64c1d1bb0d46" - }, - "viewer.camera.topView": { - "message": "Vista superior", - "sourceHash": "1ab1eb3a14f4b0e0" - }, - "viewer.common.off": { - "message": "Desactivado", - "sourceHash": "50accd740fa2e736" - }, - "viewer.common.on": { - "message": "Activado", - "sourceHash": "9cc01faad79c28da" - }, - "viewer.display.camera": { - "message": "Cámara", - "sourceHash": "14622745dbf93f4c" - }, - "viewer.display.colored": { - "message": "A color", - "sourceHash": "d3d478fa6c6d7ddc" - }, - "viewer.display.colors": { - "message": "Colores", - "sourceHash": "82b74c4f5ef43336" - }, - "viewer.display.edges.off": { - "message": "Desactivadas", - "sourceHash": "50accd740fa2e736" - }, - "viewer.display.edges.offDetail": { - "message": "Sin líneas de arista", - "sourceHash": "4ff58913a4492199" - }, - "viewer.display.edges.soft": { - "message": "Suaves", - "sourceHash": "d266d83ec490df4e" - }, - "viewer.display.edges.softDetail": { - "message": "Contorno tenue de los pliegues principales", - "sourceHash": "0c583f1d4201293b" - }, - "viewer.display.edges.strong": { - "message": "Marcadas", - "sourceHash": "f82927a01d4c5e45" - }, - "viewer.display.edges.strongDetail": { - "message": "Líneas de arista nítidas y opacas", - "sourceHash": "2416a48df28d6205" - }, - "viewer.display.edges.title": { - "message": "Aristas", - "sourceHash": "027bbc8b7287c06a" - }, - "viewer.display.monochrome": { - "message": "Monocromo", - "sourceHash": "ecde2f7c26914be3" - }, - "viewer.display.orthographic": { - "message": "Ortográfica", - "sourceHash": "ed59dd8ce7797a66" - }, - "viewer.display.perspective": { - "message": "Perspectiva", - "sourceHash": "316e25993ff72b88" - }, - "viewer.display.render.rendered": { - "message": "Realista", - "sourceHash": "f363702a10aedaeb" - }, - "viewer.display.render.renderedDetail": { - "message": "Oclusión ambiental completa", - "sourceHash": "8bb9e54fc786ca77" - }, - "viewer.display.render.solid": { - "message": "Sólido", - "sourceHash": "ef3e765e692faf4c" - }, - "viewer.display.render.solidDetail": { - "message": "Plano y rápido, sin oclusión ambiental", - "sourceHash": "8827df1cdeb02ed9" - }, - "viewer.display.render.title": { - "message": "Renderizado", - "sourceHash": "5e58af99d6d4fa6a" - }, - "viewer.display.shadows": { - "message": "Sombras", - "sourceHash": "4c6bcb30b40047fc" - }, - "viewer.display.title": { - "message": "Ajustes de visualización", - "sourceHash": "d0db46f206a88e2e" - }, - "viewer.floorplan.alignNorth": { - "message": "Orientar la vista al norte", - "sourceHash": "2bdf83801d4a40af" - }, - "viewer.floorplan.empty": { - "message": "No hay planos de planta disponibles para esta escena.", - "sourceHash": "286a757d8b52bb02" - }, - "viewer.floorplan.fit": { - "message": "Ajustar plano", - "sourceHash": "bf63c679ab94dbcd" - }, - "viewer.floorplan.floor": { - "message": "Planta", - "sourceHash": "d7148fa065bc709b" - }, - "viewer.floorplan.floorPlan": { - "message": "Plano de planta", - "sourceHash": "fee6203bb9046929" - }, - "viewer.floorplan.viewLabel": { - "message": "Vista 2D de {floor}", - "sourceHash": "28b255b7bf5f1084" - }, - "viewer.floorplan.zoomIn": { - "message": "Acercar", - "sourceHash": "d17804412f8dd081" - }, - "viewer.floorplan.zoomOut": { - "message": "Alejar", - "sourceHash": "23b012a283b2c14a" - }, - "viewer.header.back": { - "message": "Volver", - "sourceHash": "7ceade8f25b58fe9" - }, - "viewer.header.levels": { - "message": "Plantas", - "sourceHash": "cc3d601d59e2e26c" - }, - "viewer.header.rooms": { - "message": "Estancias", - "sourceHash": "3e7408ed44676176" - }, - "viewer.header.selection": { - "message": "Selección", - "sourceHash": "170b2049ef0f9131" - }, - "viewer.header.units": { - "message": "Unidades", - "sourceHash": "b404dded35241274" - }, - "viewer.header.untitled": { - "message": "Sin título", - "sourceHash": "6547a4c0c368042a" - }, - "viewer.header.zones": { - "message": "Zonas", - "sourceHash": "665db49a26f81425" - }, - "viewer.levels.exploded": { - "message": "Separadas", - "sourceHash": "be12f8788f1f0a05" - }, - "viewer.levels.label": { - "message": "Plantas: {mode}", - "sourceHash": "4bf4a6861d7d633e" - }, - "viewer.levels.manual": { - "message": "Manual", - "sourceHash": "387082cd5f118bed" - }, - "viewer.levels.solo": { - "message": "Aislada", - "sourceHash": "e9f59b513f2f45ac" - }, - "viewer.levels.stacked": { - "message": "Apiladas", - "sourceHash": "c203c84eee18c0f6" - }, - "viewer.stage.layout": { - "message": "Disposición de vistas", - "sourceHash": "9dd21ca47cffa3b8" - }, - "viewer.stage.split": { - "message": "Dividida", - "sourceHash": "e4cd64b1680ac0d2" - }, - "viewer.theme.blueprint": { - "message": "Plano técnico", - "sourceHash": "f14b89d713d2528a" - }, - "viewer.theme.mediterranean": { - "message": "Mediterráneo", - "sourceHash": "142bcb2841f423c5" - }, - "viewer.theme.night": { - "message": "Noche", - "sourceHash": "b399942ec5d7e1ba" - }, - "viewer.theme.overcast": { - "message": "Nublado", - "sourceHash": "f512d1b947b8ae5a" - }, - "viewer.theme.paper": { - "message": "Papel", - "sourceHash": "583542b3bc837192" - }, - "viewer.theme.studio": { - "message": "Estudio", - "sourceHash": "7abc2dc6e39b1747" - }, - "viewer.theme.sunset": { - "message": "Atardecer", - "sourceHash": "efe94d3ec8d8c44e" - }, - "viewer.theme.title": { - "message": "Ambiente", - "sourceHash": "82720864e5034398" - }, - "viewer.theme.twilight": { - "message": "Crepúsculo", - "sourceHash": "4c10ea0aa0e2ea41" - }, - "viewer.theme.verdant": { - "message": "Verde", - "sourceHash": "f9592fb0672740c0" - }, - "viewer.visibility.guides": { - "message": "Guías", - "sourceHash": "892d5dd34372028b" - }, - "viewer.visibility.scans": { - "message": "Escaneos 3D", - "sourceHash": "3260bfa69cca50e5" - }, - "viewer.visibility.title": { - "message": "Visibilidad", - "sourceHash": "2cefbb554eadbb6e" - }, - "viewer.walkthrough.label": { - "message": "Recorrido: {state}", - "sourceHash": "31afd9dee47ba943" - }, - "viewer.walls.cutaway": { - "message": "Vista seccionada", - "sourceHash": "4da8b3678ed77b45" - }, - "viewer.walls.fullHeight": { - "message": "Altura completa", - "sourceHash": "131bac2467f4fdf6" - }, - "viewer.walls.label": { - "message": "Muros: {mode}", - "sourceHash": "32f3d058cf2526d2" - }, - "viewer.walls.low": { - "message": "Bajos", - "sourceHash": "803532407fb5962b" - } - } -} diff --git a/packages/editor/src/i18n/catalogs/fr.json b/packages/editor/src/i18n/catalogs/fr.json deleted file mode 100644 index 86889813fc..0000000000 --- a/packages/editor/src/i18n/catalogs/fr.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "locale": "fr", - "messages": { - "core.level.basement": { - "message": "Sous-sol", - "sourceHash": "8caa959c0ae800c3" - }, - "core.level.basementNumber": { - "message": "Sous-sol {number}", - "sourceHash": "14927085ee91ba05" - }, - "core.level.floorNumber": { - "message": "Étage {number}", - "sourceHash": "e59074a7b9b3f56e" - }, - "core.level.groundFloor": { - "message": "Rez-de-chaussée", - "sourceHash": "cde3efc9b7ddfa41" - }, - "editor.buildPanel.advanced": { - "message": "Avancé", - "sourceHash": "f571cb2d06ae01ae" - }, - "editor.buildPanel.rooms.hint": { - "message": "murs · sol · plafond", - "sourceHash": "fb40866c2d6db5fb" - }, - "editor.buildPanel.rooms.idle": { - "message": "Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.", - "sourceHash": "271039bd919dc4c1" - }, - "editor.buildPanel.rooms.polygon.caption": { - "message": "{lead} puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.", - "sourceHash": "ae2800b6c9e2b6ed" - }, - "editor.buildPanel.rooms.polygon.label": { - "message": "Polygone", - "sourceHash": "4e186fc057733162" - }, - "editor.buildPanel.rooms.polygon.lead": { - "message": "Cliquez sur chaque angle,", - "sourceHash": "345e3aca3879b5c5" - }, - "editor.buildPanel.rooms.polygon.title": { - "message": "Pièce polygonale", - "sourceHash": "8ec6152e139b0e3f" - }, - "editor.buildPanel.rooms.rectangle.caption": { - "message": "{lead} sur la grille. Les murs, le sol et le plafond sont créés ensemble.", - "sourceHash": "9b46295eedcbedae" - }, - "editor.buildPanel.rooms.rectangle.label": { - "message": "Rectangle", - "sourceHash": "2c2c20272b0370cc" - }, - "editor.buildPanel.rooms.rectangle.lead": { - "message": "Cliquez sur deux angles", - "sourceHash": "060faa48e299d7c9" - }, - "editor.buildPanel.rooms.rectangle.title": { - "message": "Pièce rectangulaire", - "sourceHash": "83a45603ac7f41e8" - }, - "editor.buildPanel.rooms.title": { - "message": "Pièces", - "sourceHash": "3e7408ed44676176" - }, - "editor.buildPanel.rooms.walls.caption": { - "message": "{lead} Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.", - "sourceHash": "8a21a03c4acf396b" - }, - "editor.buildPanel.rooms.walls.label": { - "message": "Murs", - "sourceHash": "aac9a732d2159ccc" - }, - "editor.buildPanel.rooms.walls.lead": { - "message": "Tracez un mur à la fois.", - "sourceHash": "9843edd68426fa2d" - }, - "editor.buildPanel.rooms.walls.title": { - "message": "Murs", - "sourceHash": "aac9a732d2159ccc" - }, - "viewer.camera.orbitLeft": { - "message": "Pivoter à gauche", - "sourceHash": "8bf25afa3cb8baea" - }, - "viewer.camera.orbitRight": { - "message": "Pivoter à droite", - "sourceHash": "b9cc64c1d1bb0d46" - }, - "viewer.camera.topView": { - "message": "Vue de dessus", - "sourceHash": "1ab1eb3a14f4b0e0" - }, - "viewer.common.off": { - "message": "Désactivé", - "sourceHash": "50accd740fa2e736" - }, - "viewer.common.on": { - "message": "Activé", - "sourceHash": "9cc01faad79c28da" - }, - "viewer.display.camera": { - "message": "Caméra", - "sourceHash": "14622745dbf93f4c" - }, - "viewer.display.colored": { - "message": "En couleur", - "sourceHash": "d3d478fa6c6d7ddc" - }, - "viewer.display.colors": { - "message": "Couleurs", - "sourceHash": "82b74c4f5ef43336" - }, - "viewer.display.edges.off": { - "message": "Désactivées", - "sourceHash": "50accd740fa2e736" - }, - "viewer.display.edges.offDetail": { - "message": "Aucune ligne d’arête", - "sourceHash": "4ff58913a4492199" - }, - "viewer.display.edges.soft": { - "message": "Douces", - "sourceHash": "d266d83ec490df4e" - }, - "viewer.display.edges.softDetail": { - "message": "Contour léger des plis principaux", - "sourceHash": "0c583f1d4201293b" - }, - "viewer.display.edges.strong": { - "message": "Marquées", - "sourceHash": "f82927a01d4c5e45" - }, - "viewer.display.edges.strongDetail": { - "message": "Lignes d’arête nettes et opaques", - "sourceHash": "2416a48df28d6205" - }, - "viewer.display.edges.title": { - "message": "Arêtes", - "sourceHash": "027bbc8b7287c06a" - }, - "viewer.display.monochrome": { - "message": "Monochrome", - "sourceHash": "ecde2f7c26914be3" - }, - "viewer.display.orthographic": { - "message": "Orthographique", - "sourceHash": "ed59dd8ce7797a66" - }, - "viewer.display.perspective": { - "message": "Perspective", - "sourceHash": "316e25993ff72b88" - }, - "viewer.display.render.rendered": { - "message": "Réaliste", - "sourceHash": "f363702a10aedaeb" - }, - "viewer.display.render.renderedDetail": { - "message": "Occlusion ambiante complète", - "sourceHash": "8bb9e54fc786ca77" - }, - "viewer.display.render.solid": { - "message": "Solide", - "sourceHash": "ef3e765e692faf4c" - }, - "viewer.display.render.solidDetail": { - "message": "Rapide et uniforme, sans occlusion ambiante", - "sourceHash": "8827df1cdeb02ed9" - }, - "viewer.display.render.title": { - "message": "Rendu", - "sourceHash": "5e58af99d6d4fa6a" - }, - "viewer.display.shadows": { - "message": "Ombres", - "sourceHash": "4c6bcb30b40047fc" - }, - "viewer.display.title": { - "message": "Paramètres d’affichage", - "sourceHash": "d0db46f206a88e2e" - }, - "viewer.floorplan.alignNorth": { - "message": "Orienter la vue vers le nord", - "sourceHash": "2bdf83801d4a40af" - }, - "viewer.floorplan.empty": { - "message": "Aucun plan d’étage n’est disponible pour cette scène.", - "sourceHash": "286a757d8b52bb02" - }, - "viewer.floorplan.fit": { - "message": "Ajuster le plan", - "sourceHash": "bf63c679ab94dbcd" - }, - "viewer.floorplan.floor": { - "message": "Niveau", - "sourceHash": "d7148fa065bc709b" - }, - "viewer.floorplan.floorPlan": { - "message": "Plan d’étage", - "sourceHash": "fee6203bb9046929" - }, - "viewer.floorplan.viewLabel": { - "message": "Vue 2D — {floor}", - "sourceHash": "28b255b7bf5f1084" - }, - "viewer.floorplan.zoomIn": { - "message": "Zoom avant", - "sourceHash": "d17804412f8dd081" - }, - "viewer.floorplan.zoomOut": { - "message": "Zoom arrière", - "sourceHash": "23b012a283b2c14a" - }, - "viewer.header.back": { - "message": "Retour", - "sourceHash": "7ceade8f25b58fe9" - }, - "viewer.header.levels": { - "message": "Niveaux", - "sourceHash": "cc3d601d59e2e26c" - }, - "viewer.header.rooms": { - "message": "Pièces", - "sourceHash": "3e7408ed44676176" - }, - "viewer.header.selection": { - "message": "Sélection", - "sourceHash": "170b2049ef0f9131" - }, - "viewer.header.units": { - "message": "Unités", - "sourceHash": "b404dded35241274" - }, - "viewer.header.untitled": { - "message": "Sans titre", - "sourceHash": "6547a4c0c368042a" - }, - "viewer.header.zones": { - "message": "Zones", - "sourceHash": "665db49a26f81425" - }, - "viewer.levels.exploded": { - "message": "Éclatés", - "sourceHash": "be12f8788f1f0a05" - }, - "viewer.levels.label": { - "message": "Niveaux : {mode}", - "sourceHash": "4bf4a6861d7d633e" - }, - "viewer.levels.manual": { - "message": "Manuel", - "sourceHash": "387082cd5f118bed" - }, - "viewer.levels.solo": { - "message": "Isolé", - "sourceHash": "e9f59b513f2f45ac" - }, - "viewer.levels.stacked": { - "message": "Superposés", - "sourceHash": "c203c84eee18c0f6" - }, - "viewer.stage.layout": { - "message": "Disposition des vues", - "sourceHash": "9dd21ca47cffa3b8" - }, - "viewer.stage.split": { - "message": "Partagé", - "sourceHash": "e4cd64b1680ac0d2" - }, - "viewer.theme.blueprint": { - "message": "Plan", - "sourceHash": "f14b89d713d2528a" - }, - "viewer.theme.mediterranean": { - "message": "Méditerranéen", - "sourceHash": "142bcb2841f423c5" - }, - "viewer.theme.night": { - "message": "Nuit", - "sourceHash": "b399942ec5d7e1ba" - }, - "viewer.theme.overcast": { - "message": "Ciel couvert", - "sourceHash": "f512d1b947b8ae5a" - }, - "viewer.theme.paper": { - "message": "Papier", - "sourceHash": "583542b3bc837192" - }, - "viewer.theme.studio": { - "message": "Studio", - "sourceHash": "7abc2dc6e39b1747" - }, - "viewer.theme.sunset": { - "message": "Coucher de soleil", - "sourceHash": "efe94d3ec8d8c44e" - }, - "viewer.theme.title": { - "message": "Ambiance", - "sourceHash": "82720864e5034398" - }, - "viewer.theme.twilight": { - "message": "Crépuscule", - "sourceHash": "4c10ea0aa0e2ea41" - }, - "viewer.theme.verdant": { - "message": "Verdoyant", - "sourceHash": "f9592fb0672740c0" - }, - "viewer.visibility.guides": { - "message": "Repères", - "sourceHash": "892d5dd34372028b" - }, - "viewer.visibility.scans": { - "message": "Relevés 3D", - "sourceHash": "3260bfa69cca50e5" - }, - "viewer.visibility.title": { - "message": "Visibilité", - "sourceHash": "2cefbb554eadbb6e" - }, - "viewer.walkthrough.label": { - "message": "Visite : {state}", - "sourceHash": "31afd9dee47ba943" - }, - "viewer.walls.cutaway": { - "message": "Vue en coupe", - "sourceHash": "4da8b3678ed77b45" - }, - "viewer.walls.fullHeight": { - "message": "Pleine hauteur", - "sourceHash": "131bac2467f4fdf6" - }, - "viewer.walls.label": { - "message": "Murs : {mode}", - "sourceHash": "32f3d058cf2526d2" - }, - "viewer.walls.low": { - "message": "Bas", - "sourceHash": "803532407fb5962b" - } - } -} diff --git a/packages/editor/src/i18n/catalogs/ja.json b/packages/editor/src/i18n/catalogs/ja.json deleted file mode 100644 index 912f35ab91..0000000000 --- a/packages/editor/src/i18n/catalogs/ja.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "locale": "ja", - "messages": { - "core.level.basement": { - "message": "地下1階", - "sourceHash": "8caa959c0ae800c3" - }, - "core.level.basementNumber": { - "message": "地下{number}階", - "sourceHash": "14927085ee91ba05" - }, - "core.level.floorNumber": { - "message": "{number}階", - "sourceHash": "e59074a7b9b3f56e" - }, - "core.level.groundFloor": { - "message": "1階", - "sourceHash": "cde3efc9b7ddfa41" - }, - "editor.buildPanel.advanced": { - "message": "詳細", - "sourceHash": "f571cb2d06ae01ae" - }, - "editor.buildPanel.rooms.hint": { - "message": "壁 · 床 · 天井", - "sourceHash": "fb40866c2d6db5fb" - }, - "editor.buildPanel.rooms.idle": { - "message": "形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。", - "sourceHash": "271039bd919dc4c1" - }, - "editor.buildPanel.rooms.polygon.caption": { - "message": "{lead}最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。", - "sourceHash": "ae2800b6c9e2b6ed" - }, - "editor.buildPanel.rooms.polygon.label": { - "message": "多角形", - "sourceHash": "4e186fc057733162" - }, - "editor.buildPanel.rooms.polygon.lead": { - "message": "各頂点をクリックし、", - "sourceHash": "345e3aca3879b5c5" - }, - "editor.buildPanel.rooms.polygon.title": { - "message": "多角形の部屋", - "sourceHash": "8ec6152e139b0e3f" - }, - "editor.buildPanel.rooms.rectangle.caption": { - "message": "グリッド上で{lead}します。壁・床・天井をまとめて作成します。", - "sourceHash": "9b46295eedcbedae" - }, - "editor.buildPanel.rooms.rectangle.label": { - "message": "長方形", - "sourceHash": "2c2c20272b0370cc" - }, - "editor.buildPanel.rooms.rectangle.lead": { - "message": "対角の2点をクリック", - "sourceHash": "060faa48e299d7c9" - }, - "editor.buildPanel.rooms.rectangle.title": { - "message": "長方形の部屋", - "sourceHash": "83a45603ac7f41e8" - }, - "editor.buildPanel.rooms.title": { - "message": "部屋", - "sourceHash": "3e7408ed44676176" - }, - "editor.buildPanel.rooms.walls.caption": { - "message": "{lead}輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。", - "sourceHash": "8a21a03c4acf396b" - }, - "editor.buildPanel.rooms.walls.label": { - "message": "壁", - "sourceHash": "aac9a732d2159ccc" - }, - "editor.buildPanel.rooms.walls.lead": { - "message": "壁を1面ずつ描画します。", - "sourceHash": "9843edd68426fa2d" - }, - "editor.buildPanel.rooms.walls.title": { - "message": "壁", - "sourceHash": "aac9a732d2159ccc" - }, - "viewer.camera.orbitLeft": { - "message": "左に回転", - "sourceHash": "8bf25afa3cb8baea" - }, - "viewer.camera.orbitRight": { - "message": "右に回転", - "sourceHash": "b9cc64c1d1bb0d46" - }, - "viewer.camera.topView": { - "message": "上面図", - "sourceHash": "1ab1eb3a14f4b0e0" - }, - "viewer.common.off": { - "message": "オフ", - "sourceHash": "50accd740fa2e736" - }, - "viewer.common.on": { - "message": "オン", - "sourceHash": "9cc01faad79c28da" - }, - "viewer.display.camera": { - "message": "カメラ", - "sourceHash": "14622745dbf93f4c" - }, - "viewer.display.colored": { - "message": "カラー表示", - "sourceHash": "d3d478fa6c6d7ddc" - }, - "viewer.display.colors": { - "message": "カラー", - "sourceHash": "82b74c4f5ef43336" - }, - "viewer.display.edges.off": { - "message": "オフ", - "sourceHash": "50accd740fa2e736" - }, - "viewer.display.edges.offDetail": { - "message": "エッジ線を表示しない", - "sourceHash": "4ff58913a4492199" - }, - "viewer.display.edges.soft": { - "message": "ソフト", - "sourceHash": "d266d83ec490df4e" - }, - "viewer.display.edges.softDetail": { - "message": "主要な稜線を薄く表示", - "sourceHash": "0c583f1d4201293b" - }, - "viewer.display.edges.strong": { - "message": "強調", - "sourceHash": "f82927a01d4c5e45" - }, - "viewer.display.edges.strongDetail": { - "message": "エッジ線をくっきり表示", - "sourceHash": "2416a48df28d6205" - }, - "viewer.display.edges.title": { - "message": "エッジ", - "sourceHash": "027bbc8b7287c06a" - }, - "viewer.display.monochrome": { - "message": "モノクロ", - "sourceHash": "ecde2f7c26914be3" - }, - "viewer.display.orthographic": { - "message": "平行投影", - "sourceHash": "ed59dd8ce7797a66" - }, - "viewer.display.perspective": { - "message": "透視投影", - "sourceHash": "316e25993ff72b88" - }, - "viewer.display.render.rendered": { - "message": "レンダリング", - "sourceHash": "f363702a10aedaeb" - }, - "viewer.display.render.renderedDetail": { - "message": "アンビエントオクルージョンを完全表示", - "sourceHash": "8bb9e54fc786ca77" - }, - "viewer.display.render.solid": { - "message": "ソリッド", - "sourceHash": "ef3e765e692faf4c" - }, - "viewer.display.render.solidDetail": { - "message": "軽量表示(アンビエントオクルージョンなし)", - "sourceHash": "8827df1cdeb02ed9" - }, - "viewer.display.render.title": { - "message": "レンダリング", - "sourceHash": "5e58af99d6d4fa6a" - }, - "viewer.display.shadows": { - "message": "影", - "sourceHash": "4c6bcb30b40047fc" - }, - "viewer.display.title": { - "message": "表示設定", - "sourceHash": "d0db46f206a88e2e" - }, - "viewer.floorplan.alignNorth": { - "message": "北を上に表示", - "sourceHash": "2bdf83801d4a40af" - }, - "viewer.floorplan.empty": { - "message": "このシーンには表示できる平面図がありません。", - "sourceHash": "286a757d8b52bb02" - }, - "viewer.floorplan.fit": { - "message": "全体を表示", - "sourceHash": "bf63c679ab94dbcd" - }, - "viewer.floorplan.floor": { - "message": "階", - "sourceHash": "d7148fa065bc709b" - }, - "viewer.floorplan.floorPlan": { - "message": "平面図", - "sourceHash": "fee6203bb9046929" - }, - "viewer.floorplan.viewLabel": { - "message": "{floor}の2Dビュー", - "sourceHash": "28b255b7bf5f1084" - }, - "viewer.floorplan.zoomIn": { - "message": "拡大", - "sourceHash": "d17804412f8dd081" - }, - "viewer.floorplan.zoomOut": { - "message": "縮小", - "sourceHash": "23b012a283b2c14a" - }, - "viewer.header.back": { - "message": "戻る", - "sourceHash": "7ceade8f25b58fe9" - }, - "viewer.header.levels": { - "message": "階層", - "sourceHash": "cc3d601d59e2e26c" - }, - "viewer.header.rooms": { - "message": "部屋", - "sourceHash": "3e7408ed44676176" - }, - "viewer.header.selection": { - "message": "選択", - "sourceHash": "170b2049ef0f9131" - }, - "viewer.header.units": { - "message": "住戸", - "sourceHash": "b404dded35241274" - }, - "viewer.header.untitled": { - "message": "名称未設定", - "sourceHash": "6547a4c0c368042a" - }, - "viewer.header.zones": { - "message": "ゾーン", - "sourceHash": "665db49a26f81425" - }, - "viewer.levels.exploded": { - "message": "分解", - "sourceHash": "be12f8788f1f0a05" - }, - "viewer.levels.label": { - "message": "階層:{mode}", - "sourceHash": "4bf4a6861d7d633e" - }, - "viewer.levels.manual": { - "message": "手動", - "sourceHash": "387082cd5f118bed" - }, - "viewer.levels.solo": { - "message": "単独", - "sourceHash": "e9f59b513f2f45ac" - }, - "viewer.levels.stacked": { - "message": "積層", - "sourceHash": "c203c84eee18c0f6" - }, - "viewer.stage.layout": { - "message": "ビュー配置", - "sourceHash": "9dd21ca47cffa3b8" - }, - "viewer.stage.split": { - "message": "分割", - "sourceHash": "e4cd64b1680ac0d2" - }, - "viewer.theme.blueprint": { - "message": "青図", - "sourceHash": "f14b89d713d2528a" - }, - "viewer.theme.mediterranean": { - "message": "地中海", - "sourceHash": "142bcb2841f423c5" - }, - "viewer.theme.night": { - "message": "夜景", - "sourceHash": "b399942ec5d7e1ba" - }, - "viewer.theme.overcast": { - "message": "曇天", - "sourceHash": "f512d1b947b8ae5a" - }, - "viewer.theme.paper": { - "message": "ペーパー", - "sourceHash": "583542b3bc837192" - }, - "viewer.theme.studio": { - "message": "スタジオ", - "sourceHash": "7abc2dc6e39b1747" - }, - "viewer.theme.sunset": { - "message": "夕景", - "sourceHash": "efe94d3ec8d8c44e" - }, - "viewer.theme.title": { - "message": "シーンテーマ", - "sourceHash": "82720864e5034398" - }, - "viewer.theme.twilight": { - "message": "薄暮", - "sourceHash": "4c10ea0aa0e2ea41" - }, - "viewer.theme.verdant": { - "message": "緑景", - "sourceHash": "f9592fb0672740c0" - }, - "viewer.visibility.guides": { - "message": "ガイド", - "sourceHash": "892d5dd34372028b" - }, - "viewer.visibility.scans": { - "message": "3Dスキャン", - "sourceHash": "3260bfa69cca50e5" - }, - "viewer.visibility.title": { - "message": "表示", - "sourceHash": "2cefbb554eadbb6e" - }, - "viewer.walkthrough.label": { - "message": "ウォークスルー:{state}", - "sourceHash": "31afd9dee47ba943" - }, - "viewer.walls.cutaway": { - "message": "カット表示", - "sourceHash": "4da8b3678ed77b45" - }, - "viewer.walls.fullHeight": { - "message": "全高", - "sourceHash": "131bac2467f4fdf6" - }, - "viewer.walls.label": { - "message": "壁:{mode}", - "sourceHash": "32f3d058cf2526d2" - }, - "viewer.walls.low": { - "message": "低く表示", - "sourceHash": "803532407fb5962b" - } - } -} diff --git a/packages/editor/src/i18n/catalogs/zh-CN.json b/packages/editor/src/i18n/catalogs/zh-CN.json deleted file mode 100644 index d2fe5dacfb..0000000000 --- a/packages/editor/src/i18n/catalogs/zh-CN.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "locale": "zh-CN", - "messages": { - "core.level.basement": { - "message": "地下层", - "sourceHash": "8caa959c0ae800c3" - }, - "core.level.basementNumber": { - "message": "地下 {number} 层", - "sourceHash": "14927085ee91ba05" - }, - "core.level.floorNumber": { - "message": "{number} 层", - "sourceHash": "e59074a7b9b3f56e" - }, - "core.level.groundFloor": { - "message": "首层", - "sourceHash": "cde3efc9b7ddfa41" - }, - "editor.buildPanel.advanced": { - "message": "高级", - "sourceHash": "f571cb2d06ae01ae" - }, - "editor.buildPanel.rooms.hint": { - "message": "墙体 · 楼板 · 天花板", - "sourceHash": "fb40866c2d6db5fb" - }, - "editor.buildPanel.rooms.idle": { - "message": "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和天花板。", - "sourceHash": "271039bd919dc4c1" - }, - "editor.buildPanel.rooms.polygon.caption": { - "message": "{lead}然后再次点击起点闭合。适用于 L 形和斜角房间。", - "sourceHash": "ae2800b6c9e2b6ed" - }, - "editor.buildPanel.rooms.polygon.label": { - "message": "多边形", - "sourceHash": "4e186fc057733162" - }, - "editor.buildPanel.rooms.polygon.lead": { - "message": "依次点击各个角点,", - "sourceHash": "345e3aca3879b5c5" - }, - "editor.buildPanel.rooms.polygon.title": { - "message": "多边形房间", - "sourceHash": "8ec6152e139b0e3f" - }, - "editor.buildPanel.rooms.rectangle.caption": { - "message": "在网格上{lead}。墙体、楼板和天花板将一并创建。", - "sourceHash": "9b46295eedcbedae" - }, - "editor.buildPanel.rooms.rectangle.label": { - "message": "矩形", - "sourceHash": "2c2c20272b0370cc" - }, - "editor.buildPanel.rooms.rectangle.lead": { - "message": "点击两个对角点", - "sourceHash": "060faa48e299d7c9" - }, - "editor.buildPanel.rooms.rectangle.title": { - "message": "矩形房间", - "sourceHash": "83a45603ac7f41e8" - }, - "editor.buildPanel.rooms.title": { - "message": "房间", - "sourceHash": "3e7408ed44676176" - }, - "editor.buildPanel.rooms.walls.caption": { - "message": "{lead}闭合轮廓后会形成房间;保持开口可绘制隔墙。", - "sourceHash": "8a21a03c4acf396b" - }, - "editor.buildPanel.rooms.walls.label": { - "message": "墙体", - "sourceHash": "aac9a732d2159ccc" - }, - "editor.buildPanel.rooms.walls.lead": { - "message": "逐面绘制墙体。", - "sourceHash": "9843edd68426fa2d" - }, - "editor.buildPanel.rooms.walls.title": { - "message": "墙体", - "sourceHash": "aac9a732d2159ccc" - }, - "viewer.camera.orbitLeft": { - "message": "向左旋转", - "sourceHash": "8bf25afa3cb8baea" - }, - "viewer.camera.orbitRight": { - "message": "向右旋转", - "sourceHash": "b9cc64c1d1bb0d46" - }, - "viewer.camera.topView": { - "message": "俯视图", - "sourceHash": "1ab1eb3a14f4b0e0" - }, - "viewer.common.off": { - "message": "关闭", - "sourceHash": "50accd740fa2e736" - }, - "viewer.common.on": { - "message": "开启", - "sourceHash": "9cc01faad79c28da" - }, - "viewer.display.camera": { - "message": "相机", - "sourceHash": "14622745dbf93f4c" - }, - "viewer.display.colored": { - "message": "彩色", - "sourceHash": "d3d478fa6c6d7ddc" - }, - "viewer.display.colors": { - "message": "颜色", - "sourceHash": "82b74c4f5ef43336" - }, - "viewer.display.edges.off": { - "message": "关闭", - "sourceHash": "50accd740fa2e736" - }, - "viewer.display.edges.offDetail": { - "message": "不显示边线", - "sourceHash": "4ff58913a4492199" - }, - "viewer.display.edges.soft": { - "message": "柔和", - "sourceHash": "d266d83ec490df4e" - }, - "viewer.display.edges.softDetail": { - "message": "淡化显示主要折线", - "sourceHash": "0c583f1d4201293b" - }, - "viewer.display.edges.strong": { - "message": "清晰", - "sourceHash": "f82927a01d4c5e45" - }, - "viewer.display.edges.strongDetail": { - "message": "清晰、不透明的边线", - "sourceHash": "2416a48df28d6205" - }, - "viewer.display.edges.title": { - "message": "边线", - "sourceHash": "027bbc8b7287c06a" - }, - "viewer.display.monochrome": { - "message": "单色", - "sourceHash": "ecde2f7c26914be3" - }, - "viewer.display.orthographic": { - "message": "正交", - "sourceHash": "ed59dd8ce7797a66" - }, - "viewer.display.perspective": { - "message": "透视", - "sourceHash": "316e25993ff72b88" - }, - "viewer.display.render.rendered": { - "message": "渲染效果", - "sourceHash": "f363702a10aedaeb" - }, - "viewer.display.render.renderedDetail": { - "message": "完整环境光遮蔽", - "sourceHash": "8bb9e54fc786ca77" - }, - "viewer.display.render.solid": { - "message": "实体", - "sourceHash": "ef3e765e692faf4c" - }, - "viewer.display.render.solidDetail": { - "message": "平整快速,不启用环境光遮蔽", - "sourceHash": "8827df1cdeb02ed9" - }, - "viewer.display.render.title": { - "message": "渲染", - "sourceHash": "5e58af99d6d4fa6a" - }, - "viewer.display.shadows": { - "message": "阴影", - "sourceHash": "4c6bcb30b40047fc" - }, - "viewer.display.title": { - "message": "显示设置", - "sourceHash": "d0db46f206a88e2e" - }, - "viewer.floorplan.alignNorth": { - "message": "视图对齐北向", - "sourceHash": "2bdf83801d4a40af" - }, - "viewer.floorplan.empty": { - "message": "此场景没有可用的平面图。", - "sourceHash": "286a757d8b52bb02" - }, - "viewer.floorplan.fit": { - "message": "适合窗口", - "sourceHash": "bf63c679ab94dbcd" - }, - "viewer.floorplan.floor": { - "message": "楼层", - "sourceHash": "d7148fa065bc709b" - }, - "viewer.floorplan.floorPlan": { - "message": "平面图", - "sourceHash": "fee6203bb9046929" - }, - "viewer.floorplan.viewLabel": { - "message": "{floor}二维视图", - "sourceHash": "28b255b7bf5f1084" - }, - "viewer.floorplan.zoomIn": { - "message": "放大", - "sourceHash": "d17804412f8dd081" - }, - "viewer.floorplan.zoomOut": { - "message": "缩小", - "sourceHash": "23b012a283b2c14a" - }, - "viewer.header.back": { - "message": "返回", - "sourceHash": "7ceade8f25b58fe9" - }, - "viewer.header.levels": { - "message": "楼层", - "sourceHash": "cc3d601d59e2e26c" - }, - "viewer.header.rooms": { - "message": "房间", - "sourceHash": "3e7408ed44676176" - }, - "viewer.header.selection": { - "message": "当前选择", - "sourceHash": "170b2049ef0f9131" - }, - "viewer.header.units": { - "message": "单元", - "sourceHash": "b404dded35241274" - }, - "viewer.header.untitled": { - "message": "未命名", - "sourceHash": "6547a4c0c368042a" - }, - "viewer.header.zones": { - "message": "分区", - "sourceHash": "665db49a26f81425" - }, - "viewer.levels.exploded": { - "message": "分解", - "sourceHash": "be12f8788f1f0a05" - }, - "viewer.levels.label": { - "message": "楼层:{mode}", - "sourceHash": "4bf4a6861d7d633e" - }, - "viewer.levels.manual": { - "message": "手动", - "sourceHash": "387082cd5f118bed" - }, - "viewer.levels.solo": { - "message": "单层", - "sourceHash": "e9f59b513f2f45ac" - }, - "viewer.levels.stacked": { - "message": "叠层", - "sourceHash": "c203c84eee18c0f6" - }, - "viewer.stage.layout": { - "message": "视图布局", - "sourceHash": "9dd21ca47cffa3b8" - }, - "viewer.stage.split": { - "message": "分屏", - "sourceHash": "e4cd64b1680ac0d2" - }, - "viewer.theme.blueprint": { - "message": "蓝图", - "sourceHash": "f14b89d713d2528a" - }, - "viewer.theme.mediterranean": { - "message": "地中海", - "sourceHash": "142bcb2841f423c5" - }, - "viewer.theme.night": { - "message": "夜景", - "sourceHash": "b399942ec5d7e1ba" - }, - "viewer.theme.overcast": { - "message": "阴天", - "sourceHash": "f512d1b947b8ae5a" - }, - "viewer.theme.paper": { - "message": "纸面", - "sourceHash": "583542b3bc837192" - }, - "viewer.theme.studio": { - "message": "影棚", - "sourceHash": "7abc2dc6e39b1747" - }, - "viewer.theme.sunset": { - "message": "日落", - "sourceHash": "efe94d3ec8d8c44e" - }, - "viewer.theme.title": { - "message": "场景主题", - "sourceHash": "82720864e5034398" - }, - "viewer.theme.twilight": { - "message": "暮色", - "sourceHash": "4c10ea0aa0e2ea41" - }, - "viewer.theme.verdant": { - "message": "绿意", - "sourceHash": "f9592fb0672740c0" - }, - "viewer.visibility.guides": { - "message": "参考图", - "sourceHash": "892d5dd34372028b" - }, - "viewer.visibility.scans": { - "message": "扫描模型", - "sourceHash": "3260bfa69cca50e5" - }, - "viewer.visibility.title": { - "message": "可见性", - "sourceHash": "2cefbb554eadbb6e" - }, - "viewer.walkthrough.label": { - "message": "漫游:{state}", - "sourceHash": "31afd9dee47ba943" - }, - "viewer.walls.cutaway": { - "message": "剖切墙", - "sourceHash": "4da8b3678ed77b45" - }, - "viewer.walls.fullHeight": { - "message": "全高墙", - "sourceHash": "131bac2467f4fdf6" - }, - "viewer.walls.label": { - "message": "墙体:{mode}", - "sourceHash": "32f3d058cf2526d2" - }, - "viewer.walls.low": { - "message": "矮墙", - "sourceHash": "803532407fb5962b" - } - } -} diff --git a/packages/editor/src/i18n/locales.ts b/packages/editor/src/i18n/locales.ts index 831c5a48da..42445578ec 100644 --- a/packages/editor/src/i18n/locales.ts +++ b/packages/editor/src/i18n/locales.ts @@ -1,25 +1,23 @@ -import { - registerMessages, - type TranslationMessage, - type TranslationMessages, -} from '@pascal-app/core' -import es from './catalogs/es.json' -import fr from './catalogs/fr.json' -import ja from './catalogs/ja.json' -import zhCN from './catalogs/zh-CN.json' +import { type CompiledTranslationMessages, registerCompiledMessages } from '@pascal-app/core' +import { messages as en } from './locales/en/messages' +import { messages as es } from './locales/es/messages' +import { messages as fr } from './locales/fr/messages' +import { messages as ja } from './locales/ja/messages' +import { messages as zhCN } from './locales/zh-CN/messages' -export type BundledLocale = 'zh-CN' | 'fr' | 'es' | 'ja' +export type BundledLocale = 'en' | 'zh-CN' | 'fr' | 'es' | 'ja' type LocaleCatalog = { locale: BundledLocale - messages: Record + messages: CompiledTranslationMessages } export const bundledLocalePacks: Record = { - 'zh-CN': zhCN as LocaleCatalog, - fr: fr as LocaleCatalog, - es: es as LocaleCatalog, - ja: ja as LocaleCatalog, + en: { locale: 'en', messages: en }, + 'zh-CN': { locale: 'zh-CN', messages: zhCN }, + fr: { locale: 'fr', messages: fr }, + es: { locale: 'es', messages: es }, + ja: { locale: 'ja', messages: ja }, } let registered = false @@ -29,20 +27,6 @@ export function registerBundledLocales(): void { registered = true for (const catalog of Object.values(bundledLocalePacks)) { - const namespaces = new Map() - for (const [fullKey, entry] of Object.entries(catalog.messages)) { - const separator = fullKey.indexOf('.') - if (separator < 1) { - throw new Error(`Invalid translation key "${fullKey}" in ${catalog.locale}`) - } - const namespace = fullKey.slice(0, separator) - const key = fullKey.slice(separator + 1) - const messages = namespaces.get(namespace) ?? {} - messages[key] = entry.message - namespaces.set(namespace, messages) - } - for (const [namespace, messages] of namespaces) { - registerMessages(catalog.locale, namespace, messages) - } + registerCompiledMessages(catalog.locale, '', catalog.messages) } } diff --git a/packages/editor/src/i18n/locales/en/messages.po b/packages/editor/src/i18n/locales/en/messages.po new file mode 100644 index 0000000000..8e63c32fd0 --- /dev/null +++ b/packages/editor/src/i18n/locales/en/messages.po @@ -0,0 +1,362 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2026-10-26 22:28+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: en\n" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +msgid "core.level.basement" +msgstr "Basement" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +msgid "core.level.basementNumber" +msgstr "Basement {number}" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +msgid "core.level.floorNumber" +msgstr "Floor {number}" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +msgid "core.level.groundFloor" +msgstr "Ground floor" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +msgid "editor.buildPanel.advanced" +msgstr "Advanced" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +msgid "editor.buildPanel.rooms.hint" +msgstr "walls · floor · ceiling" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +msgid "editor.buildPanel.rooms.idle" +msgstr "Pick a shape to start. Every room brings its own walls, floor and ceiling." + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +msgid "editor.buildPanel.rooms.polygon.caption" +msgstr "{lead} then the first one again to close. For L-shapes and angled rooms." + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +msgid "editor.buildPanel.rooms.polygon.label" +msgstr "Polygon" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +msgid "editor.buildPanel.rooms.polygon.lead" +msgstr "Click each corner," + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +msgid "editor.buildPanel.rooms.polygon.title" +msgstr "Polygon room" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +msgid "editor.buildPanel.rooms.rectangle.caption" +msgstr "{lead} on the grid. Walls, floor and ceiling are created together." + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +msgid "editor.buildPanel.rooms.rectangle.label" +msgstr "Rectangle" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +msgid "editor.buildPanel.rooms.rectangle.lead" +msgstr "Click two corners" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +msgid "editor.buildPanel.rooms.rectangle.title" +msgstr "Rectangle room" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +msgid "editor.buildPanel.rooms.title" +msgstr "Rooms" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +msgid "editor.buildPanel.rooms.walls.caption" +msgstr "{lead} Close a loop and it becomes a room; leave it open for partitions." + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +msgid "editor.buildPanel.rooms.walls.label" +msgstr "Walls" + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +msgid "editor.buildPanel.rooms.walls.lead" +msgstr "Draw one wall at a time." + +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +msgid "editor.buildPanel.rooms.walls.title" +msgstr "Walls" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +msgid "viewer.camera.orbitLeft" +msgstr "Orbit left" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +msgid "viewer.camera.orbitRight" +msgstr "Orbit right" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +msgid "viewer.camera.topView" +msgstr "Top view" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +msgid "viewer.common.off" +msgstr "Off" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +msgid "viewer.common.on" +msgstr "On" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +msgid "viewer.display.camera" +msgstr "Camera" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +msgid "viewer.display.colored" +msgstr "Colored" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +msgid "viewer.display.colors" +msgstr "Colors" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +msgid "viewer.display.edges.off" +msgstr "Off" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +msgid "viewer.display.edges.offDetail" +msgstr "No edge lines" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +msgid "viewer.display.edges.soft" +msgstr "Soft" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +msgid "viewer.display.edges.softDetail" +msgstr "Faint outline of major creases" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +msgid "viewer.display.edges.strong" +msgstr "Strong" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +msgid "viewer.display.edges.strongDetail" +msgstr "Crisp, opaque edge lines" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +msgid "viewer.display.edges.title" +msgstr "Edges" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +msgid "viewer.display.monochrome" +msgstr "Monochrome" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +msgid "viewer.display.orthographic" +msgstr "Orthographic" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +msgid "viewer.display.perspective" +msgstr "Perspective" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +msgid "viewer.display.render.rendered" +msgstr "Rendered" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +msgid "viewer.display.render.renderedDetail" +msgstr "Full ambient occlusion" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +msgid "viewer.display.render.solid" +msgstr "Solid" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +msgid "viewer.display.render.solidDetail" +msgstr "Flat and fast — no ambient occlusion" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +msgid "viewer.display.render.title" +msgstr "Render" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +msgid "viewer.display.shadows" +msgstr "Shadows" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +msgid "viewer.display.title" +msgstr "Display settings" + +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +msgid "viewer.floorplan.alignNorth" +msgstr "Align view to north" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +msgid "viewer.floorplan.empty" +msgstr "No floor plans are available for this scene." + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +msgid "viewer.floorplan.fit" +msgstr "Fit floor plan" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +msgid "viewer.floorplan.floor" +msgstr "Floor" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +msgid "viewer.floorplan.floorPlan" +msgstr "Floor plan" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +msgid "viewer.floorplan.viewLabel" +msgstr "{floor} 2D view" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +msgid "viewer.floorplan.zoomIn" +msgstr "Zoom in" + +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +msgid "viewer.floorplan.zoomOut" +msgstr "Zoom out" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +msgid "viewer.header.back" +msgstr "Back" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +msgid "viewer.header.levels" +msgstr "Levels" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +msgid "viewer.header.rooms" +msgstr "Rooms" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +msgid "viewer.header.selection" +msgstr "Selection" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +msgid "viewer.header.units" +msgstr "Units" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +msgid "viewer.header.untitled" +msgstr "Untitled" + +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +msgid "viewer.header.zones" +msgstr "Zones" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +msgid "viewer.levels.exploded" +msgstr "Exploded" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +msgid "viewer.levels.label" +msgstr "Levels: {mode}" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +msgid "viewer.levels.manual" +msgstr "Manual" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +msgid "viewer.levels.solo" +msgstr "Solo" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +msgid "viewer.levels.stacked" +msgstr "Stacked" + +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +msgid "viewer.stage.layout" +msgstr "Viewer layout" + +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +msgid "viewer.stage.split" +msgstr "Split" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +msgid "viewer.theme.blueprint" +msgstr "Blueprint" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +msgid "viewer.theme.mediterranean" +msgstr "Mediterranean" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +msgid "viewer.theme.night" +msgstr "Night" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +msgid "viewer.theme.overcast" +msgstr "Overcast" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +msgid "viewer.theme.paper" +msgstr "Paper" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +msgid "viewer.theme.studio" +msgstr "Studio" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +msgid "viewer.theme.sunset" +msgstr "Sunset" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +msgid "viewer.theme.title" +msgstr "Scene theme" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +msgid "viewer.theme.twilight" +msgstr "Twilight" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +msgid "viewer.theme.verdant" +msgstr "Verdant" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +msgid "viewer.visibility.guides" +msgstr "Guides" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +msgid "viewer.visibility.scans" +msgstr "Scans" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +msgid "viewer.visibility.title" +msgstr "Visibility" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +msgid "viewer.walkthrough.label" +msgstr "Walkthrough: {state}" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +msgid "viewer.walls.cutaway" +msgstr "Cutaway" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +msgid "viewer.walls.fullHeight" +msgstr "Full height" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +msgid "viewer.walls.label" +msgstr "Walls: {mode}" + +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +msgid "viewer.walls.low" +msgstr "Low" diff --git a/packages/editor/src/i18n/locales/en/messages.ts b/packages/editor/src/i18n/locales/en/messages.ts new file mode 100644 index 0000000000..0cd37f56df --- /dev/null +++ b/packages/editor/src/i18n/locales/en/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"core.level.basement\":[\"Basement\"],\"core.level.basementNumber\":[\"Basement \",[\"number\"]],\"core.level.floorNumber\":[\"Floor \",[\"number\"]],\"core.level.groundFloor\":[\"Ground floor\"],\"editor.buildPanel.advanced\":[\"Advanced\"],\"editor.buildPanel.rooms.hint\":[\"walls · floor · ceiling\"],\"editor.buildPanel.rooms.idle\":[\"Pick a shape to start. Every room brings its own walls, floor and ceiling.\"],\"editor.buildPanel.rooms.polygon.caption\":[[\"lead\"],\" then the first one again to close. For L-shapes and angled rooms.\"],\"editor.buildPanel.rooms.polygon.label\":[\"Polygon\"],\"editor.buildPanel.rooms.polygon.lead\":[\"Click each corner,\"],\"editor.buildPanel.rooms.polygon.title\":[\"Polygon room\"],\"editor.buildPanel.rooms.rectangle.caption\":[[\"lead\"],\" on the grid. Walls, floor and ceiling are created together.\"],\"editor.buildPanel.rooms.rectangle.label\":[\"Rectangle\"],\"editor.buildPanel.rooms.rectangle.lead\":[\"Click two corners\"],\"editor.buildPanel.rooms.rectangle.title\":[\"Rectangle room\"],\"editor.buildPanel.rooms.title\":[\"Rooms\"],\"editor.buildPanel.rooms.walls.caption\":[[\"lead\"],\" Close a loop and it becomes a room; leave it open for partitions.\"],\"editor.buildPanel.rooms.walls.label\":[\"Walls\"],\"editor.buildPanel.rooms.walls.lead\":[\"Draw one wall at a time.\"],\"editor.buildPanel.rooms.walls.title\":[\"Walls\"],\"viewer.camera.orbitLeft\":[\"Orbit left\"],\"viewer.camera.orbitRight\":[\"Orbit right\"],\"viewer.camera.topView\":[\"Top view\"],\"viewer.common.off\":[\"Off\"],\"viewer.common.on\":[\"On\"],\"viewer.display.camera\":[\"Camera\"],\"viewer.display.colored\":[\"Colored\"],\"viewer.display.colors\":[\"Colors\"],\"viewer.display.edges.off\":[\"Off\"],\"viewer.display.edges.offDetail\":[\"No edge lines\"],\"viewer.display.edges.soft\":[\"Soft\"],\"viewer.display.edges.softDetail\":[\"Faint outline of major creases\"],\"viewer.display.edges.strong\":[\"Strong\"],\"viewer.display.edges.strongDetail\":[\"Crisp, opaque edge lines\"],\"viewer.display.edges.title\":[\"Edges\"],\"viewer.display.monochrome\":[\"Monochrome\"],\"viewer.display.orthographic\":[\"Orthographic\"],\"viewer.display.perspective\":[\"Perspective\"],\"viewer.display.render.rendered\":[\"Rendered\"],\"viewer.display.render.renderedDetail\":[\"Full ambient occlusion\"],\"viewer.display.render.solid\":[\"Solid\"],\"viewer.display.render.solidDetail\":[\"Flat and fast — no ambient occlusion\"],\"viewer.display.render.title\":[\"Render\"],\"viewer.display.shadows\":[\"Shadows\"],\"viewer.display.title\":[\"Display settings\"],\"viewer.floorplan.alignNorth\":[\"Align view to north\"],\"viewer.floorplan.empty\":[\"No floor plans are available for this scene.\"],\"viewer.floorplan.fit\":[\"Fit floor plan\"],\"viewer.floorplan.floor\":[\"Floor\"],\"viewer.floorplan.floorPlan\":[\"Floor plan\"],\"viewer.floorplan.viewLabel\":[[\"floor\"],\" 2D view\"],\"viewer.floorplan.zoomIn\":[\"Zoom in\"],\"viewer.floorplan.zoomOut\":[\"Zoom out\"],\"viewer.header.back\":[\"Back\"],\"viewer.header.levels\":[\"Levels\"],\"viewer.header.rooms\":[\"Rooms\"],\"viewer.header.selection\":[\"Selection\"],\"viewer.header.units\":[\"Units\"],\"viewer.header.untitled\":[\"Untitled\"],\"viewer.header.zones\":[\"Zones\"],\"viewer.levels.exploded\":[\"Exploded\"],\"viewer.levels.label\":[\"Levels: \",[\"mode\"]],\"viewer.levels.manual\":[\"Manual\"],\"viewer.levels.solo\":[\"Solo\"],\"viewer.levels.stacked\":[\"Stacked\"],\"viewer.stage.layout\":[\"Viewer layout\"],\"viewer.stage.split\":[\"Split\"],\"viewer.theme.blueprint\":[\"Blueprint\"],\"viewer.theme.mediterranean\":[\"Mediterranean\"],\"viewer.theme.night\":[\"Night\"],\"viewer.theme.overcast\":[\"Overcast\"],\"viewer.theme.paper\":[\"Paper\"],\"viewer.theme.studio\":[\"Studio\"],\"viewer.theme.sunset\":[\"Sunset\"],\"viewer.theme.title\":[\"Scene theme\"],\"viewer.theme.twilight\":[\"Twilight\"],\"viewer.theme.verdant\":[\"Verdant\"],\"viewer.visibility.guides\":[\"Guides\"],\"viewer.visibility.scans\":[\"Scans\"],\"viewer.visibility.title\":[\"Visibility\"],\"viewer.walkthrough.label\":[\"Walkthrough: \",[\"state\"]],\"viewer.walls.cutaway\":[\"Cutaway\"],\"viewer.walls.fullHeight\":[\"Full height\"],\"viewer.walls.label\":[\"Walls: \",[\"mode\"]],\"viewer.walls.low\":[\"Low\"]}")as Messages; \ No newline at end of file diff --git a/packages/editor/src/i18n/locales/es/messages.po b/packages/editor/src/i18n/locales/es/messages.po new file mode 100644 index 0000000000..e2a5302907 --- /dev/null +++ b/packages/editor/src/i18n/locales/es/messages.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2026-10-26 22:28+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: es\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +# Source-Hash: 8caa959c0ae800c3 +# Review: human +# English: "Basement" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +msgid "core.level.basement" +msgstr "Sótano" + +# Source-Hash: 14927085ee91ba05 +# Review: human +# English: "Basement {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +msgid "core.level.basementNumber" +msgstr "Sótano {number}" + +# Source-Hash: e59074a7b9b3f56e +# Review: human +# English: "Floor {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +msgid "core.level.floorNumber" +msgstr "Planta {number}" + +# Source-Hash: cde3efc9b7ddfa41 +# Review: human +# English: "Ground floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +msgid "core.level.groundFloor" +msgstr "Planta baja" + +# Source-Hash: f571cb2d06ae01ae +# Review: human +# English: "Advanced" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +msgid "editor.buildPanel.advanced" +msgstr "Avanzado" + +# Source-Hash: fb40866c2d6db5fb +# Review: human +# English: "walls · floor · ceiling" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +msgid "editor.buildPanel.rooms.hint" +msgstr "muros · suelo · techo" + +# Source-Hash: 271039bd919dc4c1 +# Review: human +# English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +msgid "editor.buildPanel.rooms.idle" +msgstr "Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo." + +# Source-Hash: ae2800b6c9e2b6ed +# Review: human +# English: "{lead} then the first one again to close. For L-shapes and angled rooms." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +msgid "editor.buildPanel.rooms.polygon.caption" +msgstr "{lead} y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos." + +# Source-Hash: 4e186fc057733162 +# Review: human +# English: "Polygon" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +msgid "editor.buildPanel.rooms.polygon.label" +msgstr "Polígono" + +# Source-Hash: 345e3aca3879b5c5 +# Review: human +# English: "Click each corner," +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +msgid "editor.buildPanel.rooms.polygon.lead" +msgstr "Haz clic en cada esquina" + +# Source-Hash: 8ec6152e139b0e3f +# Review: human +# English: "Polygon room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +msgid "editor.buildPanel.rooms.polygon.title" +msgstr "Estancia poligonal" + +# Source-Hash: 9b46295eedcbedae +# Review: human +# English: "{lead} on the grid. Walls, floor and ceiling are created together." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +msgid "editor.buildPanel.rooms.rectangle.caption" +msgstr "{lead} en la cuadrícula. Los muros, el suelo y el techo se crean a la vez." + +# Source-Hash: 2c2c20272b0370cc +# Review: human +# English: "Rectangle" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +msgid "editor.buildPanel.rooms.rectangle.label" +msgstr "Rectángulo" + +# Source-Hash: 060faa48e299d7c9 +# Review: human +# English: "Click two corners" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +msgid "editor.buildPanel.rooms.rectangle.lead" +msgstr "Haz clic en dos esquinas" + +# Source-Hash: 83a45603ac7f41e8 +# Review: human +# English: "Rectangle room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +msgid "editor.buildPanel.rooms.rectangle.title" +msgstr "Estancia rectangular" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +msgid "editor.buildPanel.rooms.title" +msgstr "Estancias" + +# Source-Hash: 8a21a03c4acf396b +# Review: human +# English: "{lead} Close a loop and it becomes a room; leave it open for partitions." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +msgid "editor.buildPanel.rooms.walls.caption" +msgstr "{lead} Cierra el contorno para crear una estancia o déjalo abierto para tabiques." + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +msgid "editor.buildPanel.rooms.walls.label" +msgstr "Muros" + +# Source-Hash: 9843edd68426fa2d +# Review: human +# English: "Draw one wall at a time." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +msgid "editor.buildPanel.rooms.walls.lead" +msgstr "Dibuja un muro cada vez." + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +msgid "editor.buildPanel.rooms.walls.title" +msgstr "Muros" + +# Source-Hash: 8bf25afa3cb8baea +# Review: human +# English: "Orbit left" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +msgid "viewer.camera.orbitLeft" +msgstr "Orbitar a la izquierda" + +# Source-Hash: b9cc64c1d1bb0d46 +# Review: human +# English: "Orbit right" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +msgid "viewer.camera.orbitRight" +msgstr "Orbitar a la derecha" + +# Source-Hash: 1ab1eb3a14f4b0e0 +# Review: human +# English: "Top view" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +msgid "viewer.camera.topView" +msgstr "Vista superior" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +msgid "viewer.common.off" +msgstr "Desactivado" + +# Source-Hash: 9cc01faad79c28da +# Review: human +# English: "On" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +msgid "viewer.common.on" +msgstr "Activado" + +# Source-Hash: 14622745dbf93f4c +# Review: human +# English: "Camera" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +msgid "viewer.display.camera" +msgstr "Cámara" + +# Source-Hash: d3d478fa6c6d7ddc +# Review: human +# English: "Colored" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +msgid "viewer.display.colored" +msgstr "A color" + +# Source-Hash: 82b74c4f5ef43336 +# Review: human +# English: "Colors" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +msgid "viewer.display.colors" +msgstr "Colores" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +msgid "viewer.display.edges.off" +msgstr "Desactivadas" + +# Source-Hash: 4ff58913a4492199 +# Review: human +# English: "No edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +msgid "viewer.display.edges.offDetail" +msgstr "Sin líneas de arista" + +# Source-Hash: d266d83ec490df4e +# Review: human +# English: "Soft" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +msgid "viewer.display.edges.soft" +msgstr "Suaves" + +# Source-Hash: 0c583f1d4201293b +# Review: human +# English: "Faint outline of major creases" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +msgid "viewer.display.edges.softDetail" +msgstr "Contorno tenue de los pliegues principales" + +# Source-Hash: f82927a01d4c5e45 +# Review: human +# English: "Strong" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +msgid "viewer.display.edges.strong" +msgstr "Marcadas" + +# Source-Hash: 2416a48df28d6205 +# Review: human +# English: "Crisp, opaque edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +msgid "viewer.display.edges.strongDetail" +msgstr "Líneas de arista nítidas y opacas" + +# Source-Hash: 027bbc8b7287c06a +# Review: human +# English: "Edges" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +msgid "viewer.display.edges.title" +msgstr "Aristas" + +# Source-Hash: ecde2f7c26914be3 +# Review: human +# English: "Monochrome" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +msgid "viewer.display.monochrome" +msgstr "Monocromo" + +# Source-Hash: ed59dd8ce7797a66 +# Review: human +# English: "Orthographic" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +msgid "viewer.display.orthographic" +msgstr "Ortográfica" + +# Source-Hash: 316e25993ff72b88 +# Review: human +# English: "Perspective" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +msgid "viewer.display.perspective" +msgstr "Perspectiva" + +# Source-Hash: f363702a10aedaeb +# Review: human +# English: "Rendered" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +msgid "viewer.display.render.rendered" +msgstr "Realista" + +# Source-Hash: 8bb9e54fc786ca77 +# Review: human +# English: "Full ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +msgid "viewer.display.render.renderedDetail" +msgstr "Oclusión ambiental completa" + +# Source-Hash: ef3e765e692faf4c +# Review: human +# English: "Solid" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +msgid "viewer.display.render.solid" +msgstr "Sólido" + +# Source-Hash: 8827df1cdeb02ed9 +# Review: human +# English: "Flat and fast — no ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +msgid "viewer.display.render.solidDetail" +msgstr "Plano y rápido, sin oclusión ambiental" + +# Source-Hash: 5e58af99d6d4fa6a +# Review: human +# English: "Render" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +msgid "viewer.display.render.title" +msgstr "Renderizado" + +# Source-Hash: 4c6bcb30b40047fc +# Review: human +# English: "Shadows" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +msgid "viewer.display.shadows" +msgstr "Sombras" + +# Source-Hash: d0db46f206a88e2e +# Review: human +# English: "Display settings" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +msgid "viewer.display.title" +msgstr "Ajustes de visualización" + +# Source-Hash: 2bdf83801d4a40af +# Review: human +# English: "Align view to north" +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +msgid "viewer.floorplan.alignNorth" +msgstr "Orientar la vista al norte" + +# Source-Hash: 286a757d8b52bb02 +# Review: human +# English: "No floor plans are available for this scene." +#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +msgid "viewer.floorplan.empty" +msgstr "No hay planos de planta disponibles para esta escena." + +# Source-Hash: bf63c679ab94dbcd +# Review: human +# English: "Fit floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +msgid "viewer.floorplan.fit" +msgstr "Ajustar plano" + +# Source-Hash: d7148fa065bc709b +# Review: human +# English: "Floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +msgid "viewer.floorplan.floor" +msgstr "Planta" + +# Source-Hash: fee6203bb9046929 +# Review: human +# English: "Floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +msgid "viewer.floorplan.floorPlan" +msgstr "Plano de planta" + +# Source-Hash: 28b255b7bf5f1084 +# Review: human +# English: "{floor} 2D view" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +msgid "viewer.floorplan.viewLabel" +msgstr "Vista 2D de {floor}" + +# Source-Hash: d17804412f8dd081 +# Review: human +# English: "Zoom in" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +msgid "viewer.floorplan.zoomIn" +msgstr "Acercar" + +# Source-Hash: 23b012a283b2c14a +# Review: human +# English: "Zoom out" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +msgid "viewer.floorplan.zoomOut" +msgstr "Alejar" + +# Source-Hash: 7ceade8f25b58fe9 +# Review: human +# English: "Back" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +msgid "viewer.header.back" +msgstr "Volver" + +# Source-Hash: cc3d601d59e2e26c +# Review: human +# English: "Levels" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +msgid "viewer.header.levels" +msgstr "Plantas" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +msgid "viewer.header.rooms" +msgstr "Estancias" + +# Source-Hash: 170b2049ef0f9131 +# Review: human +# English: "Selection" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +msgid "viewer.header.selection" +msgstr "Selección" + +# Source-Hash: b404dded35241274 +# Review: human +# English: "Units" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +msgid "viewer.header.units" +msgstr "Unidades" + +# Source-Hash: 6547a4c0c368042a +# Review: human +# English: "Untitled" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +msgid "viewer.header.untitled" +msgstr "Sin título" + +# Source-Hash: 665db49a26f81425 +# Review: human +# English: "Zones" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +msgid "viewer.header.zones" +msgstr "Zonas" + +# Source-Hash: be12f8788f1f0a05 +# Review: human +# English: "Exploded" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +msgid "viewer.levels.exploded" +msgstr "Separadas" + +# Source-Hash: 4bf4a6861d7d633e +# Review: human +# English: "Levels: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +msgid "viewer.levels.label" +msgstr "Plantas: {mode}" + +# Source-Hash: 387082cd5f118bed +# Review: human +# English: "Manual" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +msgid "viewer.levels.manual" +msgstr "Manual" + +# Source-Hash: e9f59b513f2f45ac +# Review: human +# English: "Solo" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +msgid "viewer.levels.solo" +msgstr "Aislada" + +# Source-Hash: c203c84eee18c0f6 +# Review: human +# English: "Stacked" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +msgid "viewer.levels.stacked" +msgstr "Apiladas" + +# Source-Hash: 9dd21ca47cffa3b8 +# Review: human +# English: "Viewer layout" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +msgid "viewer.stage.layout" +msgstr "Disposición de vistas" + +# Source-Hash: e4cd64b1680ac0d2 +# Review: human +# English: "Split" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +msgid "viewer.stage.split" +msgstr "Dividida" + +# Source-Hash: f14b89d713d2528a +# Review: human +# English: "Blueprint" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +msgid "viewer.theme.blueprint" +msgstr "Plano técnico" + +# Source-Hash: 142bcb2841f423c5 +# Review: human +# English: "Mediterranean" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +msgid "viewer.theme.mediterranean" +msgstr "Mediterráneo" + +# Source-Hash: b399942ec5d7e1ba +# Review: human +# English: "Night" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +msgid "viewer.theme.night" +msgstr "Noche" + +# Source-Hash: f512d1b947b8ae5a +# Review: human +# English: "Overcast" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +msgid "viewer.theme.overcast" +msgstr "Nublado" + +# Source-Hash: 583542b3bc837192 +# Review: human +# English: "Paper" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +msgid "viewer.theme.paper" +msgstr "Papel" + +# Source-Hash: 7abc2dc6e39b1747 +# Review: human +# English: "Studio" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +msgid "viewer.theme.studio" +msgstr "Estudio" + +# Source-Hash: efe94d3ec8d8c44e +# Review: human +# English: "Sunset" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +msgid "viewer.theme.sunset" +msgstr "Atardecer" + +# Source-Hash: 82720864e5034398 +# Review: human +# English: "Scene theme" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +msgid "viewer.theme.title" +msgstr "Ambiente" + +# Source-Hash: 4c10ea0aa0e2ea41 +# Review: human +# English: "Twilight" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +msgid "viewer.theme.twilight" +msgstr "Crepúsculo" + +# Source-Hash: f9592fb0672740c0 +# Review: human +# English: "Verdant" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +msgid "viewer.theme.verdant" +msgstr "Verde" + +# Source-Hash: 892d5dd34372028b +# Review: human +# English: "Guides" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +msgid "viewer.visibility.guides" +msgstr "Guías" + +# Source-Hash: 3260bfa69cca50e5 +# Review: human +# English: "Scans" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +msgid "viewer.visibility.scans" +msgstr "Escaneos 3D" + +# Source-Hash: 2cefbb554eadbb6e +# Review: human +# English: "Visibility" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +msgid "viewer.visibility.title" +msgstr "Visibilidad" + +# Source-Hash: 31afd9dee47ba943 +# Review: human +# English: "Walkthrough: {state}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +msgid "viewer.walkthrough.label" +msgstr "Recorrido: {state}" + +# Source-Hash: 4da8b3678ed77b45 +# Review: human +# English: "Cutaway" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +msgid "viewer.walls.cutaway" +msgstr "Vista seccionada" + +# Source-Hash: 131bac2467f4fdf6 +# Review: human +# English: "Full height" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +msgid "viewer.walls.fullHeight" +msgstr "Altura completa" + +# Source-Hash: 32f3d058cf2526d2 +# Review: human +# English: "Walls: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +msgid "viewer.walls.label" +msgstr "Muros: {mode}" + +# Source-Hash: 803532407fb5962b +# Review: human +# English: "Low" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +msgid "viewer.walls.low" +msgstr "Bajos" diff --git a/packages/editor/src/i18n/locales/es/messages.ts b/packages/editor/src/i18n/locales/es/messages.ts new file mode 100644 index 0000000000..24fa7da3af --- /dev/null +++ b/packages/editor/src/i18n/locales/es/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"core.level.basement\":[\"Sótano\"],\"core.level.basementNumber\":[\"Sótano \",[\"number\"]],\"core.level.floorNumber\":[\"Planta \",[\"number\"]],\"core.level.groundFloor\":[\"Planta baja\"],\"editor.buildPanel.advanced\":[\"Avanzado\"],\"editor.buildPanel.rooms.hint\":[\"muros · suelo · techo\"],\"editor.buildPanel.rooms.idle\":[\"Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo.\"],\"editor.buildPanel.rooms.polygon.caption\":[[\"lead\"],\" y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos.\"],\"editor.buildPanel.rooms.polygon.label\":[\"Polígono\"],\"editor.buildPanel.rooms.polygon.lead\":[\"Haz clic en cada esquina\"],\"editor.buildPanel.rooms.polygon.title\":[\"Estancia poligonal\"],\"editor.buildPanel.rooms.rectangle.caption\":[[\"lead\"],\" en la cuadrícula. Los muros, el suelo y el techo se crean a la vez.\"],\"editor.buildPanel.rooms.rectangle.label\":[\"Rectángulo\"],\"editor.buildPanel.rooms.rectangle.lead\":[\"Haz clic en dos esquinas\"],\"editor.buildPanel.rooms.rectangle.title\":[\"Estancia rectangular\"],\"editor.buildPanel.rooms.title\":[\"Estancias\"],\"editor.buildPanel.rooms.walls.caption\":[[\"lead\"],\" Cierra el contorno para crear una estancia o déjalo abierto para tabiques.\"],\"editor.buildPanel.rooms.walls.label\":[\"Muros\"],\"editor.buildPanel.rooms.walls.lead\":[\"Dibuja un muro cada vez.\"],\"editor.buildPanel.rooms.walls.title\":[\"Muros\"],\"viewer.camera.orbitLeft\":[\"Orbitar a la izquierda\"],\"viewer.camera.orbitRight\":[\"Orbitar a la derecha\"],\"viewer.camera.topView\":[\"Vista superior\"],\"viewer.common.off\":[\"Desactivado\"],\"viewer.common.on\":[\"Activado\"],\"viewer.display.camera\":[\"Cámara\"],\"viewer.display.colored\":[\"A color\"],\"viewer.display.colors\":[\"Colores\"],\"viewer.display.edges.off\":[\"Desactivadas\"],\"viewer.display.edges.offDetail\":[\"Sin líneas de arista\"],\"viewer.display.edges.soft\":[\"Suaves\"],\"viewer.display.edges.softDetail\":[\"Contorno tenue de los pliegues principales\"],\"viewer.display.edges.strong\":[\"Marcadas\"],\"viewer.display.edges.strongDetail\":[\"Líneas de arista nítidas y opacas\"],\"viewer.display.edges.title\":[\"Aristas\"],\"viewer.display.monochrome\":[\"Monocromo\"],\"viewer.display.orthographic\":[\"Ortográfica\"],\"viewer.display.perspective\":[\"Perspectiva\"],\"viewer.display.render.rendered\":[\"Realista\"],\"viewer.display.render.renderedDetail\":[\"Oclusión ambiental completa\"],\"viewer.display.render.solid\":[\"Sólido\"],\"viewer.display.render.solidDetail\":[\"Plano y rápido, sin oclusión ambiental\"],\"viewer.display.render.title\":[\"Renderizado\"],\"viewer.display.shadows\":[\"Sombras\"],\"viewer.display.title\":[\"Ajustes de visualización\"],\"viewer.floorplan.alignNorth\":[\"Orientar la vista al norte\"],\"viewer.floorplan.empty\":[\"No hay planos de planta disponibles para esta escena.\"],\"viewer.floorplan.fit\":[\"Ajustar plano\"],\"viewer.floorplan.floor\":[\"Planta\"],\"viewer.floorplan.floorPlan\":[\"Plano de planta\"],\"viewer.floorplan.viewLabel\":[\"Vista 2D de \",[\"floor\"]],\"viewer.floorplan.zoomIn\":[\"Acercar\"],\"viewer.floorplan.zoomOut\":[\"Alejar\"],\"viewer.header.back\":[\"Volver\"],\"viewer.header.levels\":[\"Plantas\"],\"viewer.header.rooms\":[\"Estancias\"],\"viewer.header.selection\":[\"Selección\"],\"viewer.header.units\":[\"Unidades\"],\"viewer.header.untitled\":[\"Sin título\"],\"viewer.header.zones\":[\"Zonas\"],\"viewer.levels.exploded\":[\"Separadas\"],\"viewer.levels.label\":[\"Plantas: \",[\"mode\"]],\"viewer.levels.manual\":[\"Manual\"],\"viewer.levels.solo\":[\"Aislada\"],\"viewer.levels.stacked\":[\"Apiladas\"],\"viewer.stage.layout\":[\"Disposición de vistas\"],\"viewer.stage.split\":[\"Dividida\"],\"viewer.theme.blueprint\":[\"Plano técnico\"],\"viewer.theme.mediterranean\":[\"Mediterráneo\"],\"viewer.theme.night\":[\"Noche\"],\"viewer.theme.overcast\":[\"Nublado\"],\"viewer.theme.paper\":[\"Papel\"],\"viewer.theme.studio\":[\"Estudio\"],\"viewer.theme.sunset\":[\"Atardecer\"],\"viewer.theme.title\":[\"Ambiente\"],\"viewer.theme.twilight\":[\"Crepúsculo\"],\"viewer.theme.verdant\":[\"Verde\"],\"viewer.visibility.guides\":[\"Guías\"],\"viewer.visibility.scans\":[\"Escaneos 3D\"],\"viewer.visibility.title\":[\"Visibilidad\"],\"viewer.walkthrough.label\":[\"Recorrido: \",[\"state\"]],\"viewer.walls.cutaway\":[\"Vista seccionada\"],\"viewer.walls.fullHeight\":[\"Altura completa\"],\"viewer.walls.label\":[\"Muros: \",[\"mode\"]],\"viewer.walls.low\":[\"Bajos\"]}")as Messages; \ No newline at end of file diff --git a/packages/editor/src/i18n/locales/fr/messages.po b/packages/editor/src/i18n/locales/fr/messages.po new file mode 100644 index 0000000000..86eddafe95 --- /dev/null +++ b/packages/editor/src/i18n/locales/fr/messages.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2026-10-26 22:28+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: fr\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +# Source-Hash: 8caa959c0ae800c3 +# Review: human +# English: "Basement" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +msgid "core.level.basement" +msgstr "Sous-sol" + +# Source-Hash: 14927085ee91ba05 +# Review: human +# English: "Basement {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +msgid "core.level.basementNumber" +msgstr "Sous-sol {number}" + +# Source-Hash: e59074a7b9b3f56e +# Review: human +# English: "Floor {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +msgid "core.level.floorNumber" +msgstr "Étage {number}" + +# Source-Hash: cde3efc9b7ddfa41 +# Review: human +# English: "Ground floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +msgid "core.level.groundFloor" +msgstr "Rez-de-chaussée" + +# Source-Hash: f571cb2d06ae01ae +# Review: human +# English: "Advanced" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +msgid "editor.buildPanel.advanced" +msgstr "Avancé" + +# Source-Hash: fb40866c2d6db5fb +# Review: human +# English: "walls · floor · ceiling" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +msgid "editor.buildPanel.rooms.hint" +msgstr "murs · sol · plafond" + +# Source-Hash: 271039bd919dc4c1 +# Review: human +# English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +msgid "editor.buildPanel.rooms.idle" +msgstr "Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond." + +# Source-Hash: ae2800b6c9e2b6ed +# Review: human +# English: "{lead} then the first one again to close. For L-shapes and angled rooms." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +msgid "editor.buildPanel.rooms.polygon.caption" +msgstr "{lead} puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques." + +# Source-Hash: 4e186fc057733162 +# Review: human +# English: "Polygon" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +msgid "editor.buildPanel.rooms.polygon.label" +msgstr "Polygone" + +# Source-Hash: 345e3aca3879b5c5 +# Review: human +# English: "Click each corner," +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +msgid "editor.buildPanel.rooms.polygon.lead" +msgstr "Cliquez sur chaque angle," + +# Source-Hash: 8ec6152e139b0e3f +# Review: human +# English: "Polygon room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +msgid "editor.buildPanel.rooms.polygon.title" +msgstr "Pièce polygonale" + +# Source-Hash: 9b46295eedcbedae +# Review: human +# English: "{lead} on the grid. Walls, floor and ceiling are created together." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +msgid "editor.buildPanel.rooms.rectangle.caption" +msgstr "{lead} sur la grille. Les murs, le sol et le plafond sont créés ensemble." + +# Source-Hash: 2c2c20272b0370cc +# Review: human +# English: "Rectangle" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +msgid "editor.buildPanel.rooms.rectangle.label" +msgstr "Rectangle" + +# Source-Hash: 060faa48e299d7c9 +# Review: human +# English: "Click two corners" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +msgid "editor.buildPanel.rooms.rectangle.lead" +msgstr "Cliquez sur deux angles" + +# Source-Hash: 83a45603ac7f41e8 +# Review: human +# English: "Rectangle room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +msgid "editor.buildPanel.rooms.rectangle.title" +msgstr "Pièce rectangulaire" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +msgid "editor.buildPanel.rooms.title" +msgstr "Pièces" + +# Source-Hash: 8a21a03c4acf396b +# Review: human +# English: "{lead} Close a loop and it becomes a room; leave it open for partitions." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +msgid "editor.buildPanel.rooms.walls.caption" +msgstr "{lead} Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison." + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +msgid "editor.buildPanel.rooms.walls.label" +msgstr "Murs" + +# Source-Hash: 9843edd68426fa2d +# Review: human +# English: "Draw one wall at a time." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +msgid "editor.buildPanel.rooms.walls.lead" +msgstr "Tracez un mur à la fois." + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +msgid "editor.buildPanel.rooms.walls.title" +msgstr "Murs" + +# Source-Hash: 8bf25afa3cb8baea +# Review: human +# English: "Orbit left" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +msgid "viewer.camera.orbitLeft" +msgstr "Pivoter à gauche" + +# Source-Hash: b9cc64c1d1bb0d46 +# Review: human +# English: "Orbit right" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +msgid "viewer.camera.orbitRight" +msgstr "Pivoter à droite" + +# Source-Hash: 1ab1eb3a14f4b0e0 +# Review: human +# English: "Top view" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +msgid "viewer.camera.topView" +msgstr "Vue de dessus" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +msgid "viewer.common.off" +msgstr "Désactivé" + +# Source-Hash: 9cc01faad79c28da +# Review: human +# English: "On" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +msgid "viewer.common.on" +msgstr "Activé" + +# Source-Hash: 14622745dbf93f4c +# Review: human +# English: "Camera" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +msgid "viewer.display.camera" +msgstr "Caméra" + +# Source-Hash: d3d478fa6c6d7ddc +# Review: human +# English: "Colored" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +msgid "viewer.display.colored" +msgstr "En couleur" + +# Source-Hash: 82b74c4f5ef43336 +# Review: human +# English: "Colors" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +msgid "viewer.display.colors" +msgstr "Couleurs" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +msgid "viewer.display.edges.off" +msgstr "Désactivées" + +# Source-Hash: 4ff58913a4492199 +# Review: human +# English: "No edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +msgid "viewer.display.edges.offDetail" +msgstr "Aucune ligne d’arête" + +# Source-Hash: d266d83ec490df4e +# Review: human +# English: "Soft" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +msgid "viewer.display.edges.soft" +msgstr "Douces" + +# Source-Hash: 0c583f1d4201293b +# Review: human +# English: "Faint outline of major creases" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +msgid "viewer.display.edges.softDetail" +msgstr "Contour léger des plis principaux" + +# Source-Hash: f82927a01d4c5e45 +# Review: human +# English: "Strong" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +msgid "viewer.display.edges.strong" +msgstr "Marquées" + +# Source-Hash: 2416a48df28d6205 +# Review: human +# English: "Crisp, opaque edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +msgid "viewer.display.edges.strongDetail" +msgstr "Lignes d’arête nettes et opaques" + +# Source-Hash: 027bbc8b7287c06a +# Review: human +# English: "Edges" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +msgid "viewer.display.edges.title" +msgstr "Arêtes" + +# Source-Hash: ecde2f7c26914be3 +# Review: human +# English: "Monochrome" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +msgid "viewer.display.monochrome" +msgstr "Monochrome" + +# Source-Hash: ed59dd8ce7797a66 +# Review: human +# English: "Orthographic" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +msgid "viewer.display.orthographic" +msgstr "Orthographique" + +# Source-Hash: 316e25993ff72b88 +# Review: human +# English: "Perspective" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +msgid "viewer.display.perspective" +msgstr "Perspective" + +# Source-Hash: f363702a10aedaeb +# Review: human +# English: "Rendered" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +msgid "viewer.display.render.rendered" +msgstr "Réaliste" + +# Source-Hash: 8bb9e54fc786ca77 +# Review: human +# English: "Full ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +msgid "viewer.display.render.renderedDetail" +msgstr "Occlusion ambiante complète" + +# Source-Hash: ef3e765e692faf4c +# Review: human +# English: "Solid" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +msgid "viewer.display.render.solid" +msgstr "Solide" + +# Source-Hash: 8827df1cdeb02ed9 +# Review: human +# English: "Flat and fast — no ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +msgid "viewer.display.render.solidDetail" +msgstr "Rapide et uniforme, sans occlusion ambiante" + +# Source-Hash: 5e58af99d6d4fa6a +# Review: human +# English: "Render" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +msgid "viewer.display.render.title" +msgstr "Rendu" + +# Source-Hash: 4c6bcb30b40047fc +# Review: human +# English: "Shadows" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +msgid "viewer.display.shadows" +msgstr "Ombres" + +# Source-Hash: d0db46f206a88e2e +# Review: human +# English: "Display settings" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +msgid "viewer.display.title" +msgstr "Paramètres d’affichage" + +# Source-Hash: 2bdf83801d4a40af +# Review: human +# English: "Align view to north" +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +msgid "viewer.floorplan.alignNorth" +msgstr "Orienter la vue vers le nord" + +# Source-Hash: 286a757d8b52bb02 +# Review: human +# English: "No floor plans are available for this scene." +#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +msgid "viewer.floorplan.empty" +msgstr "Aucun plan d’étage n’est disponible pour cette scène." + +# Source-Hash: bf63c679ab94dbcd +# Review: human +# English: "Fit floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +msgid "viewer.floorplan.fit" +msgstr "Ajuster le plan" + +# Source-Hash: d7148fa065bc709b +# Review: human +# English: "Floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +msgid "viewer.floorplan.floor" +msgstr "Niveau" + +# Source-Hash: fee6203bb9046929 +# Review: human +# English: "Floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +msgid "viewer.floorplan.floorPlan" +msgstr "Plan d’étage" + +# Source-Hash: 28b255b7bf5f1084 +# Review: human +# English: "{floor} 2D view" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +msgid "viewer.floorplan.viewLabel" +msgstr "Vue 2D — {floor}" + +# Source-Hash: d17804412f8dd081 +# Review: human +# English: "Zoom in" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +msgid "viewer.floorplan.zoomIn" +msgstr "Zoom avant" + +# Source-Hash: 23b012a283b2c14a +# Review: human +# English: "Zoom out" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +msgid "viewer.floorplan.zoomOut" +msgstr "Zoom arrière" + +# Source-Hash: 7ceade8f25b58fe9 +# Review: human +# English: "Back" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +msgid "viewer.header.back" +msgstr "Retour" + +# Source-Hash: cc3d601d59e2e26c +# Review: human +# English: "Levels" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +msgid "viewer.header.levels" +msgstr "Niveaux" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +msgid "viewer.header.rooms" +msgstr "Pièces" + +# Source-Hash: 170b2049ef0f9131 +# Review: human +# English: "Selection" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +msgid "viewer.header.selection" +msgstr "Sélection" + +# Source-Hash: b404dded35241274 +# Review: human +# English: "Units" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +msgid "viewer.header.units" +msgstr "Unités" + +# Source-Hash: 6547a4c0c368042a +# Review: human +# English: "Untitled" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +msgid "viewer.header.untitled" +msgstr "Sans titre" + +# Source-Hash: 665db49a26f81425 +# Review: human +# English: "Zones" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +msgid "viewer.header.zones" +msgstr "Zones" + +# Source-Hash: be12f8788f1f0a05 +# Review: human +# English: "Exploded" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +msgid "viewer.levels.exploded" +msgstr "Éclatés" + +# Source-Hash: 4bf4a6861d7d633e +# Review: human +# English: "Levels: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +msgid "viewer.levels.label" +msgstr "Niveaux : {mode}" + +# Source-Hash: 387082cd5f118bed +# Review: human +# English: "Manual" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +msgid "viewer.levels.manual" +msgstr "Manuel" + +# Source-Hash: e9f59b513f2f45ac +# Review: human +# English: "Solo" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +msgid "viewer.levels.solo" +msgstr "Isolé" + +# Source-Hash: c203c84eee18c0f6 +# Review: human +# English: "Stacked" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +msgid "viewer.levels.stacked" +msgstr "Superposés" + +# Source-Hash: 9dd21ca47cffa3b8 +# Review: human +# English: "Viewer layout" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +msgid "viewer.stage.layout" +msgstr "Disposition des vues" + +# Source-Hash: e4cd64b1680ac0d2 +# Review: human +# English: "Split" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +msgid "viewer.stage.split" +msgstr "Partagé" + +# Source-Hash: f14b89d713d2528a +# Review: human +# English: "Blueprint" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +msgid "viewer.theme.blueprint" +msgstr "Plan" + +# Source-Hash: 142bcb2841f423c5 +# Review: human +# English: "Mediterranean" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +msgid "viewer.theme.mediterranean" +msgstr "Méditerranéen" + +# Source-Hash: b399942ec5d7e1ba +# Review: human +# English: "Night" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +msgid "viewer.theme.night" +msgstr "Nuit" + +# Source-Hash: f512d1b947b8ae5a +# Review: human +# English: "Overcast" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +msgid "viewer.theme.overcast" +msgstr "Ciel couvert" + +# Source-Hash: 583542b3bc837192 +# Review: human +# English: "Paper" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +msgid "viewer.theme.paper" +msgstr "Papier" + +# Source-Hash: 7abc2dc6e39b1747 +# Review: human +# English: "Studio" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +msgid "viewer.theme.studio" +msgstr "Studio" + +# Source-Hash: efe94d3ec8d8c44e +# Review: human +# English: "Sunset" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +msgid "viewer.theme.sunset" +msgstr "Coucher de soleil" + +# Source-Hash: 82720864e5034398 +# Review: human +# English: "Scene theme" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +msgid "viewer.theme.title" +msgstr "Ambiance" + +# Source-Hash: 4c10ea0aa0e2ea41 +# Review: human +# English: "Twilight" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +msgid "viewer.theme.twilight" +msgstr "Crépuscule" + +# Source-Hash: f9592fb0672740c0 +# Review: human +# English: "Verdant" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +msgid "viewer.theme.verdant" +msgstr "Verdoyant" + +# Source-Hash: 892d5dd34372028b +# Review: human +# English: "Guides" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +msgid "viewer.visibility.guides" +msgstr "Repères" + +# Source-Hash: 3260bfa69cca50e5 +# Review: human +# English: "Scans" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +msgid "viewer.visibility.scans" +msgstr "Relevés 3D" + +# Source-Hash: 2cefbb554eadbb6e +# Review: human +# English: "Visibility" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +msgid "viewer.visibility.title" +msgstr "Visibilité" + +# Source-Hash: 31afd9dee47ba943 +# Review: human +# English: "Walkthrough: {state}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +msgid "viewer.walkthrough.label" +msgstr "Visite : {state}" + +# Source-Hash: 4da8b3678ed77b45 +# Review: human +# English: "Cutaway" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +msgid "viewer.walls.cutaway" +msgstr "Vue en coupe" + +# Source-Hash: 131bac2467f4fdf6 +# Review: human +# English: "Full height" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +msgid "viewer.walls.fullHeight" +msgstr "Pleine hauteur" + +# Source-Hash: 32f3d058cf2526d2 +# Review: human +# English: "Walls: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +msgid "viewer.walls.label" +msgstr "Murs : {mode}" + +# Source-Hash: 803532407fb5962b +# Review: human +# English: "Low" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +msgid "viewer.walls.low" +msgstr "Bas" diff --git a/packages/editor/src/i18n/locales/fr/messages.ts b/packages/editor/src/i18n/locales/fr/messages.ts new file mode 100644 index 0000000000..42848704ea --- /dev/null +++ b/packages/editor/src/i18n/locales/fr/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"core.level.basement\":[\"Sous-sol\"],\"core.level.basementNumber\":[\"Sous-sol \",[\"number\"]],\"core.level.floorNumber\":[\"Étage \",[\"number\"]],\"core.level.groundFloor\":[\"Rez-de-chaussée\"],\"editor.buildPanel.advanced\":[\"Avancé\"],\"editor.buildPanel.rooms.hint\":[\"murs · sol · plafond\"],\"editor.buildPanel.rooms.idle\":[\"Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond.\"],\"editor.buildPanel.rooms.polygon.caption\":[[\"lead\"],\" puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques.\"],\"editor.buildPanel.rooms.polygon.label\":[\"Polygone\"],\"editor.buildPanel.rooms.polygon.lead\":[\"Cliquez sur chaque angle,\"],\"editor.buildPanel.rooms.polygon.title\":[\"Pièce polygonale\"],\"editor.buildPanel.rooms.rectangle.caption\":[[\"lead\"],\" sur la grille. Les murs, le sol et le plafond sont créés ensemble.\"],\"editor.buildPanel.rooms.rectangle.label\":[\"Rectangle\"],\"editor.buildPanel.rooms.rectangle.lead\":[\"Cliquez sur deux angles\"],\"editor.buildPanel.rooms.rectangle.title\":[\"Pièce rectangulaire\"],\"editor.buildPanel.rooms.title\":[\"Pièces\"],\"editor.buildPanel.rooms.walls.caption\":[[\"lead\"],\" Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison.\"],\"editor.buildPanel.rooms.walls.label\":[\"Murs\"],\"editor.buildPanel.rooms.walls.lead\":[\"Tracez un mur à la fois.\"],\"editor.buildPanel.rooms.walls.title\":[\"Murs\"],\"viewer.camera.orbitLeft\":[\"Pivoter à gauche\"],\"viewer.camera.orbitRight\":[\"Pivoter à droite\"],\"viewer.camera.topView\":[\"Vue de dessus\"],\"viewer.common.off\":[\"Désactivé\"],\"viewer.common.on\":[\"Activé\"],\"viewer.display.camera\":[\"Caméra\"],\"viewer.display.colored\":[\"En couleur\"],\"viewer.display.colors\":[\"Couleurs\"],\"viewer.display.edges.off\":[\"Désactivées\"],\"viewer.display.edges.offDetail\":[\"Aucune ligne d’arête\"],\"viewer.display.edges.soft\":[\"Douces\"],\"viewer.display.edges.softDetail\":[\"Contour léger des plis principaux\"],\"viewer.display.edges.strong\":[\"Marquées\"],\"viewer.display.edges.strongDetail\":[\"Lignes d’arête nettes et opaques\"],\"viewer.display.edges.title\":[\"Arêtes\"],\"viewer.display.monochrome\":[\"Monochrome\"],\"viewer.display.orthographic\":[\"Orthographique\"],\"viewer.display.perspective\":[\"Perspective\"],\"viewer.display.render.rendered\":[\"Réaliste\"],\"viewer.display.render.renderedDetail\":[\"Occlusion ambiante complète\"],\"viewer.display.render.solid\":[\"Solide\"],\"viewer.display.render.solidDetail\":[\"Rapide et uniforme, sans occlusion ambiante\"],\"viewer.display.render.title\":[\"Rendu\"],\"viewer.display.shadows\":[\"Ombres\"],\"viewer.display.title\":[\"Paramètres d’affichage\"],\"viewer.floorplan.alignNorth\":[\"Orienter la vue vers le nord\"],\"viewer.floorplan.empty\":[\"Aucun plan d’étage n’est disponible pour cette scène.\"],\"viewer.floorplan.fit\":[\"Ajuster le plan\"],\"viewer.floorplan.floor\":[\"Niveau\"],\"viewer.floorplan.floorPlan\":[\"Plan d’étage\"],\"viewer.floorplan.viewLabel\":[\"Vue 2D — \",[\"floor\"]],\"viewer.floorplan.zoomIn\":[\"Zoom avant\"],\"viewer.floorplan.zoomOut\":[\"Zoom arrière\"],\"viewer.header.back\":[\"Retour\"],\"viewer.header.levels\":[\"Niveaux\"],\"viewer.header.rooms\":[\"Pièces\"],\"viewer.header.selection\":[\"Sélection\"],\"viewer.header.units\":[\"Unités\"],\"viewer.header.untitled\":[\"Sans titre\"],\"viewer.header.zones\":[\"Zones\"],\"viewer.levels.exploded\":[\"Éclatés\"],\"viewer.levels.label\":[\"Niveaux : \",[\"mode\"]],\"viewer.levels.manual\":[\"Manuel\"],\"viewer.levels.solo\":[\"Isolé\"],\"viewer.levels.stacked\":[\"Superposés\"],\"viewer.stage.layout\":[\"Disposition des vues\"],\"viewer.stage.split\":[\"Partagé\"],\"viewer.theme.blueprint\":[\"Plan\"],\"viewer.theme.mediterranean\":[\"Méditerranéen\"],\"viewer.theme.night\":[\"Nuit\"],\"viewer.theme.overcast\":[\"Ciel couvert\"],\"viewer.theme.paper\":[\"Papier\"],\"viewer.theme.studio\":[\"Studio\"],\"viewer.theme.sunset\":[\"Coucher de soleil\"],\"viewer.theme.title\":[\"Ambiance\"],\"viewer.theme.twilight\":[\"Crépuscule\"],\"viewer.theme.verdant\":[\"Verdoyant\"],\"viewer.visibility.guides\":[\"Repères\"],\"viewer.visibility.scans\":[\"Relevés 3D\"],\"viewer.visibility.title\":[\"Visibilité\"],\"viewer.walkthrough.label\":[\"Visite : \",[\"state\"]],\"viewer.walls.cutaway\":[\"Vue en coupe\"],\"viewer.walls.fullHeight\":[\"Pleine hauteur\"],\"viewer.walls.label\":[\"Murs : \",[\"mode\"]],\"viewer.walls.low\":[\"Bas\"]}")as Messages; \ No newline at end of file diff --git a/packages/editor/src/i18n/locales/ja/messages.po b/packages/editor/src/i18n/locales/ja/messages.po new file mode 100644 index 0000000000..bc74dd7e86 --- /dev/null +++ b/packages/editor/src/i18n/locales/ja/messages.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2026-10-26 22:28+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: ja\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +# Source-Hash: 8caa959c0ae800c3 +# Review: human +# English: "Basement" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +msgid "core.level.basement" +msgstr "地下1階" + +# Source-Hash: 14927085ee91ba05 +# Review: human +# English: "Basement {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +msgid "core.level.basementNumber" +msgstr "地下{number}階" + +# Source-Hash: e59074a7b9b3f56e +# Review: human +# English: "Floor {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +msgid "core.level.floorNumber" +msgstr "{number}階" + +# Source-Hash: cde3efc9b7ddfa41 +# Review: human +# English: "Ground floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +msgid "core.level.groundFloor" +msgstr "1階" + +# Source-Hash: f571cb2d06ae01ae +# Review: human +# English: "Advanced" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +msgid "editor.buildPanel.advanced" +msgstr "詳細" + +# Source-Hash: fb40866c2d6db5fb +# Review: human +# English: "walls · floor · ceiling" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +msgid "editor.buildPanel.rooms.hint" +msgstr "壁 · 床 · 天井" + +# Source-Hash: 271039bd919dc4c1 +# Review: human +# English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +msgid "editor.buildPanel.rooms.idle" +msgstr "形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。" + +# Source-Hash: ae2800b6c9e2b6ed +# Review: human +# English: "{lead} then the first one again to close. For L-shapes and angled rooms." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +msgid "editor.buildPanel.rooms.polygon.caption" +msgstr "{lead}最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。" + +# Source-Hash: 4e186fc057733162 +# Review: human +# English: "Polygon" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +msgid "editor.buildPanel.rooms.polygon.label" +msgstr "多角形" + +# Source-Hash: 345e3aca3879b5c5 +# Review: human +# English: "Click each corner," +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +msgid "editor.buildPanel.rooms.polygon.lead" +msgstr "各頂点をクリックし、" + +# Source-Hash: 8ec6152e139b0e3f +# Review: human +# English: "Polygon room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +msgid "editor.buildPanel.rooms.polygon.title" +msgstr "多角形の部屋" + +# Source-Hash: 9b46295eedcbedae +# Review: human +# English: "{lead} on the grid. Walls, floor and ceiling are created together." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +msgid "editor.buildPanel.rooms.rectangle.caption" +msgstr "グリッド上で{lead}します。壁・床・天井をまとめて作成します。" + +# Source-Hash: 2c2c20272b0370cc +# Review: human +# English: "Rectangle" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +msgid "editor.buildPanel.rooms.rectangle.label" +msgstr "長方形" + +# Source-Hash: 060faa48e299d7c9 +# Review: human +# English: "Click two corners" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +msgid "editor.buildPanel.rooms.rectangle.lead" +msgstr "対角の2点をクリック" + +# Source-Hash: 83a45603ac7f41e8 +# Review: human +# English: "Rectangle room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +msgid "editor.buildPanel.rooms.rectangle.title" +msgstr "長方形の部屋" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +msgid "editor.buildPanel.rooms.title" +msgstr "部屋" + +# Source-Hash: 8a21a03c4acf396b +# Review: human +# English: "{lead} Close a loop and it becomes a room; leave it open for partitions." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +msgid "editor.buildPanel.rooms.walls.caption" +msgstr "{lead}輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。" + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +msgid "editor.buildPanel.rooms.walls.label" +msgstr "壁" + +# Source-Hash: 9843edd68426fa2d +# Review: human +# English: "Draw one wall at a time." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +msgid "editor.buildPanel.rooms.walls.lead" +msgstr "壁を1面ずつ描画します。" + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +msgid "editor.buildPanel.rooms.walls.title" +msgstr "壁" + +# Source-Hash: 8bf25afa3cb8baea +# Review: human +# English: "Orbit left" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +msgid "viewer.camera.orbitLeft" +msgstr "左に回転" + +# Source-Hash: b9cc64c1d1bb0d46 +# Review: human +# English: "Orbit right" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +msgid "viewer.camera.orbitRight" +msgstr "右に回転" + +# Source-Hash: 1ab1eb3a14f4b0e0 +# Review: human +# English: "Top view" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +msgid "viewer.camera.topView" +msgstr "上面図" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +msgid "viewer.common.off" +msgstr "オフ" + +# Source-Hash: 9cc01faad79c28da +# Review: human +# English: "On" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +msgid "viewer.common.on" +msgstr "オン" + +# Source-Hash: 14622745dbf93f4c +# Review: human +# English: "Camera" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +msgid "viewer.display.camera" +msgstr "カメラ" + +# Source-Hash: d3d478fa6c6d7ddc +# Review: human +# English: "Colored" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +msgid "viewer.display.colored" +msgstr "カラー表示" + +# Source-Hash: 82b74c4f5ef43336 +# Review: human +# English: "Colors" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +msgid "viewer.display.colors" +msgstr "カラー" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +msgid "viewer.display.edges.off" +msgstr "オフ" + +# Source-Hash: 4ff58913a4492199 +# Review: human +# English: "No edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +msgid "viewer.display.edges.offDetail" +msgstr "エッジ線を表示しない" + +# Source-Hash: d266d83ec490df4e +# Review: human +# English: "Soft" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +msgid "viewer.display.edges.soft" +msgstr "ソフト" + +# Source-Hash: 0c583f1d4201293b +# Review: human +# English: "Faint outline of major creases" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +msgid "viewer.display.edges.softDetail" +msgstr "主要な稜線を薄く表示" + +# Source-Hash: f82927a01d4c5e45 +# Review: human +# English: "Strong" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +msgid "viewer.display.edges.strong" +msgstr "強調" + +# Source-Hash: 2416a48df28d6205 +# Review: human +# English: "Crisp, opaque edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +msgid "viewer.display.edges.strongDetail" +msgstr "エッジ線をくっきり表示" + +# Source-Hash: 027bbc8b7287c06a +# Review: human +# English: "Edges" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +msgid "viewer.display.edges.title" +msgstr "エッジ" + +# Source-Hash: ecde2f7c26914be3 +# Review: human +# English: "Monochrome" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +msgid "viewer.display.monochrome" +msgstr "モノクロ" + +# Source-Hash: ed59dd8ce7797a66 +# Review: human +# English: "Orthographic" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +msgid "viewer.display.orthographic" +msgstr "平行投影" + +# Source-Hash: 316e25993ff72b88 +# Review: human +# English: "Perspective" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +msgid "viewer.display.perspective" +msgstr "透視投影" + +# Source-Hash: f363702a10aedaeb +# Review: human +# English: "Rendered" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +msgid "viewer.display.render.rendered" +msgstr "レンダリング" + +# Source-Hash: 8bb9e54fc786ca77 +# Review: human +# English: "Full ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +msgid "viewer.display.render.renderedDetail" +msgstr "アンビエントオクルージョンを完全表示" + +# Source-Hash: ef3e765e692faf4c +# Review: human +# English: "Solid" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +msgid "viewer.display.render.solid" +msgstr "ソリッド" + +# Source-Hash: 8827df1cdeb02ed9 +# Review: human +# English: "Flat and fast — no ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +msgid "viewer.display.render.solidDetail" +msgstr "軽量表示(アンビエントオクルージョンなし)" + +# Source-Hash: 5e58af99d6d4fa6a +# Review: human +# English: "Render" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +msgid "viewer.display.render.title" +msgstr "レンダリング" + +# Source-Hash: 4c6bcb30b40047fc +# Review: human +# English: "Shadows" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +msgid "viewer.display.shadows" +msgstr "影" + +# Source-Hash: d0db46f206a88e2e +# Review: human +# English: "Display settings" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +msgid "viewer.display.title" +msgstr "表示設定" + +# Source-Hash: 2bdf83801d4a40af +# Review: human +# English: "Align view to north" +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +msgid "viewer.floorplan.alignNorth" +msgstr "北を上に表示" + +# Source-Hash: 286a757d8b52bb02 +# Review: human +# English: "No floor plans are available for this scene." +#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +msgid "viewer.floorplan.empty" +msgstr "このシーンには表示できる平面図がありません。" + +# Source-Hash: bf63c679ab94dbcd +# Review: human +# English: "Fit floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +msgid "viewer.floorplan.fit" +msgstr "全体を表示" + +# Source-Hash: d7148fa065bc709b +# Review: human +# English: "Floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +msgid "viewer.floorplan.floor" +msgstr "階" + +# Source-Hash: fee6203bb9046929 +# Review: human +# English: "Floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +msgid "viewer.floorplan.floorPlan" +msgstr "平面図" + +# Source-Hash: 28b255b7bf5f1084 +# Review: human +# English: "{floor} 2D view" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +msgid "viewer.floorplan.viewLabel" +msgstr "{floor}の2Dビュー" + +# Source-Hash: d17804412f8dd081 +# Review: human +# English: "Zoom in" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +msgid "viewer.floorplan.zoomIn" +msgstr "拡大" + +# Source-Hash: 23b012a283b2c14a +# Review: human +# English: "Zoom out" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +msgid "viewer.floorplan.zoomOut" +msgstr "縮小" + +# Source-Hash: 7ceade8f25b58fe9 +# Review: human +# English: "Back" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +msgid "viewer.header.back" +msgstr "戻る" + +# Source-Hash: cc3d601d59e2e26c +# Review: human +# English: "Levels" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +msgid "viewer.header.levels" +msgstr "階層" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +msgid "viewer.header.rooms" +msgstr "部屋" + +# Source-Hash: 170b2049ef0f9131 +# Review: human +# English: "Selection" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +msgid "viewer.header.selection" +msgstr "選択" + +# Source-Hash: b404dded35241274 +# Review: human +# English: "Units" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +msgid "viewer.header.units" +msgstr "住戸" + +# Source-Hash: 6547a4c0c368042a +# Review: human +# English: "Untitled" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +msgid "viewer.header.untitled" +msgstr "名称未設定" + +# Source-Hash: 665db49a26f81425 +# Review: human +# English: "Zones" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +msgid "viewer.header.zones" +msgstr "ゾーン" + +# Source-Hash: be12f8788f1f0a05 +# Review: human +# English: "Exploded" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +msgid "viewer.levels.exploded" +msgstr "分解" + +# Source-Hash: 4bf4a6861d7d633e +# Review: human +# English: "Levels: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +msgid "viewer.levels.label" +msgstr "階層:{mode}" + +# Source-Hash: 387082cd5f118bed +# Review: human +# English: "Manual" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +msgid "viewer.levels.manual" +msgstr "手動" + +# Source-Hash: e9f59b513f2f45ac +# Review: human +# English: "Solo" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +msgid "viewer.levels.solo" +msgstr "単独" + +# Source-Hash: c203c84eee18c0f6 +# Review: human +# English: "Stacked" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +msgid "viewer.levels.stacked" +msgstr "積層" + +# Source-Hash: 9dd21ca47cffa3b8 +# Review: human +# English: "Viewer layout" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +msgid "viewer.stage.layout" +msgstr "ビュー配置" + +# Source-Hash: e4cd64b1680ac0d2 +# Review: human +# English: "Split" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +msgid "viewer.stage.split" +msgstr "分割" + +# Source-Hash: f14b89d713d2528a +# Review: human +# English: "Blueprint" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +msgid "viewer.theme.blueprint" +msgstr "青図" + +# Source-Hash: 142bcb2841f423c5 +# Review: human +# English: "Mediterranean" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +msgid "viewer.theme.mediterranean" +msgstr "地中海" + +# Source-Hash: b399942ec5d7e1ba +# Review: human +# English: "Night" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +msgid "viewer.theme.night" +msgstr "夜景" + +# Source-Hash: f512d1b947b8ae5a +# Review: human +# English: "Overcast" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +msgid "viewer.theme.overcast" +msgstr "曇天" + +# Source-Hash: 583542b3bc837192 +# Review: human +# English: "Paper" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +msgid "viewer.theme.paper" +msgstr "ペーパー" + +# Source-Hash: 7abc2dc6e39b1747 +# Review: human +# English: "Studio" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +msgid "viewer.theme.studio" +msgstr "スタジオ" + +# Source-Hash: efe94d3ec8d8c44e +# Review: human +# English: "Sunset" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +msgid "viewer.theme.sunset" +msgstr "夕景" + +# Source-Hash: 82720864e5034398 +# Review: human +# English: "Scene theme" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +msgid "viewer.theme.title" +msgstr "シーンテーマ" + +# Source-Hash: 4c10ea0aa0e2ea41 +# Review: human +# English: "Twilight" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +msgid "viewer.theme.twilight" +msgstr "薄暮" + +# Source-Hash: f9592fb0672740c0 +# Review: human +# English: "Verdant" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +msgid "viewer.theme.verdant" +msgstr "緑景" + +# Source-Hash: 892d5dd34372028b +# Review: human +# English: "Guides" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +msgid "viewer.visibility.guides" +msgstr "ガイド" + +# Source-Hash: 3260bfa69cca50e5 +# Review: human +# English: "Scans" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +msgid "viewer.visibility.scans" +msgstr "3Dスキャン" + +# Source-Hash: 2cefbb554eadbb6e +# Review: human +# English: "Visibility" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +msgid "viewer.visibility.title" +msgstr "表示" + +# Source-Hash: 31afd9dee47ba943 +# Review: human +# English: "Walkthrough: {state}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +msgid "viewer.walkthrough.label" +msgstr "ウォークスルー:{state}" + +# Source-Hash: 4da8b3678ed77b45 +# Review: human +# English: "Cutaway" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +msgid "viewer.walls.cutaway" +msgstr "カット表示" + +# Source-Hash: 131bac2467f4fdf6 +# Review: human +# English: "Full height" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +msgid "viewer.walls.fullHeight" +msgstr "全高" + +# Source-Hash: 32f3d058cf2526d2 +# Review: human +# English: "Walls: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +msgid "viewer.walls.label" +msgstr "壁:{mode}" + +# Source-Hash: 803532407fb5962b +# Review: human +# English: "Low" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +msgid "viewer.walls.low" +msgstr "低く表示" diff --git a/packages/editor/src/i18n/locales/ja/messages.ts b/packages/editor/src/i18n/locales/ja/messages.ts new file mode 100644 index 0000000000..c213b9a5f3 --- /dev/null +++ b/packages/editor/src/i18n/locales/ja/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"core.level.basement\":[\"地下1階\"],\"core.level.basementNumber\":[\"地下\",[\"number\"],\"階\"],\"core.level.floorNumber\":[[\"number\"],\"階\"],\"core.level.groundFloor\":[\"1階\"],\"editor.buildPanel.advanced\":[\"詳細\"],\"editor.buildPanel.rooms.hint\":[\"壁 · 床 · 天井\"],\"editor.buildPanel.rooms.idle\":[\"形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。\"],\"editor.buildPanel.rooms.polygon.caption\":[[\"lead\"],\"最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。\"],\"editor.buildPanel.rooms.polygon.label\":[\"多角形\"],\"editor.buildPanel.rooms.polygon.lead\":[\"各頂点をクリックし、\"],\"editor.buildPanel.rooms.polygon.title\":[\"多角形の部屋\"],\"editor.buildPanel.rooms.rectangle.caption\":[\"グリッド上で\",[\"lead\"],\"します。壁・床・天井をまとめて作成します。\"],\"editor.buildPanel.rooms.rectangle.label\":[\"長方形\"],\"editor.buildPanel.rooms.rectangle.lead\":[\"対角の2点をクリック\"],\"editor.buildPanel.rooms.rectangle.title\":[\"長方形の部屋\"],\"editor.buildPanel.rooms.title\":[\"部屋\"],\"editor.buildPanel.rooms.walls.caption\":[[\"lead\"],\"輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。\"],\"editor.buildPanel.rooms.walls.label\":[\"壁\"],\"editor.buildPanel.rooms.walls.lead\":[\"壁を1面ずつ描画します。\"],\"editor.buildPanel.rooms.walls.title\":[\"壁\"],\"viewer.camera.orbitLeft\":[\"左に回転\"],\"viewer.camera.orbitRight\":[\"右に回転\"],\"viewer.camera.topView\":[\"上面図\"],\"viewer.common.off\":[\"オフ\"],\"viewer.common.on\":[\"オン\"],\"viewer.display.camera\":[\"カメラ\"],\"viewer.display.colored\":[\"カラー表示\"],\"viewer.display.colors\":[\"カラー\"],\"viewer.display.edges.off\":[\"オフ\"],\"viewer.display.edges.offDetail\":[\"エッジ線を表示しない\"],\"viewer.display.edges.soft\":[\"ソフト\"],\"viewer.display.edges.softDetail\":[\"主要な稜線を薄く表示\"],\"viewer.display.edges.strong\":[\"強調\"],\"viewer.display.edges.strongDetail\":[\"エッジ線をくっきり表示\"],\"viewer.display.edges.title\":[\"エッジ\"],\"viewer.display.monochrome\":[\"モノクロ\"],\"viewer.display.orthographic\":[\"平行投影\"],\"viewer.display.perspective\":[\"透視投影\"],\"viewer.display.render.rendered\":[\"レンダリング\"],\"viewer.display.render.renderedDetail\":[\"アンビエントオクルージョンを完全表示\"],\"viewer.display.render.solid\":[\"ソリッド\"],\"viewer.display.render.solidDetail\":[\"軽量表示(アンビエントオクルージョンなし)\"],\"viewer.display.render.title\":[\"レンダリング\"],\"viewer.display.shadows\":[\"影\"],\"viewer.display.title\":[\"表示設定\"],\"viewer.floorplan.alignNorth\":[\"北を上に表示\"],\"viewer.floorplan.empty\":[\"このシーンには表示できる平面図がありません。\"],\"viewer.floorplan.fit\":[\"全体を表示\"],\"viewer.floorplan.floor\":[\"階\"],\"viewer.floorplan.floorPlan\":[\"平面図\"],\"viewer.floorplan.viewLabel\":[[\"floor\"],\"の2Dビュー\"],\"viewer.floorplan.zoomIn\":[\"拡大\"],\"viewer.floorplan.zoomOut\":[\"縮小\"],\"viewer.header.back\":[\"戻る\"],\"viewer.header.levels\":[\"階層\"],\"viewer.header.rooms\":[\"部屋\"],\"viewer.header.selection\":[\"選択\"],\"viewer.header.units\":[\"住戸\"],\"viewer.header.untitled\":[\"名称未設定\"],\"viewer.header.zones\":[\"ゾーン\"],\"viewer.levels.exploded\":[\"分解\"],\"viewer.levels.label\":[\"階層:\",[\"mode\"]],\"viewer.levels.manual\":[\"手動\"],\"viewer.levels.solo\":[\"単独\"],\"viewer.levels.stacked\":[\"積層\"],\"viewer.stage.layout\":[\"ビュー配置\"],\"viewer.stage.split\":[\"分割\"],\"viewer.theme.blueprint\":[\"青図\"],\"viewer.theme.mediterranean\":[\"地中海\"],\"viewer.theme.night\":[\"夜景\"],\"viewer.theme.overcast\":[\"曇天\"],\"viewer.theme.paper\":[\"ペーパー\"],\"viewer.theme.studio\":[\"スタジオ\"],\"viewer.theme.sunset\":[\"夕景\"],\"viewer.theme.title\":[\"シーンテーマ\"],\"viewer.theme.twilight\":[\"薄暮\"],\"viewer.theme.verdant\":[\"緑景\"],\"viewer.visibility.guides\":[\"ガイド\"],\"viewer.visibility.scans\":[\"3Dスキャン\"],\"viewer.visibility.title\":[\"表示\"],\"viewer.walkthrough.label\":[\"ウォークスルー:\",[\"state\"]],\"viewer.walls.cutaway\":[\"カット表示\"],\"viewer.walls.fullHeight\":[\"全高\"],\"viewer.walls.label\":[\"壁:\",[\"mode\"]],\"viewer.walls.low\":[\"低く表示\"]}")as Messages; \ No newline at end of file diff --git a/packages/editor/src/i18n/locales/zh-CN/messages.po b/packages/editor/src/i18n/locales/zh-CN/messages.po new file mode 100644 index 0000000000..31b20848f9 --- /dev/null +++ b/packages/editor/src/i18n/locales/zh-CN/messages.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2026-10-26 22:28+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: zh-CN\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +# Source-Hash: 8caa959c0ae800c3 +# Review: human +# English: "Basement" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +msgid "core.level.basement" +msgstr "地下层" + +# Source-Hash: 14927085ee91ba05 +# Review: human +# English: "Basement {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +msgid "core.level.basementNumber" +msgstr "地下 {number} 层" + +# Source-Hash: e59074a7b9b3f56e +# Review: human +# English: "Floor {number}" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +msgid "core.level.floorNumber" +msgstr "{number} 层" + +# Source-Hash: cde3efc9b7ddfa41 +# Review: human +# English: "Ground floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +msgid "core.level.groundFloor" +msgstr "首层" + +# Source-Hash: f571cb2d06ae01ae +# Review: human +# English: "Advanced" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +msgid "editor.buildPanel.advanced" +msgstr "高级" + +# Source-Hash: fb40866c2d6db5fb +# Review: human +# English: "walls · floor · ceiling" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +msgid "editor.buildPanel.rooms.hint" +msgstr "墙体 · 楼板 · 天花板" + +# Source-Hash: 271039bd919dc4c1 +# Review: human +# English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +msgid "editor.buildPanel.rooms.idle" +msgstr "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和天花板。" + +# Source-Hash: ae2800b6c9e2b6ed +# Review: human +# English: "{lead} then the first one again to close. For L-shapes and angled rooms." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +msgid "editor.buildPanel.rooms.polygon.caption" +msgstr "{lead}然后再次点击起点闭合。适用于 L 形和斜角房间。" + +# Source-Hash: 4e186fc057733162 +# Review: human +# English: "Polygon" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +msgid "editor.buildPanel.rooms.polygon.label" +msgstr "多边形" + +# Source-Hash: 345e3aca3879b5c5 +# Review: human +# English: "Click each corner," +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +msgid "editor.buildPanel.rooms.polygon.lead" +msgstr "依次点击各个角点," + +# Source-Hash: 8ec6152e139b0e3f +# Review: human +# English: "Polygon room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +msgid "editor.buildPanel.rooms.polygon.title" +msgstr "多边形房间" + +# Source-Hash: 9b46295eedcbedae +# Review: human +# English: "{lead} on the grid. Walls, floor and ceiling are created together." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +msgid "editor.buildPanel.rooms.rectangle.caption" +msgstr "在网格上{lead}。墙体、楼板和天花板将一并创建。" + +# Source-Hash: 2c2c20272b0370cc +# Review: human +# English: "Rectangle" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +msgid "editor.buildPanel.rooms.rectangle.label" +msgstr "矩形" + +# Source-Hash: 060faa48e299d7c9 +# Review: human +# English: "Click two corners" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +msgid "editor.buildPanel.rooms.rectangle.lead" +msgstr "点击两个对角点" + +# Source-Hash: 83a45603ac7f41e8 +# Review: human +# English: "Rectangle room" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +msgid "editor.buildPanel.rooms.rectangle.title" +msgstr "矩形房间" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +msgid "editor.buildPanel.rooms.title" +msgstr "房间" + +# Source-Hash: 8a21a03c4acf396b +# Review: human +# English: "{lead} Close a loop and it becomes a room; leave it open for partitions." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +msgid "editor.buildPanel.rooms.walls.caption" +msgstr "{lead}闭合轮廓后会形成房间;保持开口可绘制隔墙。" + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +msgid "editor.buildPanel.rooms.walls.label" +msgstr "墙体" + +# Source-Hash: 9843edd68426fa2d +# Review: human +# English: "Draw one wall at a time." +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +msgid "editor.buildPanel.rooms.walls.lead" +msgstr "逐面绘制墙体。" + +# Source-Hash: aac9a732d2159ccc +# Review: human +# English: "Walls" +#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +msgid "editor.buildPanel.rooms.walls.title" +msgstr "墙体" + +# Source-Hash: 8bf25afa3cb8baea +# Review: human +# English: "Orbit left" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +msgid "viewer.camera.orbitLeft" +msgstr "向左旋转" + +# Source-Hash: b9cc64c1d1bb0d46 +# Review: human +# English: "Orbit right" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +msgid "viewer.camera.orbitRight" +msgstr "向右旋转" + +# Source-Hash: 1ab1eb3a14f4b0e0 +# Review: human +# English: "Top view" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +msgid "viewer.camera.topView" +msgstr "俯视图" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +msgid "viewer.common.off" +msgstr "关闭" + +# Source-Hash: 9cc01faad79c28da +# Review: human +# English: "On" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +msgid "viewer.common.on" +msgstr "开启" + +# Source-Hash: 14622745dbf93f4c +# Review: human +# English: "Camera" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +msgid "viewer.display.camera" +msgstr "相机" + +# Source-Hash: d3d478fa6c6d7ddc +# Review: human +# English: "Colored" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +msgid "viewer.display.colored" +msgstr "彩色" + +# Source-Hash: 82b74c4f5ef43336 +# Review: human +# English: "Colors" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +msgid "viewer.display.colors" +msgstr "颜色" + +# Source-Hash: 50accd740fa2e736 +# Review: human +# English: "Off" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +msgid "viewer.display.edges.off" +msgstr "关闭" + +# Source-Hash: 4ff58913a4492199 +# Review: human +# English: "No edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +msgid "viewer.display.edges.offDetail" +msgstr "不显示边线" + +# Source-Hash: d266d83ec490df4e +# Review: human +# English: "Soft" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +msgid "viewer.display.edges.soft" +msgstr "柔和" + +# Source-Hash: 0c583f1d4201293b +# Review: human +# English: "Faint outline of major creases" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +msgid "viewer.display.edges.softDetail" +msgstr "淡化显示主要折线" + +# Source-Hash: f82927a01d4c5e45 +# Review: human +# English: "Strong" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +msgid "viewer.display.edges.strong" +msgstr "清晰" + +# Source-Hash: 2416a48df28d6205 +# Review: human +# English: "Crisp, opaque edge lines" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +msgid "viewer.display.edges.strongDetail" +msgstr "清晰、不透明的边线" + +# Source-Hash: 027bbc8b7287c06a +# Review: human +# English: "Edges" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +msgid "viewer.display.edges.title" +msgstr "边线" + +# Source-Hash: ecde2f7c26914be3 +# Review: human +# English: "Monochrome" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +msgid "viewer.display.monochrome" +msgstr "单色" + +# Source-Hash: ed59dd8ce7797a66 +# Review: human +# English: "Orthographic" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +msgid "viewer.display.orthographic" +msgstr "正交" + +# Source-Hash: 316e25993ff72b88 +# Review: human +# English: "Perspective" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +msgid "viewer.display.perspective" +msgstr "透视" + +# Source-Hash: f363702a10aedaeb +# Review: human +# English: "Rendered" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +msgid "viewer.display.render.rendered" +msgstr "渲染效果" + +# Source-Hash: 8bb9e54fc786ca77 +# Review: human +# English: "Full ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +msgid "viewer.display.render.renderedDetail" +msgstr "完整环境光遮蔽" + +# Source-Hash: ef3e765e692faf4c +# Review: human +# English: "Solid" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +msgid "viewer.display.render.solid" +msgstr "实体" + +# Source-Hash: 8827df1cdeb02ed9 +# Review: human +# English: "Flat and fast — no ambient occlusion" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +msgid "viewer.display.render.solidDetail" +msgstr "平整快速,不启用环境光遮蔽" + +# Source-Hash: 5e58af99d6d4fa6a +# Review: human +# English: "Render" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +msgid "viewer.display.render.title" +msgstr "渲染" + +# Source-Hash: 4c6bcb30b40047fc +# Review: human +# English: "Shadows" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +msgid "viewer.display.shadows" +msgstr "阴影" + +# Source-Hash: d0db46f206a88e2e +# Review: human +# English: "Display settings" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +msgid "viewer.display.title" +msgstr "显示设置" + +# Source-Hash: 2bdf83801d4a40af +# Review: human +# English: "Align view to north" +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +msgid "viewer.floorplan.alignNorth" +msgstr "视图对齐北向" + +# Source-Hash: 286a757d8b52bb02 +# Review: human +# English: "No floor plans are available for this scene." +#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +msgid "viewer.floorplan.empty" +msgstr "此场景没有可用的平面图。" + +# Source-Hash: bf63c679ab94dbcd +# Review: human +# English: "Fit floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +msgid "viewer.floorplan.fit" +msgstr "适合窗口" + +# Source-Hash: d7148fa065bc709b +# Review: human +# English: "Floor" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +msgid "viewer.floorplan.floor" +msgstr "楼层" + +# Source-Hash: fee6203bb9046929 +# Review: human +# English: "Floor plan" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +msgid "viewer.floorplan.floorPlan" +msgstr "平面图" + +# Source-Hash: 28b255b7bf5f1084 +# Review: human +# English: "{floor} 2D view" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +msgid "viewer.floorplan.viewLabel" +msgstr "{floor}二维视图" + +# Source-Hash: d17804412f8dd081 +# Review: human +# English: "Zoom in" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +msgid "viewer.floorplan.zoomIn" +msgstr "放大" + +# Source-Hash: 23b012a283b2c14a +# Review: human +# English: "Zoom out" +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 +#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +msgid "viewer.floorplan.zoomOut" +msgstr "缩小" + +# Source-Hash: 7ceade8f25b58fe9 +# Review: human +# English: "Back" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +msgid "viewer.header.back" +msgstr "返回" + +# Source-Hash: cc3d601d59e2e26c +# Review: human +# English: "Levels" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +msgid "viewer.header.levels" +msgstr "楼层" + +# Source-Hash: 3e7408ed44676176 +# Review: human +# English: "Rooms" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +msgid "viewer.header.rooms" +msgstr "房间" + +# Source-Hash: 170b2049ef0f9131 +# Review: human +# English: "Selection" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +msgid "viewer.header.selection" +msgstr "当前选择" + +# Source-Hash: b404dded35241274 +# Review: human +# English: "Units" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +msgid "viewer.header.units" +msgstr "单元" + +# Source-Hash: 6547a4c0c368042a +# Review: human +# English: "Untitled" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +msgid "viewer.header.untitled" +msgstr "未命名" + +# Source-Hash: 665db49a26f81425 +# Review: human +# English: "Zones" +#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +msgid "viewer.header.zones" +msgstr "分区" + +# Source-Hash: be12f8788f1f0a05 +# Review: human +# English: "Exploded" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +msgid "viewer.levels.exploded" +msgstr "分解" + +# Source-Hash: 4bf4a6861d7d633e +# Review: human +# English: "Levels: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +msgid "viewer.levels.label" +msgstr "楼层:{mode}" + +# Source-Hash: 387082cd5f118bed +# Review: human +# English: "Manual" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +msgid "viewer.levels.manual" +msgstr "手动" + +# Source-Hash: e9f59b513f2f45ac +# Review: human +# English: "Solo" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +msgid "viewer.levels.solo" +msgstr "单层" + +# Source-Hash: c203c84eee18c0f6 +# Review: human +# English: "Stacked" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +msgid "viewer.levels.stacked" +msgstr "叠层" + +# Source-Hash: 9dd21ca47cffa3b8 +# Review: human +# English: "Viewer layout" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +msgid "viewer.stage.layout" +msgstr "视图布局" + +# Source-Hash: e4cd64b1680ac0d2 +# Review: human +# English: "Split" +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +msgid "viewer.stage.split" +msgstr "分屏" + +# Source-Hash: f14b89d713d2528a +# Review: human +# English: "Blueprint" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +msgid "viewer.theme.blueprint" +msgstr "蓝图" + +# Source-Hash: 142bcb2841f423c5 +# Review: human +# English: "Mediterranean" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +msgid "viewer.theme.mediterranean" +msgstr "地中海" + +# Source-Hash: b399942ec5d7e1ba +# Review: human +# English: "Night" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +msgid "viewer.theme.night" +msgstr "夜景" + +# Source-Hash: f512d1b947b8ae5a +# Review: human +# English: "Overcast" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +msgid "viewer.theme.overcast" +msgstr "阴天" + +# Source-Hash: 583542b3bc837192 +# Review: human +# English: "Paper" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +msgid "viewer.theme.paper" +msgstr "纸面" + +# Source-Hash: 7abc2dc6e39b1747 +# Review: human +# English: "Studio" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +msgid "viewer.theme.studio" +msgstr "影棚" + +# Source-Hash: efe94d3ec8d8c44e +# Review: human +# English: "Sunset" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +msgid "viewer.theme.sunset" +msgstr "日落" + +# Source-Hash: 82720864e5034398 +# Review: human +# English: "Scene theme" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +msgid "viewer.theme.title" +msgstr "场景主题" + +# Source-Hash: 4c10ea0aa0e2ea41 +# Review: human +# English: "Twilight" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +msgid "viewer.theme.twilight" +msgstr "暮色" + +# Source-Hash: f9592fb0672740c0 +# Review: human +# English: "Verdant" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +msgid "viewer.theme.verdant" +msgstr "绿意" + +# Source-Hash: 892d5dd34372028b +# Review: human +# English: "Guides" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +msgid "viewer.visibility.guides" +msgstr "参考图" + +# Source-Hash: 3260bfa69cca50e5 +# Review: human +# English: "Scans" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +msgid "viewer.visibility.scans" +msgstr "扫描模型" + +# Source-Hash: 2cefbb554eadbb6e +# Review: human +# English: "Visibility" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +msgid "viewer.visibility.title" +msgstr "可见性" + +# Source-Hash: 31afd9dee47ba943 +# Review: human +# English: "Walkthrough: {state}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +msgid "viewer.walkthrough.label" +msgstr "漫游:{state}" + +# Source-Hash: 4da8b3678ed77b45 +# Review: human +# English: "Cutaway" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +msgid "viewer.walls.cutaway" +msgstr "剖切墙" + +# Source-Hash: 131bac2467f4fdf6 +# Review: human +# English: "Full height" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +msgid "viewer.walls.fullHeight" +msgstr "全高墙" + +# Source-Hash: 32f3d058cf2526d2 +# Review: human +# English: "Walls: {mode}" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +msgid "viewer.walls.label" +msgstr "墙体:{mode}" + +# Source-Hash: 803532407fb5962b +# Review: human +# English: "Low" +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +msgid "viewer.walls.low" +msgstr "矮墙" diff --git a/packages/editor/src/i18n/locales/zh-CN/messages.ts b/packages/editor/src/i18n/locales/zh-CN/messages.ts new file mode 100644 index 0000000000..311c4550a6 --- /dev/null +++ b/packages/editor/src/i18n/locales/zh-CN/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"core.level.basement\":[\"地下层\"],\"core.level.basementNumber\":[\"地下 \",[\"number\"],\" 层\"],\"core.level.floorNumber\":[[\"number\"],\" 层\"],\"core.level.groundFloor\":[\"首层\"],\"editor.buildPanel.advanced\":[\"高级\"],\"editor.buildPanel.rooms.hint\":[\"墙体 · 楼板 · 天花板\"],\"editor.buildPanel.rooms.idle\":[\"选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和天花板。\"],\"editor.buildPanel.rooms.polygon.caption\":[[\"lead\"],\"然后再次点击起点闭合。适用于 L 形和斜角房间。\"],\"editor.buildPanel.rooms.polygon.label\":[\"多边形\"],\"editor.buildPanel.rooms.polygon.lead\":[\"依次点击各个角点,\"],\"editor.buildPanel.rooms.polygon.title\":[\"多边形房间\"],\"editor.buildPanel.rooms.rectangle.caption\":[\"在网格上\",[\"lead\"],\"。墙体、楼板和天花板将一并创建。\"],\"editor.buildPanel.rooms.rectangle.label\":[\"矩形\"],\"editor.buildPanel.rooms.rectangle.lead\":[\"点击两个对角点\"],\"editor.buildPanel.rooms.rectangle.title\":[\"矩形房间\"],\"editor.buildPanel.rooms.title\":[\"房间\"],\"editor.buildPanel.rooms.walls.caption\":[[\"lead\"],\"闭合轮廓后会形成房间;保持开口可绘制隔墙。\"],\"editor.buildPanel.rooms.walls.label\":[\"墙体\"],\"editor.buildPanel.rooms.walls.lead\":[\"逐面绘制墙体。\"],\"editor.buildPanel.rooms.walls.title\":[\"墙体\"],\"viewer.camera.orbitLeft\":[\"向左旋转\"],\"viewer.camera.orbitRight\":[\"向右旋转\"],\"viewer.camera.topView\":[\"俯视图\"],\"viewer.common.off\":[\"关闭\"],\"viewer.common.on\":[\"开启\"],\"viewer.display.camera\":[\"相机\"],\"viewer.display.colored\":[\"彩色\"],\"viewer.display.colors\":[\"颜色\"],\"viewer.display.edges.off\":[\"关闭\"],\"viewer.display.edges.offDetail\":[\"不显示边线\"],\"viewer.display.edges.soft\":[\"柔和\"],\"viewer.display.edges.softDetail\":[\"淡化显示主要折线\"],\"viewer.display.edges.strong\":[\"清晰\"],\"viewer.display.edges.strongDetail\":[\"清晰、不透明的边线\"],\"viewer.display.edges.title\":[\"边线\"],\"viewer.display.monochrome\":[\"单色\"],\"viewer.display.orthographic\":[\"正交\"],\"viewer.display.perspective\":[\"透视\"],\"viewer.display.render.rendered\":[\"渲染效果\"],\"viewer.display.render.renderedDetail\":[\"完整环境光遮蔽\"],\"viewer.display.render.solid\":[\"实体\"],\"viewer.display.render.solidDetail\":[\"平整快速,不启用环境光遮蔽\"],\"viewer.display.render.title\":[\"渲染\"],\"viewer.display.shadows\":[\"阴影\"],\"viewer.display.title\":[\"显示设置\"],\"viewer.floorplan.alignNorth\":[\"视图对齐北向\"],\"viewer.floorplan.empty\":[\"此场景没有可用的平面图。\"],\"viewer.floorplan.fit\":[\"适合窗口\"],\"viewer.floorplan.floor\":[\"楼层\"],\"viewer.floorplan.floorPlan\":[\"平面图\"],\"viewer.floorplan.viewLabel\":[[\"floor\"],\"二维视图\"],\"viewer.floorplan.zoomIn\":[\"放大\"],\"viewer.floorplan.zoomOut\":[\"缩小\"],\"viewer.header.back\":[\"返回\"],\"viewer.header.levels\":[\"楼层\"],\"viewer.header.rooms\":[\"房间\"],\"viewer.header.selection\":[\"当前选择\"],\"viewer.header.units\":[\"单元\"],\"viewer.header.untitled\":[\"未命名\"],\"viewer.header.zones\":[\"分区\"],\"viewer.levels.exploded\":[\"分解\"],\"viewer.levels.label\":[\"楼层:\",[\"mode\"]],\"viewer.levels.manual\":[\"手动\"],\"viewer.levels.solo\":[\"单层\"],\"viewer.levels.stacked\":[\"叠层\"],\"viewer.stage.layout\":[\"视图布局\"],\"viewer.stage.split\":[\"分屏\"],\"viewer.theme.blueprint\":[\"蓝图\"],\"viewer.theme.mediterranean\":[\"地中海\"],\"viewer.theme.night\":[\"夜景\"],\"viewer.theme.overcast\":[\"阴天\"],\"viewer.theme.paper\":[\"纸面\"],\"viewer.theme.studio\":[\"影棚\"],\"viewer.theme.sunset\":[\"日落\"],\"viewer.theme.title\":[\"场景主题\"],\"viewer.theme.twilight\":[\"暮色\"],\"viewer.theme.verdant\":[\"绿意\"],\"viewer.visibility.guides\":[\"参考图\"],\"viewer.visibility.scans\":[\"扫描模型\"],\"viewer.visibility.title\":[\"可见性\"],\"viewer.walkthrough.label\":[\"漫游:\",[\"state\"]],\"viewer.walls.cutaway\":[\"剖切墙\"],\"viewer.walls.fullHeight\":[\"全高墙\"],\"viewer.walls.label\":[\"墙体:\",[\"mode\"]],\"viewer.walls.low\":[\"矮墙\"]}")as Messages; \ No newline at end of file diff --git a/packages/editor/src/i18n/source-catalog.json b/packages/editor/src/i18n/source-catalog.json deleted file mode 100644 index 6e3c6486f6..0000000000 --- a/packages/editor/src/i18n/source-catalog.json +++ /dev/null @@ -1,469 +0,0 @@ -{ - "core.level.basement": { - "default": "Basement", - "hash": "8caa959c0ae800c3", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:160", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:59" - ] - }, - "core.level.basementNumber": { - "default": "Basement {number}", - "hash": "14927085ee91ba05", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:162", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:61" - ] - }, - "core.level.floorNumber": { - "default": "Floor {number}", - "hash": "e59074a7b9b3f56e", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:165", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:64" - ] - }, - "core.level.groundFloor": { - "default": "Ground floor", - "hash": "cde3efc9b7ddfa41", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:159", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:58" - ] - }, - "editor.buildPanel.advanced": { - "default": "Advanced", - "hash": "f571cb2d06ae01ae", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:99"] - }, - "editor.buildPanel.rooms.hint": { - "default": "walls · floor · ceiling", - "hash": "fb40866c2d6db5fb", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:236"] - }, - "editor.buildPanel.rooms.idle": { - "default": "Pick a shape to start. Every room brings its own walls, floor and ceiling.", - "hash": "271039bd919dc4c1", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:262"] - }, - "editor.buildPanel.rooms.polygon.caption": { - "default": "{lead} then the first one again to close. For L-shapes and angled rooms.", - "hash": "ae2800b6c9e2b6ed", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:302"] - }, - "editor.buildPanel.rooms.polygon.label": { - "default": "Polygon", - "hash": "4e186fc057733162", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:299"] - }, - "editor.buildPanel.rooms.polygon.lead": { - "default": "Click each corner,", - "hash": "345e3aca3879b5c5", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:301"] - }, - "editor.buildPanel.rooms.polygon.title": { - "default": "Polygon room", - "hash": "8ec6152e139b0e3f", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:300"] - }, - "editor.buildPanel.rooms.rectangle.caption": { - "default": "{lead} on the grid. Walls, floor and ceiling are created together.", - "hash": "9b46295eedcbedae", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:279"] - }, - "editor.buildPanel.rooms.rectangle.label": { - "default": "Rectangle", - "hash": "2c2c20272b0370cc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:276"] - }, - "editor.buildPanel.rooms.rectangle.lead": { - "default": "Click two corners", - "hash": "060faa48e299d7c9", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:278"] - }, - "editor.buildPanel.rooms.rectangle.title": { - "default": "Rectangle room", - "hash": "83a45603ac7f41e8", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:277"] - }, - "editor.buildPanel.rooms.title": { - "default": "Rooms", - "hash": "3e7408ed44676176", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:238"] - }, - "editor.buildPanel.rooms.walls.caption": { - "default": "{lead} Close a loop and it becomes a room; leave it open for partitions.", - "hash": "8a21a03c4acf396b", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:291"] - }, - "editor.buildPanel.rooms.walls.label": { - "default": "Walls", - "hash": "aac9a732d2159ccc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:288"] - }, - "editor.buildPanel.rooms.walls.lead": { - "default": "Draw one wall at a time.", - "hash": "9843edd68426fa2d", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:290"] - }, - "editor.buildPanel.rooms.walls.title": { - "default": "Walls", - "hash": "aac9a732d2159ccc", - "context": ["packages/editor/src/components/ui/build-panel/build-panel.tsx:289"] - }, - "viewer.camera.orbitLeft": { - "default": "Orbit left", - "hash": "8bf25afa3cb8baea", - "context": [ - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:482", - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:490" - ] - }, - "viewer.camera.orbitRight": { - "default": "Orbit right", - "hash": "b9cc64c1d1bb0d46", - "context": [ - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:498", - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:506" - ] - }, - "viewer.camera.topView": { - "default": "Top view", - "hash": "1ab1eb3a14f4b0e0", - "context": [ - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:514", - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:522" - ] - }, - "viewer.common.off": { - "default": "Off", - "hash": "50accd740fa2e736", - "context": [ - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:212", - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:463" - ] - }, - "viewer.common.on": { - "default": "On", - "hash": "9cc01faad79c28da", - "context": [ - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:212", - "packages/editor/src/components/viewer/viewer-controls-bar.tsx:462" - ] - }, - "viewer.display.camera": { - "default": "Camera", - "hash": "14622745dbf93f4c", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:225"] - }, - "viewer.display.colored": { - "default": "Colored", - "hash": "d3d478fa6c6d7ddc", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:239"] - }, - "viewer.display.colors": { - "default": "Colors", - "hash": "82b74c4f5ef43336", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:236"] - }, - "viewer.display.edges.off": { - "default": "Off", - "hash": "50accd740fa2e736", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:174"] - }, - "viewer.display.edges.offDetail": { - "default": "No edge lines", - "hash": "4ff58913a4492199", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:175"] - }, - "viewer.display.edges.soft": { - "default": "Soft", - "hash": "d266d83ec490df4e", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:179"] - }, - "viewer.display.edges.softDetail": { - "default": "Faint outline of major creases", - "hash": "0c583f1d4201293b", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:180"] - }, - "viewer.display.edges.strong": { - "default": "Strong", - "hash": "f82927a01d4c5e45", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:184"] - }, - "viewer.display.edges.strongDetail": { - "default": "Crisp, opaque edge lines", - "hash": "2416a48df28d6205", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:185"] - }, - "viewer.display.edges.title": { - "default": "Edges", - "hash": "027bbc8b7287c06a", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:312"] - }, - "viewer.display.monochrome": { - "default": "Monochrome", - "hash": "ecde2f7c26914be3", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:240"] - }, - "viewer.display.orthographic": { - "default": "Orthographic", - "hash": "ed59dd8ce7797a66", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:229"] - }, - "viewer.display.perspective": { - "default": "Perspective", - "hash": "316e25993ff72b88", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:228"] - }, - "viewer.display.render.rendered": { - "default": "Rendered", - "hash": "f363702a10aedaeb", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:166"] - }, - "viewer.display.render.renderedDetail": { - "default": "Full ambient occlusion", - "hash": "8bb9e54fc786ca77", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:167"] - }, - "viewer.display.render.solid": { - "default": "Solid", - "hash": "ef3e765e692faf4c", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:160"] - }, - "viewer.display.render.solidDetail": { - "default": "Flat and fast — no ambient occlusion", - "hash": "8827df1cdeb02ed9", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:161"] - }, - "viewer.display.render.title": { - "default": "Render", - "hash": "5e58af99d6d4fa6a", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:249"] - }, - "viewer.display.shadows": { - "default": "Shadows", - "hash": "4c6bcb30b40047fc", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:210"] - }, - "viewer.display.title": { - "default": "Display settings", - "hash": "d0db46f206a88e2e", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:196"] - }, - "viewer.floorplan.alignNorth": { - "default": "Align view to north", - "hash": "2bdf83801d4a40af", - "context": ["packages/editor/src/components/viewer/floorplan-compass-button.tsx:23"] - }, - "viewer.floorplan.empty": { - "default": "No floor plans are available for this scene.", - "hash": "286a757d8b52bb02", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:893"] - }, - "viewer.floorplan.fit": { - "default": "Fit floor plan", - "hash": "bf63c679ab94dbcd", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1050", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1053" - ] - }, - "viewer.floorplan.floor": { - "default": "Floor", - "hash": "d7148fa065bc709b", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:997", - "packages/editor/src/components/viewer/floorplan-preview.tsx:999" - ] - }, - "viewer.floorplan.floorPlan": { - "default": "Floor plan", - "hash": "fee6203bb9046929", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:909"] - }, - "viewer.floorplan.viewLabel": { - "default": "{floor} 2D view", - "hash": "28b255b7bf5f1084", - "context": ["packages/editor/src/components/viewer/floorplan-preview.tsx:906"] - }, - "viewer.floorplan.zoomIn": { - "default": "Zoom in", - "hash": "d17804412f8dd081", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1059", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1062" - ] - }, - "viewer.floorplan.zoomOut": { - "default": "Zoom out", - "hash": "23b012a283b2c14a", - "context": [ - "packages/editor/src/components/viewer/floorplan-preview.tsx:1041", - "packages/editor/src/components/viewer/floorplan-preview.tsx:1044" - ] - }, - "viewer.header.back": { - "default": "Back", - "hash": "7ceade8f25b58fe9", - "context": [ - "packages/editor/src/components/viewer/viewer-scene-header.tsx:144", - "packages/editor/src/components/viewer/viewer-scene-header.tsx:153" - ] - }, - "viewer.header.levels": { - "default": "Levels", - "hash": "cc3d601d59e2e26c", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:217"] - }, - "viewer.header.rooms": { - "default": "Rooms", - "hash": "3e7408ed44676176", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:50"] - }, - "viewer.header.selection": { - "default": "Selection", - "hash": "170b2049ef0f9131", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:182"] - }, - "viewer.header.units": { - "default": "Units", - "hash": "b404dded35241274", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:49"] - }, - "viewer.header.untitled": { - "default": "Untitled", - "hash": "6547a4c0c368042a", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:163"] - }, - "viewer.header.zones": { - "default": "Zones", - "hash": "665db49a26f81425", - "context": ["packages/editor/src/components/viewer/viewer-scene-header.tsx:51"] - }, - "viewer.levels.exploded": { - "default": "Exploded", - "hash": "be12f8788f1f0a05", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:58"] - }, - "viewer.levels.label": { - "default": "Levels: {mode}", - "hash": "4bf4a6861d7d633e", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:397"] - }, - "viewer.levels.manual": { - "default": "Manual", - "hash": "387082cd5f118bed", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:60"] - }, - "viewer.levels.solo": { - "default": "Solo", - "hash": "e9f59b513f2f45ac", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:59"] - }, - "viewer.levels.stacked": { - "default": "Stacked", - "hash": "c203c84eee18c0f6", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:57"] - }, - "viewer.stage.layout": { - "default": "Viewer layout", - "hash": "9dd21ca47cffa3b8", - "context": ["packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31"] - }, - "viewer.stage.split": { - "default": "Split", - "hash": "e4cd64b1680ac0d2", - "context": ["packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59"] - }, - "viewer.theme.blueprint": { - "default": "Blueprint", - "hash": "f14b89d713d2528a", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:74"] - }, - "viewer.theme.mediterranean": { - "default": "Mediterranean", - "hash": "142bcb2841f423c5", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:75"] - }, - "viewer.theme.night": { - "default": "Night", - "hash": "b399942ec5d7e1ba", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:77"] - }, - "viewer.theme.overcast": { - "default": "Overcast", - "hash": "f512d1b947b8ae5a", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:73"] - }, - "viewer.theme.paper": { - "default": "Paper", - "hash": "583542b3bc837192", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:71"] - }, - "viewer.theme.studio": { - "default": "Studio", - "hash": "7abc2dc6e39b1747", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:70"] - }, - "viewer.theme.sunset": { - "default": "Sunset", - "hash": "efe94d3ec8d8c44e", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:72"] - }, - "viewer.theme.title": { - "default": "Scene theme", - "hash": "82720864e5034398", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:277"] - }, - "viewer.theme.twilight": { - "default": "Twilight", - "hash": "4c10ea0aa0e2ea41", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:76"] - }, - "viewer.theme.verdant": { - "default": "Verdant", - "hash": "f9592fb0672740c0", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:78"] - }, - "viewer.visibility.guides": { - "default": "Guides", - "hash": "892d5dd34372028b", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:134"] - }, - "viewer.visibility.scans": { - "default": "Scans", - "hash": "3260bfa69cca50e5", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:121"] - }, - "viewer.visibility.title": { - "default": "Visibility", - "hash": "2cefbb554eadbb6e", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:106"] - }, - "viewer.walkthrough.label": { - "default": "Walkthrough: {state}", - "hash": "31afd9dee47ba943", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:460"] - }, - "viewer.walls.cutaway": { - "default": "Cutaway", - "hash": "4da8b3678ed77b45", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:66"] - }, - "viewer.walls.fullHeight": { - "default": "Full height", - "hash": "131bac2467f4fdf6", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:64"] - }, - "viewer.walls.label": { - "default": "Walls: {mode}", - "hash": "32f3d058cf2526d2", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:424"] - }, - "viewer.walls.low": { - "default": "Low", - "hash": "803532407fb5962b", - "context": ["packages/editor/src/components/viewer/viewer-controls-bar.tsx:65"] - } -} diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index edcba4e241..39a0983e32 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -7,6 +7,8 @@ export { acquireSceneReadOnlyLease, applyScenePatch, applySceneSnapshot, + type CompiledTranslationMessages, + createI18n, DEFAULT_CURVED_STAIR_INNER_RADIUS, DEFAULT_CURVED_STAIR_SWEEP_ANGLE, DEFAULT_SPIRAL_SHOW_CENTER_COLUMN, @@ -22,11 +24,15 @@ export { DEFAULT_STAIR_THICKNESS, DEFAULT_STAIR_TYPE, DEFAULT_STAIR_WIDTH, + defaultI18n, formatLength, formatNumber, getLocale, normalizeDecimalInput, + type PascalI18n, + PascalI18nProvider, parseLocalizedNumber, + registerCompiledMessages, registerMessages, type SceneCommit, type SceneCommitListener, @@ -38,14 +44,13 @@ export { setLocale, subscribe, subscribeSceneCommits, - type Translate, type TranslationMessage, type TranslationMessages, type TranslationParams, translate, useLocale, + usePascalI18n, useScene, - useTranslate, } from '@pascal-app/core' export { useViewer } from '@pascal-app/viewer' export type { EditorProps } from './components/editor' diff --git a/scripts/i18n.test.ts b/scripts/i18n.test.ts index 7e3aac07ba..9b8de68277 100644 --- a/scripts/i18n.test.ts +++ b/scripts/i18n.test.ts @@ -1,87 +1,108 @@ import { expect, test } from 'bun:test' import { - hasRequiredPluralForms, - type LocaleCatalog, - refreshLocaleCatalog, - type SourceCatalog, + hashSource, + refreshTranslations, + type SourceMessage, + type TranslationEntry, type TranslationProvider, + validateTranslation, } from './i18n' -test('plural validation follows the target locale categories', () => { - const english = { one: '{count} item', other: '{count} items' } +test('ICU plural validation follows the target locale categories', () => { + const english = '{count, plural, one {# item} other {# items}}' - expect(hasRequiredPluralForms(english, { other: '{count} 個' }, 'ja')).toBeTrue() - expect(hasRequiredPluralForms(english, { other: '{count} 个' }, 'zh-CN')).toBeTrue() - expect(hasRequiredPluralForms(english, { other: '{count} items' }, 'en')).toBeFalse() - expect(hasRequiredPluralForms(english, english, 'en')).toBeTrue() + expect(validateTranslation(english, '{count, plural, other {# 個}}', 'ja')).toEqual([]) + expect(validateTranslation(english, '{count, plural, other {# 个}}', 'zh-CN')).toEqual([]) + expect(validateTranslation(english, '{count, plural, other {# items}}', 'en')).toContain( + 'plural selector "count" is missing "one"', + ) + expect(validateTranslation(english, english, 'en')).toEqual([]) }) test('AI translation refreshes only missing and stale entries through a mocked provider', async () => { - const source: SourceCatalog = { + const sources: SourceMessage[] = [ + { id: 'viewer.current', message: 'Current', context: ['current.tsx:1'] }, + { id: 'viewer.missing', message: 'Hello {name}', context: ['missing.tsx:2'] }, + { id: 'viewer.stale', message: 'Changed', context: ['stale.tsx:3'] }, + ] + const current: Record = { 'viewer.current': { - default: 'Current', - hash: 'hash-current', - context: ['current.tsx:1'], - }, - 'viewer.missing': { - default: 'Hello {name}', - hash: 'hash-missing', - context: ['missing.tsx:2'], + translation: 'Actuel', + sourceHash: hashSource('Current'), + review: 'human', + fuzzy: false, }, 'viewer.stale': { - default: 'Changed', - hash: 'hash-new', - context: ['stale.tsx:3'], - }, - } - const catalog: LocaleCatalog = { - locale: 'fr', - messages: { - 'viewer.current': { message: 'Actuel', sourceHash: 'hash-current' }, - 'viewer.stale': { message: 'Ancien', sourceHash: 'hash-old' }, + translation: 'Ancien', + sourceHash: hashSource('Old'), + review: 'human', + fuzzy: false, }, } const requests: string[][] = [] const provider: TranslationProvider = { + name: 'mock', + model: 'mock-model', async translate(request) { - requests.push(request.items.map((item) => item.key)) + requests.push(request.items.map((item) => item.id)) return { - 'viewer.missing': 'Bonjour {name}', - 'viewer.stale': 'Modifié', + locale: request.locale, + entries: [ + { + id: 'viewer.missing', + translation: 'Bonjour {name}', + reviewNotes: '', + }, + { id: 'viewer.stale', translation: 'Modifié', reviewNotes: 'Check toolbar width.' }, + ], } }, } - const refreshed = await refreshLocaleCatalog(source, catalog, provider, {}) + const refreshed = await refreshTranslations('fr', sources, current, provider) expect(requests).toEqual([['viewer.missing', 'viewer.stale']]) - expect(refreshed.messages['viewer.current']).toEqual(catalog.messages['viewer.current']) - expect(refreshed.messages['viewer.missing']).toEqual({ - message: 'Bonjour {name}', - sourceHash: 'hash-missing', + expect(refreshed['viewer.current']).toEqual(current['viewer.current']) + expect(refreshed['viewer.missing']).toEqual({ + translation: 'Bonjour {name}', + sourceHash: hashSource('Hello {name}'), + review: 'required', + fuzzy: true, + reviewNotes: '', }) - expect(refreshed.messages['viewer.stale']).toEqual({ - message: 'Modifié', - sourceHash: 'hash-new', + expect(refreshed['viewer.stale']).toEqual({ + translation: 'Modifié', + sourceHash: hashSource('Changed'), + review: 'required', + fuzzy: true, + reviewNotes: 'Check toolbar width.', }) }) test('AI translation rejects broken placeholders from a provider', async () => { - const source: SourceCatalog = { - 'viewer.greeting': { - default: 'Hello {name}', - hash: 'hash-greeting', - context: ['greeting.tsx:1'], - }, - } - const catalog: LocaleCatalog = { locale: 'fr', messages: {} } + const sources: SourceMessage[] = [ + { id: 'viewer.greeting', message: 'Hello {name}', context: ['greeting.tsx:1'] }, + ] const provider: TranslationProvider = { - async translate() { - return { 'viewer.greeting': 'Bonjour' } + name: 'mock', + model: 'mock-model', + async translate(request) { + return { + locale: request.locale, + entries: [{ id: 'viewer.greeting', translation: 'Bonjour', reviewNotes: '' }], + } }, } - expect(refreshLocaleCatalog(source, catalog, provider, {})).rejects.toThrow( - 'Translation provider changed placeholders in "viewer.greeting"', + expect(refreshTranslations('fr', sources, {}, provider)).rejects.toThrow( + 'placeholder names differ from English', ) }) + +test('translation validation applies the checked-in glossary', () => { + expect( + validateTranslation('Ceiling', '吊顶', 'zh-CN', { + terms: { ceiling: { 'zh-CN': '天花板' } }, + }), + ).toContain('expected glossary term "天花板" for "ceiling"') +}) diff --git a/scripts/i18n.ts b/scripts/i18n.ts index 28cc1a506f..416469026b 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -1,53 +1,62 @@ import { createHash } from 'node:crypto' -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { readFileSync, writeFileSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { createAnthropic } from '@ai-sdk/anthropic' +import { createOpenAI } from '@ai-sdk/openai' +import { compileMessageOrThrow } from '@lingui/message-utils/compileMessage' +import { generateText, Output } from 'ai' +import { type PoFile, type PoItem, parsePo, stringifyPo } from 'pofile-ts' import ts from 'typescript' +import { z } from 'zod' -export type Message = - | string - | { - zero?: string - one?: string - two?: string - few?: string - many?: string - other: string - } - -export type SourceEntry = { - default: Message - hash: string +export type SourceMessage = { + id: string + message: string context: string[] } -export type SourceCatalog = Record - -export type LocaleEntry = { - message: Message - sourceHash: string +export type TranslationEntry = { + translation: string + sourceHash?: string + review: 'human' | 'required' | 'missing' + fuzzy: boolean + reviewNotes?: string } -export type LocaleCatalog = { +export type TranslationRequest = { locale: string - messages: Record + items: SourceMessage[] + glossary: Glossary } -export type TranslationRequest = { +export type TranslationResponse = { locale: string - items: Array<{ key: string; source: Message; context: string[] }> - glossary: Record + entries: Array<{ id: string; translation: string; reviewNotes: string }> } export interface TranslationProvider { - translate(request: TranslationRequest): Promise> + name: string + model: string + translate(request: TranslationRequest): Promise +} + +type Glossary = { + productContext?: string + terms?: Record> } +type CompiledToken = string | [name: string, type?: string, choices?: unknown] + const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const SOURCE_CATALOG_PATH = join(ROOT, 'packages/editor/src/i18n/source-catalog.json') -const LOCALE_CATALOG_DIR = join(ROOT, 'packages/editor/src/i18n/catalogs') +const CATALOG_ROOT = join(ROOT, 'packages/editor/src/i18n/locales') const GLOSSARY_PATH = join(ROOT, 'i18n/glossary.json') -const BUNDLED_LOCALES = ['zh-CN', 'fr', 'es', 'ja'] as const +const TARGET_LOCALES = ['zh-CN', 'fr', 'es', 'ja'] as const +const ALL_LOCALES = ['en', ...TARGET_LOCALES] as const +const CONVERTED_SURFACES = [ + 'packages/editor/src/components/viewer', + 'packages/editor/src/components/ui/build-panel/build-panel.tsx', +] const SOURCE_GLOBS = [ 'packages/core/src/**/*.{ts,tsx}', 'packages/viewer/src/**/*.{ts,tsx}', @@ -55,207 +64,420 @@ const SOURCE_GLOBS = [ 'packages/nodes/src/**/*.{ts,tsx}', 'apps/editor/**/*.{ts,tsx}', ] -const CONVERTED_SURFACES = [ - 'packages/editor/src/components/viewer', - 'packages/editor/src/components/ui/build-panel/build-panel.tsx', -] -const PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'] as const +const SOURCE_HASH_PREFIX = 'Source-Hash: ' +const REVIEW_PREFIX = 'Review: ' +const ENGLISH_PREFIX = 'English: ' +const AI_PROVIDER_PREFIX = 'AI-Provider: ' +const AI_MODEL_PREFIX = 'AI-Model: ' +const REVIEW_NOTES_PREFIX = 'Review-Notes: ' + +function catalogPath(locale: string): string { + return join(CATALOG_ROOT, locale, 'messages.po') +} + +function compiledCatalogPath(locale: string): string { + return join(CATALOG_ROOT, locale, 'messages.ts') +} -function hashMessage(message: Message): string { +function readJson(path: string): T { + return JSON.parse(readFileSync(path, 'utf8')) as T +} + +export function hashSource(message: string): string { return createHash('sha256').update(JSON.stringify(message)).digest('hex').slice(0, 16) } -function messageFromNode(node: ts.Expression): Message | null { - if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text - if (!ts.isObjectLiteralExpression(node)) return null +function commentValue(item: PoItem, prefix: string): string | undefined { + return item.comments.find((comment) => comment.startsWith(prefix))?.slice(prefix.length) +} - const message: Partial> = {} - for (const property of node.properties) { - if (!ts.isPropertyAssignment(property)) return null - const name = - ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) - ? property.name.text - : null - if (!(name && PLURAL_KEYS.includes(name as (typeof PLURAL_KEYS)[number]))) return null - if (!ts.isStringLiteralLike(property.initializer)) return null - message[name as (typeof PLURAL_KEYS)[number]] = property.initializer.text - } - return typeof message.other === 'string' ? (message as Message) : null +function replaceComment(item: PoItem, prefix: string, value?: string): void { + item.comments = item.comments.filter((comment) => !comment.startsWith(prefix)) + if (value !== undefined) item.comments.push(`${prefix}${value}`) } -function sourceFiles(): string[] { - const files = new Set() - for (const pattern of SOURCE_GLOBS) { - const glob = new Bun.Glob(pattern) - for (const path of glob.scanSync({ cwd: ROOT, absolute: true, onlyFiles: true })) { - if ( - path.includes('/node_modules/') || - /\.test\.[tj]sx?$/.test(path) || - path.includes('/__fixtures__/') - ) { - continue +function sourceMessagesFromPo(po: PoFile): SourceMessage[] { + return po.items + .filter((item) => !item.obsolete) + .map((item) => ({ + id: item.msgid, + message: item.msgstr[0] ?? '', + context: [...item.references], + })) + .sort((left, right) => left.id.localeCompare(right.id)) +} + +function readSourceMessages(): SourceMessage[] { + return sourceMessagesFromPo(parsePo(readFileSync(catalogPath('en'), 'utf8'))) +} + +function readTranslationState(po: PoFile): Record { + return Object.fromEntries( + po.items + .filter((item) => !item.obsolete) + .map((item) => { + const review = commentValue(item, REVIEW_PREFIX) + return [ + item.msgid, + { + translation: item.msgstr[0] ?? '', + sourceHash: commentValue(item, SOURCE_HASH_PREFIX), + review: review === 'human' || review === 'required' ? review : 'missing', + fuzzy: Boolean(item.flags.fuzzy), + reviewNotes: commentValue(item, REVIEW_NOTES_PREFIX), + } satisfies TranslationEntry, + ] + }), + ) +} + +function variableNames(tokens: CompiledToken[]): string[] { + const names = new Set() + const visit = (entries: CompiledToken[]) => { + for (const token of entries) { + if (typeof token === 'string') continue + names.add(token[0]) + const choices = token[2] + if (!(choices && typeof choices === 'object')) continue + for (const branch of Object.values(choices)) { + if (Array.isArray(branch)) visit(branch as CompiledToken[]) } - files.add(path) } } - return [...files].sort() + visit(tokens) + return [...names].sort() } -export function extractSourceCatalog(root = ROOT): SourceCatalog { - const found = new Map }>() - - for (const path of sourceFiles()) { - const sourceText = readFileSync(path, 'utf8') - const source = ts.createSourceFile( - path, - sourceText, - ts.ScriptTarget.Latest, - true, - path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, - ) +function tags(message: string): string[] { + return [...message.matchAll(/<\/?([A-Za-z][\w-]*)>/g)].map((match) => match[0]).sort() +} - function visit(node: ts.Node): void { +function plurals(tokens: CompiledToken[]): Map }> { + const found = new Map }>() + const visit = (entries: CompiledToken[]) => { + for (const token of entries) { + if (typeof token === 'string') continue + const type = token[1] + const choices = token[2] if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.text === 't' && - node.arguments.length >= 2 + (type === 'plural' || type === 'selectordinal') && + choices && + typeof choices === 'object' ) { - const keyNode = node.arguments[0] - const defaultNode = node.arguments[1] - if (!(keyNode && defaultNode && ts.isStringLiteralLike(keyNode))) { - const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 - throw new Error( - `${relative(root, path)}:${line}: t() keys and English defaults must be static literals`, - ) - } - const message = messageFromNode(defaultNode) - if (message === null) { - const line = source.getLineAndCharacterOfPosition(defaultNode.getStart()).line + 1 - throw new Error( - `${relative(root, path)}:${line}: unsupported English message in t("${keyNode.text}")`, - ) - } - const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 - const context = `${relative(root, path)}:${line}` - const existing = found.get(keyNode.text) - if (existing && JSON.stringify(existing.default) !== JSON.stringify(message)) { - throw new Error(`Conflicting English defaults for "${keyNode.text}"`) - } - if (existing) existing.context.add(context) - else found.set(keyNode.text, { default: message, context: new Set([context]) }) + found.set(token[0], { + type, + categories: new Set(Object.keys(choices).filter((category) => category !== 'offset')), + }) + } + if (!(choices && typeof choices === 'object')) continue + for (const branch of Object.values(choices)) { + if (Array.isArray(branch)) visit(branch as CompiledToken[]) } - ts.forEachChild(node, visit) } - visit(source) } - - return Object.fromEntries( - [...found.entries()] - .sort(([left], [right]) => left.localeCompare(right)) - .map(([key, entry]) => [ - key, - { - default: entry.default, - hash: hashMessage(entry.default), - context: [...entry.context].sort(), - }, - ]), - ) + visit(tokens) + return found } -function readJson(path: string): T { - return JSON.parse(readFileSync(path, 'utf8')) as T +function requiredPluralCategories(locale: string, type: string): Intl.LDMLPluralRule[] { + return new Intl.PluralRules(locale, { + type: type === 'selectordinal' ? 'ordinal' : 'cardinal', + }).resolvedOptions().pluralCategories } -function writeJson(path: string, value: unknown): void { - mkdirSync(dirname(path), { recursive: true }) - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) - const formatted = Bun.spawnSync( - [process.execPath, 'x', 'biome', 'format', '--write', relative(ROOT, path)], - { cwd: ROOT, stdout: 'ignore', stderr: 'inherit' }, - ) - if (formatted.exitCode !== 0) throw new Error(`Could not format ${relative(ROOT, path)}`) +function validateGlossary( + source: string, + translation: string, + locale: string, + glossary: Glossary, +): string[] { + const errors: string[] = [] + const normalizedSource = source + .replaceAll(/\{[^}]+\}/g, '') + .replaceAll(/[^\p{L}\p{N}\s-]/gu, '') + .trim() + .toLocaleLowerCase('en') + const normalizedTranslation = translation.toLocaleLowerCase(locale) + for (const [english, localized] of Object.entries(glossary.terms ?? {})) { + const expected = localized[locale] + const normalizedTerm = english.toLocaleLowerCase('en') + if ( + typeof expected === 'string' && + (normalizedSource === normalizedTerm || normalizedSource === `${normalizedTerm}s`) && + !normalizedTranslation.includes(expected.toLocaleLowerCase(locale)) + ) { + errors.push(`expected glossary term "${expected}" for "${english}"`) + } + } + return errors } -function localeCatalogPath(locale: string): string { - return join(LOCALE_CATALOG_DIR, `${locale}.json`) +export function validateTranslation( + source: string, + translation: string, + locale: string, + glossary: Glossary = {}, +): string[] { + const errors: string[] = [] + let sourceTokens: CompiledToken[] + let translatedTokens: CompiledToken[] + try { + sourceTokens = compileMessageOrThrow(source) as CompiledToken[] + } catch (error) { + return [`invalid English ICU: ${error instanceof Error ? error.message : String(error)}`] + } + try { + translatedTokens = compileMessageOrThrow(translation) as CompiledToken[] + } catch (error) { + return [`invalid translated ICU: ${error instanceof Error ? error.message : String(error)}`] + } + + if ( + JSON.stringify(variableNames(sourceTokens)) !== JSON.stringify(variableNames(translatedTokens)) + ) { + errors.push('placeholder names differ from English') + } + if (JSON.stringify(tags(source)) !== JSON.stringify(tags(translation))) { + errors.push('rich-text tags differ from English') + } + + const sourcePlurals = plurals(sourceTokens) + const translatedPlurals = plurals(translatedTokens) + for (const [name, sourcePlural] of sourcePlurals) { + const translatedPlural = translatedPlurals.get(name) + if (!translatedPlural || translatedPlural.type !== sourcePlural.type) { + errors.push(`plural selector "${name}" is missing or changed`) + continue + } + for (const category of requiredPluralCategories(locale, sourcePlural.type)) { + if (!translatedPlural.categories.has(category)) { + errors.push(`plural selector "${name}" is missing "${category}"`) + } + } + } + errors.push(...validateGlossary(source, translation, locale, glossary)) + return errors } -function messageStrings(message: Message): string[] { - return typeof message === 'string' ? [message] : Object.values(message).filter(Boolean) +function assertProviderResponse( + request: TranslationRequest, + response: TranslationResponse, + currentBatch: SourceMessage[], +): Map { + if (response.locale !== request.locale) { + throw new Error( + `Translation provider returned locale "${response.locale}" for "${request.locale}"`, + ) + } + const expectedIds = currentBatch.map((item) => item.id).sort() + const actualIds = response.entries.map((entry) => entry.id).sort() + if ( + new Set(actualIds).size !== actualIds.length || + JSON.stringify(actualIds) !== JSON.stringify(expectedIds) + ) { + throw new Error('Translation provider returned missing, duplicate, or unexpected message IDs') + } + + const byId = new Map(response.entries.map((entry) => [entry.id, entry])) + for (const source of currentBatch) { + const result = byId.get(source.id) + if (!result) throw new Error(`Translation provider omitted "${source.id}"`) + const errors = validateTranslation( + source.message, + result.translation, + request.locale, + request.glossary, + ) + if (errors.length > 0) { + throw new Error(`Translation provider returned invalid "${source.id}": ${errors.join('; ')}`) + } + } + return byId } -function placeholders(message: Message): string[] { - const names = new Set() - for (const value of messageStrings(message)) { - for (const match of value.matchAll(/\{([A-Za-z_][\w.-]*)\}/g)) names.add(match[1]!) +export async function refreshTranslations( + locale: string, + sources: SourceMessage[], + current: Record, + provider: TranslationProvider, + glossary: Glossary = {}, +): Promise> { + const stale = sources.filter((source) => { + const existing = current[source.id] + return !existing?.translation || existing.sourceHash !== hashSource(source.message) + }) + if (stale.length === 0) return current + + const updated = { ...current } + for (let offset = 0; offset < stale.length; offset += 40) { + const items = stale.slice(offset, offset + 40) + const request = { locale, items, glossary } + const response = await provider.translate(request) + const translated = assertProviderResponse(request, response, items) + for (const source of items) { + const result = translated.get(source.id)! + updated[source.id] = { + translation: result.translation, + sourceHash: hashSource(source.message), + review: 'required', + fuzzy: true, + reviewNotes: result.reviewNotes, + } + } } - return [...names].sort() + return updated } -function samePlaceholders(source: Message, translated: Message): boolean { - if (typeof source === 'string' || typeof translated === 'string') { - return ( - typeof source === 'string' && - typeof translated === 'string' && - JSON.stringify(placeholders(source)) === JSON.stringify(placeholders(translated)) - ) +const translationOutputSchema = z.object({ + locale: z.string(), + entries: z.array( + z.object({ + id: z.string(), + translation: z.string(), + reviewNotes: z.string(), + }), + ), +}) + +function promptFor(request: TranslationRequest): string { + return [ + `Translate these Pascal architecture/CAD interface messages into ${request.locale}.`, + 'Return every input ID exactly once. Preserve ICU placeholders and rich-text tags exactly.', + `For ICU plurals, include the categories used by ${request.locale}: ${requiredPluralCategories(request.locale, 'plural').join(', ')}.`, + 'Use natural, concise professional UI language. Do not translate IDs.', + 'reviewNotes should briefly flag ambiguity or be an empty string.', + `Product and glossary: ${JSON.stringify(request.glossary)}`, + `Messages: ${JSON.stringify(request.items)}`, + ].join('\n\n') +} + +function configuredProvider(): TranslationProvider { + const name = + process.env.I18N_PROVIDER ?? + (process.env.OPENAI_API_KEY ? 'openai' : process.env.ANTHROPIC_API_KEY ? 'anthropic' : '') + const modelName = process.env.I18N_MODEL + if (!modelName) throw new Error('Set I18N_MODEL to the translation model') + + const model = + name === 'openai' + ? createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + })(modelName) + : name === 'anthropic' + ? createAnthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, + baseURL: process.env.ANTHROPIC_BASE_URL, + })(modelName) + : null + + if (name === 'openai' && !process.env.OPENAI_API_KEY) { + throw new Error('Set OPENAI_API_KEY for the OpenAI translation provider') + } + if (name === 'anthropic' && !process.env.ANTHROPIC_API_KEY) { + throw new Error('Set ANTHROPIC_API_KEY for the Anthropic translation provider') + } + if (!model) { + throw new Error('Set I18N_PROVIDER to "openai" or "anthropic" and provide its API key') } - return Object.entries(translated).every(([category, value]) => { - const sourceValue = source[category as keyof typeof source] ?? source.other - return JSON.stringify(placeholders(sourceValue)) === JSON.stringify(placeholders(value)) + return { + name, + model: modelName, + async translate(request) { + const result = await generateText({ + model, + output: Output.object({ schema: translationOutputSchema }), + prompt: promptFor(request), + }) + return result.output + }, + } +} + +function runLingui(command: 'extract' | 'compile', strict = false): void { + const args = [process.execPath, 'x', 'lingui', command] + if (command === 'extract') args.push('--clean') + if (command === 'compile') { + if (strict) args.push('--strict') + args.push('--typescript', '--namespace', 'es') + } + args.push('--workers', '1') + const result = Bun.spawnSync(args, { + cwd: ROOT, + stdout: 'inherit', + stderr: 'inherit', }) + if (result.exitCode !== 0) throw new Error(`Lingui ${command} failed`) } -function pluralCategories(locale: string): Intl.LDMLPluralRule[] { - try { - return new Intl.PluralRules(locale).resolvedOptions().pluralCategories - } catch { - return new Intl.PluralRules('en').resolvedOptions().pluralCategories +function writePo(path: string, po: PoFile): void { + writeFileSync(path, stringifyPo(po, { foldLength: 0 })) +} + +function annotateCatalogs(): void { + const sources = readSourceMessages() + const byId = new Map(sources.map((source) => [source.id, source])) + for (const locale of TARGET_LOCALES) { + const path = catalogPath(locale) + const po = parsePo(readFileSync(path, 'utf8')) + for (const item of po.items) { + if (item.obsolete) continue + const source = byId.get(item.msgid) + if (!source) continue + replaceComment(item, ENGLISH_PREFIX, JSON.stringify(source.message)) + if (item.msgstr[0] && !commentValue(item, REVIEW_PREFIX)) { + replaceComment(item, SOURCE_HASH_PREFIX, hashSource(source.message)) + replaceComment(item, REVIEW_PREFIX, 'human') + } + } + writePo(path, po) } } -export function hasRequiredPluralForms( - source: Message, - translated: Message, - locale: string, -): boolean { - if (typeof source === 'string') return typeof translated === 'string' - if (typeof translated === 'string') return false - return pluralCategories(locale).every((category) => category in translated) -} - -function isMessage(value: unknown): value is Message { - if (typeof value === 'string') return true - if (!(value && typeof value === 'object' && !Array.isArray(value))) return false - const record = value as Record - return ( - typeof record.other === 'string' && - Object.entries(record).every( - ([key, entry]) => - PLURAL_KEYS.includes(key as (typeof PLURAL_KEYS)[number]) && typeof entry === 'string', +function applyTranslations( + po: PoFile, + sources: SourceMessage[], + entries: Record, + provider: TranslationProvider, +): void { + const byId = new Map(sources.map((source) => [source.id, source])) + for (const item of po.items) { + const source = byId.get(item.msgid) + const entry = entries[item.msgid] + if (!(source && entry)) continue + item.msgstr = [entry.translation] + replaceComment(item, ENGLISH_PREFIX, JSON.stringify(source.message)) + replaceComment(item, SOURCE_HASH_PREFIX, entry.sourceHash) + replaceComment(item, REVIEW_PREFIX, entry.review) + replaceComment(item, REVIEW_NOTES_PREFIX, entry.reviewNotes || undefined) + replaceComment( + item, + AI_PROVIDER_PREFIX, + entry.review === 'required' ? provider.name : undefined, ) - ) + replaceComment(item, AI_MODEL_PREFIX, entry.review === 'required' ? provider.model : undefined) + if (entry.fuzzy) item.flags.fuzzy = true + else delete item.flags.fuzzy + } } -function currentSourceMatches(expected: SourceCatalog, actual: SourceCatalog): string[] { - const errors: string[] = [] - for (const [key, entry] of Object.entries(actual)) { - const stored = expected[key] - if (!stored) errors.push(`source catalog is missing "${key}"`) - else if ( - stored.hash !== entry.hash || - JSON.stringify(stored.default) !== JSON.stringify(entry.default) - ) { - errors.push(`source catalog is stale for "${key}"`) +function sourceFiles(): string[] { + const files = new Set() + for (const pattern of SOURCE_GLOBS) { + const glob = new Bun.Glob(pattern) + for (const path of glob.scanSync({ cwd: ROOT, absolute: true, onlyFiles: true })) { + if ( + path.includes('/node_modules/') || + /\.test\.[tj]sx?$/.test(path) || + path.includes('/__fixtures__/') + ) { + continue + } + files.add(path) } } - for (const key of Object.keys(expected)) { - if (!actual[key]) errors.push(`source catalog has removed key "${key}"`) - } - return errors + return [...files].sort() } function bypassedLiterals(): string[] { @@ -268,20 +490,19 @@ function bypassedLiterals(): string[] { ) for (const path of paths) { - const sourceText = readFileSync(path, 'utf8') const source = ts.createSourceFile( path, - sourceText, + readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ) - function warning(node: ts.Node, text: string): void { + const warning = (node: ts.Node, text: string) => { if (!/[A-Za-z]{2}/.test(text)) return const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 warnings.push(`${relative(ROOT, path)}:${line}: "${text.trim()}"`) } - function visit(node: ts.Node): void { + const visit = (node: ts.Node): void => { if (ts.isJsxText(node)) warning(node, node.text) if ( ts.isJsxAttribute(node) && @@ -298,208 +519,131 @@ function bypassedLiterals(): string[] { return warnings } -export function checkCatalogs(root = ROOT): string[] { - const errors: string[] = [] - const actual = extractSourceCatalog(root) - const stored = readJson(SOURCE_CATALOG_PATH) - errors.push(...currentSourceMatches(stored, actual)) - - for (const locale of BUNDLED_LOCALES) { - const catalog = readJson(localeCatalogPath(locale)) - for (const [key, source] of Object.entries(actual)) { - const translation = catalog.messages[key] - if (!translation) { - errors.push(`${locale}: missing "${key}"`) - continue - } - if (translation.sourceHash !== source.hash) errors.push(`${locale}: stale "${key}"`) - if (!samePlaceholders(source.default, translation.message)) { - errors.push(`${locale}: broken placeholders in "${key}"`) - } - if (!hasRequiredPluralForms(source.default, translation.message, locale)) { - errors.push(`${locale}: missing plural forms in "${key}"`) - } - } - } - return errors +function catalogFiles(): string[] { + return ALL_LOCALES.flatMap((locale) => [catalogPath(locale), compiledCatalogPath(locale)]) } -function promptFor(request: TranslationRequest): string { - return [ - `Translate these Pascal architecture/CAD interface messages into ${request.locale}.`, - 'Return one JSON object mapping every input key to its translated string or plural-form object.', - 'Preserve {placeholders} exactly. Keep labels concise and use a consistent professional CAD glossary.', - `For plural objects, include every cardinal category required by Intl.PluralRules: ${pluralCategories(request.locale).join(', ')}.`, - 'Do not add commentary or markdown. Human reviewers will approve the resulting diff.', - `Glossary: ${JSON.stringify(request.glossary)}`, - `Messages: ${JSON.stringify(request.items)}`, - ].join('\n\n') +function snapshot(paths: string[]): Map { + return new Map(paths.map((path) => [path, readFileSync(path, 'utf8')])) } -function parseProviderResponse(text: string, request: TranslationRequest): Record { - const start = text.indexOf('{') - const end = text.lastIndexOf('}') - if (start < 0 || end < start) throw new Error('Translation provider did not return JSON') - const parsed = JSON.parse(text.slice(start, end + 1)) as Record - const messages: Record = {} - for (const item of request.items) { - const message = parsed[item.key] - if (!isMessage(message)) throw new Error(`Translation provider omitted "${item.key}"`) - if (!samePlaceholders(item.source, message)) { - throw new Error(`Translation provider changed placeholders in "${item.key}"`) - } - if (!hasRequiredPluralForms(item.source, message, request.locale)) { - throw new Error(`Translation provider omitted plural forms in "${item.key}"`) - } - messages[item.key] = message - } - return messages +function changedSince(before: Map): string[] { + return [...before].flatMap(([path, contents]) => + readFileSync(path, 'utf8') === contents ? [] : [relative(ROOT, path)], + ) } -function configuredProvider(): TranslationProvider { - const provider = - process.env.I18N_PROVIDER ?? - (process.env.OPENAI_API_KEY ? 'openai' : process.env.ANTHROPIC_API_KEY ? 'anthropic' : '') - const model = process.env.I18N_MODEL - if (!model) throw new Error('Set I18N_MODEL to the translation model') - - if (provider === 'openai') { - const key = process.env.OPENAI_API_KEY - if (!key) throw new Error('Set OPENAI_API_KEY for the OpenAI translation provider') - return { - async translate(request) { - const response = await fetch( - `${process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, - { - method: 'POST', - headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ - model, - response_format: { type: 'json_object' }, - messages: [{ role: 'user', content: promptFor(request) }], - }), - }, - ) - if (!response.ok) throw new Error(`OpenAI translation failed (${response.status})`) - const body = (await response.json()) as { - choices?: Array<{ message?: { content?: string } }> - } - return parseProviderResponse(body.choices?.[0]?.message?.content ?? '', request) - }, - } - } - - if (provider === 'anthropic') { - const key = process.env.ANTHROPIC_API_KEY - if (!key) throw new Error('Set ANTHROPIC_API_KEY for the Anthropic translation provider') - return { - async translate(request) { - const response = await fetch( - `${process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'}/v1/messages`, - { - method: 'POST', - headers: { - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - 'x-api-key': key, - }, - body: JSON.stringify({ - model, - max_tokens: 8192, - messages: [{ role: 'user', content: promptFor(request) }], - }), - }, - ) - if (!response.ok) throw new Error(`Anthropic translation failed (${response.status})`) - const body = (await response.json()) as { - content?: Array<{ type?: string; text?: string }> - } - const text = body.content?.find((block) => block.type === 'text')?.text ?? '' - return parseProviderResponse(text, request) - }, - } +function checkCatalogMessages(sources: SourceMessage[], glossary: Glossary): string[] { + const errors: string[] = [] + const sourceById = new Map(sources.map((source) => [source.id, source])) + for (const source of sources) { + if (!source.message) errors.push(`en: missing source for "${source.id}"`) } - throw new Error('Set I18N_PROVIDER to "openai" or "anthropic" and provide its API key') -} - -export async function refreshLocaleCatalog( - source: SourceCatalog, - catalog: LocaleCatalog, - provider: TranslationProvider, - glossary: Record, -): Promise { - const stale = Object.entries(source) - .filter(([key, entry]) => catalog.messages[key]?.sourceHash !== entry.hash) - .map(([key, entry]) => ({ key, source: entry.default, context: entry.context })) - if (stale.length === 0) return catalog - - const messages = { ...catalog.messages } - for (let offset = 0; offset < stale.length; offset += 40) { - const items = stale.slice(offset, offset + 40) - const translated = await provider.translate({ locale: catalog.locale, items, glossary }) - for (const item of items) { - const message = translated[item.key] - if (!message) throw new Error(`Translation provider omitted "${item.key}"`) - if (!isMessage(message)) - throw new Error(`Translation provider returned an invalid message for "${item.key}"`) - if (!samePlaceholders(item.source, message)) { - throw new Error(`Translation provider changed placeholders in "${item.key}"`) + for (const locale of TARGET_LOCALES) { + const po = parsePo(readFileSync(catalogPath(locale), 'utf8')) + const entries = readTranslationState(po) + for (const source of sources) { + const entry = entries[source.id] + if (!entry?.translation) { + errors.push(`${locale}: missing "${source.id}"`) + continue + } + if (entry.sourceHash !== hashSource(source.message)) { + errors.push(`${locale}: stale "${source.id}"`) + } + if (entry.fuzzy || entry.review !== 'human') { + errors.push(`${locale}: "${source.id}" still requires human review`) } - if (!hasRequiredPluralForms(item.source, message, catalog.locale)) { - throw new Error(`Translation provider omitted plural forms in "${item.key}"`) + for (const error of validateTranslation( + source.message, + entry.translation, + locale, + glossary, + )) { + errors.push(`${locale}: invalid "${source.id}": ${error}`) } - messages[item.key] = { message, sourceHash: source[item.key]!.hash } + } + for (const id of Object.keys(entries)) { + if (!sourceById.has(id)) errors.push(`${locale}: obsolete "${id}"`) } } - return { ...catalog, messages: Object.fromEntries(Object.entries(messages).sort()) } + return errors +} + +export async function checkCatalogs(): Promise { + const before = snapshot(catalogFiles()) + runLingui('extract') + annotateCatalogs() + runLingui('compile', true) + const changed = changedSince(before) + const sources = readSourceMessages() + const glossary = readJson(GLOSSARY_PATH) + return [ + ...changed.map((path) => `${path} is not synchronized; run bun run i18n:extract`), + ...checkCatalogMessages(sources, glossary), + ] } async function main(): Promise { const command = process.argv[2] if (command === 'extract') { - const catalog = extractSourceCatalog() - writeJson(SOURCE_CATALOG_PATH, catalog) - console.log( - `Extracted ${Object.keys(catalog).length} messages to ${relative(ROOT, SOURCE_CATALOG_PATH)}`, - ) + runLingui('extract') + annotateCatalogs() + runLingui('compile') + console.log(`Extracted and compiled ${readSourceMessages().length} Lingui messages`) + return + } + + if (command === 'compile') { + runLingui('compile', true) + console.log(`Compiled ${ALL_LOCALES.length} Lingui catalogs`) return } if (command === 'check') { - const errors = checkCatalogs() + const errors = await checkCatalogs() for (const literal of bypassedLiterals()) { - console.warn(`[i18n warning] user-facing literal bypasses t(): ${literal}`) + console.warn(`[i18n warning] user-facing literal bypasses Lingui: ${literal}`) } if (errors.length > 0) { console.error(errors.map((error) => `[i18n] ${error}`).join('\n')) process.exitCode = 1 } else { - console.log(`Checked ${BUNDLED_LOCALES.length} bundled locale catalogs`) + console.log(`Checked ${TARGET_LOCALES.length} complete bundled PO catalogs`) } return } if (command === 'translate') { + runLingui('extract') + annotateCatalogs() const requested = process.argv.slice(3) - const locales = requested.length > 0 ? requested : [...BUNDLED_LOCALES] - const source = readJson(SOURCE_CATALOG_PATH) - const glossary = readJson>(GLOSSARY_PATH) + const locales = requested.length > 0 ? requested : [...TARGET_LOCALES] + const sources = readSourceMessages() + const glossary = readJson(GLOSSARY_PATH) const provider = configuredProvider() for (const locale of locales) { - if (!BUNDLED_LOCALES.includes(locale as (typeof BUNDLED_LOCALES)[number])) { + if (!TARGET_LOCALES.includes(locale as (typeof TARGET_LOCALES)[number])) { throw new Error(`Unknown bundled locale "${locale}"`) } - const path = localeCatalogPath(locale) - const catalog = readJson(path) - const refreshed = await refreshLocaleCatalog(source, catalog, provider, glossary) - writeJson(path, refreshed) - console.log(`Updated ${relative(ROOT, path)}`) + const path = catalogPath(locale) + const po = parsePo(readFileSync(path, 'utf8')) + const refreshed = await refreshTranslations( + locale, + sources, + readTranslationState(po), + provider, + glossary, + ) + applyTranslations(po, sources, refreshed, provider) + writePo(path, po) + console.log(`Updated ${relative(ROOT, path)}; AI entries are fuzzy and require human review`) } return } - throw new Error('Usage: bun scripts/i18n.ts [locale...]') + throw new Error('Usage: bun scripts/i18n.ts [locale...]') } if (import.meta.main) await main() From 74bbe0bbe1579c89fd231e3bf1e78307bb11526b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 22:39:32 +0000 Subject: [PATCH 15/23] fix(i18n): stabilize catalog validation --- .../editor/src/i18n/locales/en/messages.po | 106 +++++++++--------- .../editor/src/i18n/locales/es/messages.po | 106 +++++++++--------- .../editor/src/i18n/locales/fr/messages.po | 106 +++++++++--------- .../editor/src/i18n/locales/ja/messages.po | 106 +++++++++--------- .../editor/src/i18n/locales/zh-CN/messages.po | 106 +++++++++--------- scripts/i18n.ts | 2 +- 6 files changed, 266 insertions(+), 266 deletions(-) diff --git a/packages/editor/src/i18n/locales/en/messages.po b/packages/editor/src/i18n/locales/en/messages.po index 8e63c32fd0..bb377a46d4 100644 --- a/packages/editor/src/i18n/locales/en/messages.po +++ b/packages/editor/src/i18n/locales/en/messages.po @@ -91,108 +91,108 @@ msgstr "Draw one wall at a time." msgid "editor.buildPanel.rooms.walls.title" msgstr "Walls" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 msgid "viewer.camera.orbitLeft" msgstr "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 msgid "viewer.camera.orbitRight" msgstr "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 msgid "viewer.camera.topView" msgstr "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 msgid "viewer.common.off" msgstr "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 msgid "viewer.common.on" msgstr "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 msgid "viewer.display.camera" msgstr "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 msgid "viewer.display.colored" msgstr "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 msgid "viewer.display.colors" msgstr "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 msgid "viewer.display.edges.off" msgstr "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 msgid "viewer.display.edges.offDetail" msgstr "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 msgid "viewer.display.edges.soft" msgstr "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 msgid "viewer.display.edges.softDetail" msgstr "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 msgid "viewer.display.edges.strong" msgstr "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 msgid "viewer.display.edges.strongDetail" msgstr "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 msgid "viewer.display.edges.title" msgstr "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 msgid "viewer.display.monochrome" msgstr "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 msgid "viewer.display.orthographic" msgstr "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 msgid "viewer.display.perspective" msgstr "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 msgid "viewer.display.render.rendered" msgstr "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 msgid "viewer.display.render.renderedDetail" msgstr "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 msgid "viewer.display.render.solid" msgstr "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 msgid "viewer.display.render.solidDetail" msgstr "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 msgid "viewer.display.render.title" msgstr "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 msgid "viewer.display.shadows" msgstr "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 msgid "viewer.display.title" msgstr "Display settings" @@ -261,23 +261,23 @@ msgstr "Untitled" msgid "viewer.header.zones" msgstr "Zones" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 msgid "viewer.levels.exploded" msgstr "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 msgid "viewer.levels.label" msgstr "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 msgid "viewer.levels.manual" msgstr "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 msgid "viewer.levels.solo" msgstr "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 msgid "viewer.levels.stacked" msgstr "Stacked" @@ -289,74 +289,74 @@ msgstr "Viewer layout" msgid "viewer.stage.split" msgstr "Split" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 msgid "viewer.theme.blueprint" msgstr "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 msgid "viewer.theme.mediterranean" msgstr "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 msgid "viewer.theme.night" msgstr "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 msgid "viewer.theme.overcast" msgstr "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 msgid "viewer.theme.paper" msgstr "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 msgid "viewer.theme.studio" msgstr "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 msgid "viewer.theme.sunset" msgstr "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 msgid "viewer.theme.title" msgstr "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 msgid "viewer.theme.twilight" msgstr "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 msgid "viewer.theme.verdant" msgstr "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 msgid "viewer.visibility.guides" msgstr "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 msgid "viewer.visibility.scans" msgstr "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 msgid "viewer.visibility.title" msgstr "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 msgid "viewer.walkthrough.label" msgstr "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 msgid "viewer.walls.cutaway" msgstr "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 msgid "viewer.walls.fullHeight" msgstr "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 msgid "viewer.walls.label" msgstr "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 msgid "viewer.walls.low" msgstr "Low" diff --git a/packages/editor/src/i18n/locales/es/messages.po b/packages/editor/src/i18n/locales/es/messages.po index e2a5302907..0826c19ae2 100644 --- a/packages/editor/src/i18n/locales/es/messages.po +++ b/packages/editor/src/i18n/locales/es/messages.po @@ -160,180 +160,180 @@ msgstr "Muros" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 msgid "viewer.camera.orbitLeft" msgstr "Orbitar a la izquierda" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 msgid "viewer.camera.orbitRight" msgstr "Orbitar a la derecha" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 msgid "viewer.camera.topView" msgstr "Vista superior" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 msgid "viewer.common.off" msgstr "Desactivado" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 msgid "viewer.common.on" msgstr "Activado" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 msgid "viewer.display.camera" msgstr "Cámara" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 msgid "viewer.display.colored" msgstr "A color" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 msgid "viewer.display.colors" msgstr "Colores" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 msgid "viewer.display.edges.off" msgstr "Desactivadas" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 msgid "viewer.display.edges.offDetail" msgstr "Sin líneas de arista" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 msgid "viewer.display.edges.soft" msgstr "Suaves" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 msgid "viewer.display.edges.softDetail" msgstr "Contorno tenue de los pliegues principales" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 msgid "viewer.display.edges.strong" msgstr "Marcadas" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 msgid "viewer.display.edges.strongDetail" msgstr "Líneas de arista nítidas y opacas" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 msgid "viewer.display.edges.title" msgstr "Aristas" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 msgid "viewer.display.monochrome" msgstr "Monocromo" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 msgid "viewer.display.orthographic" msgstr "Ortográfica" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 msgid "viewer.display.perspective" msgstr "Perspectiva" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 msgid "viewer.display.render.rendered" msgstr "Realista" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 msgid "viewer.display.render.renderedDetail" msgstr "Oclusión ambiental completa" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 msgid "viewer.display.render.solid" msgstr "Sólido" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 msgid "viewer.display.render.solidDetail" msgstr "Plano y rápido, sin oclusión ambiental" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 msgid "viewer.display.render.title" msgstr "Renderizado" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 msgid "viewer.display.shadows" msgstr "Sombras" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 msgid "viewer.display.title" msgstr "Ajustes de visualización" @@ -450,35 +450,35 @@ msgstr "Zonas" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 msgid "viewer.levels.exploded" msgstr "Separadas" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 msgid "viewer.levels.label" msgstr "Plantas: {mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 msgid "viewer.levels.manual" msgstr "Manual" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 msgid "viewer.levels.solo" msgstr "Aislada" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 msgid "viewer.levels.stacked" msgstr "Apiladas" @@ -499,125 +499,125 @@ msgstr "Dividida" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 msgid "viewer.theme.blueprint" msgstr "Plano técnico" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 msgid "viewer.theme.mediterranean" msgstr "Mediterráneo" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 msgid "viewer.theme.night" msgstr "Noche" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 msgid "viewer.theme.overcast" msgstr "Nublado" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 msgid "viewer.theme.paper" msgstr "Papel" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 msgid "viewer.theme.studio" msgstr "Estudio" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 msgid "viewer.theme.sunset" msgstr "Atardecer" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 msgid "viewer.theme.title" msgstr "Ambiente" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 msgid "viewer.theme.twilight" msgstr "Crepúsculo" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 msgid "viewer.theme.verdant" msgstr "Verde" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 msgid "viewer.visibility.guides" msgstr "Guías" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 msgid "viewer.visibility.scans" msgstr "Escaneos 3D" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 msgid "viewer.visibility.title" msgstr "Visibilidad" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 msgid "viewer.walkthrough.label" msgstr "Recorrido: {state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 msgid "viewer.walls.cutaway" msgstr "Vista seccionada" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 msgid "viewer.walls.fullHeight" msgstr "Altura completa" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 msgid "viewer.walls.label" msgstr "Muros: {mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 msgid "viewer.walls.low" msgstr "Bajos" diff --git a/packages/editor/src/i18n/locales/fr/messages.po b/packages/editor/src/i18n/locales/fr/messages.po index 86eddafe95..d9ee69fa63 100644 --- a/packages/editor/src/i18n/locales/fr/messages.po +++ b/packages/editor/src/i18n/locales/fr/messages.po @@ -160,180 +160,180 @@ msgstr "Murs" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 msgid "viewer.camera.orbitLeft" msgstr "Pivoter à gauche" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 msgid "viewer.camera.orbitRight" msgstr "Pivoter à droite" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 msgid "viewer.camera.topView" msgstr "Vue de dessus" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 msgid "viewer.common.off" msgstr "Désactivé" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 msgid "viewer.common.on" msgstr "Activé" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 msgid "viewer.display.camera" msgstr "Caméra" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 msgid "viewer.display.colored" msgstr "En couleur" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 msgid "viewer.display.colors" msgstr "Couleurs" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 msgid "viewer.display.edges.off" msgstr "Désactivées" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 msgid "viewer.display.edges.offDetail" msgstr "Aucune ligne d’arête" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 msgid "viewer.display.edges.soft" msgstr "Douces" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 msgid "viewer.display.edges.softDetail" msgstr "Contour léger des plis principaux" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 msgid "viewer.display.edges.strong" msgstr "Marquées" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 msgid "viewer.display.edges.strongDetail" msgstr "Lignes d’arête nettes et opaques" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 msgid "viewer.display.edges.title" msgstr "Arêtes" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 msgid "viewer.display.monochrome" msgstr "Monochrome" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 msgid "viewer.display.orthographic" msgstr "Orthographique" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 msgid "viewer.display.perspective" msgstr "Perspective" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 msgid "viewer.display.render.rendered" msgstr "Réaliste" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 msgid "viewer.display.render.renderedDetail" msgstr "Occlusion ambiante complète" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 msgid "viewer.display.render.solid" msgstr "Solide" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 msgid "viewer.display.render.solidDetail" msgstr "Rapide et uniforme, sans occlusion ambiante" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 msgid "viewer.display.render.title" msgstr "Rendu" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 msgid "viewer.display.shadows" msgstr "Ombres" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 msgid "viewer.display.title" msgstr "Paramètres d’affichage" @@ -450,35 +450,35 @@ msgstr "Zones" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 msgid "viewer.levels.exploded" msgstr "Éclatés" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 msgid "viewer.levels.label" msgstr "Niveaux : {mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 msgid "viewer.levels.manual" msgstr "Manuel" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 msgid "viewer.levels.solo" msgstr "Isolé" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 msgid "viewer.levels.stacked" msgstr "Superposés" @@ -499,125 +499,125 @@ msgstr "Partagé" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 msgid "viewer.theme.blueprint" msgstr "Plan" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 msgid "viewer.theme.mediterranean" msgstr "Méditerranéen" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 msgid "viewer.theme.night" msgstr "Nuit" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 msgid "viewer.theme.overcast" msgstr "Ciel couvert" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 msgid "viewer.theme.paper" msgstr "Papier" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 msgid "viewer.theme.studio" msgstr "Studio" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 msgid "viewer.theme.sunset" msgstr "Coucher de soleil" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 msgid "viewer.theme.title" msgstr "Ambiance" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 msgid "viewer.theme.twilight" msgstr "Crépuscule" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 msgid "viewer.theme.verdant" msgstr "Verdoyant" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 msgid "viewer.visibility.guides" msgstr "Repères" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 msgid "viewer.visibility.scans" msgstr "Relevés 3D" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 msgid "viewer.visibility.title" msgstr "Visibilité" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 msgid "viewer.walkthrough.label" msgstr "Visite : {state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 msgid "viewer.walls.cutaway" msgstr "Vue en coupe" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 msgid "viewer.walls.fullHeight" msgstr "Pleine hauteur" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 msgid "viewer.walls.label" msgstr "Murs : {mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 msgid "viewer.walls.low" msgstr "Bas" diff --git a/packages/editor/src/i18n/locales/ja/messages.po b/packages/editor/src/i18n/locales/ja/messages.po index bc74dd7e86..62b46d5f82 100644 --- a/packages/editor/src/i18n/locales/ja/messages.po +++ b/packages/editor/src/i18n/locales/ja/messages.po @@ -160,180 +160,180 @@ msgstr "壁" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 msgid "viewer.camera.orbitLeft" msgstr "左に回転" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 msgid "viewer.camera.orbitRight" msgstr "右に回転" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 msgid "viewer.camera.topView" msgstr "上面図" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 msgid "viewer.common.off" msgstr "オフ" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 msgid "viewer.common.on" msgstr "オン" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 msgid "viewer.display.camera" msgstr "カメラ" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 msgid "viewer.display.colored" msgstr "カラー表示" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 msgid "viewer.display.colors" msgstr "カラー" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 msgid "viewer.display.edges.off" msgstr "オフ" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 msgid "viewer.display.edges.offDetail" msgstr "エッジ線を表示しない" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 msgid "viewer.display.edges.soft" msgstr "ソフト" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 msgid "viewer.display.edges.softDetail" msgstr "主要な稜線を薄く表示" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 msgid "viewer.display.edges.strong" msgstr "強調" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 msgid "viewer.display.edges.strongDetail" msgstr "エッジ線をくっきり表示" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 msgid "viewer.display.edges.title" msgstr "エッジ" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 msgid "viewer.display.monochrome" msgstr "モノクロ" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 msgid "viewer.display.orthographic" msgstr "平行投影" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 msgid "viewer.display.perspective" msgstr "透視投影" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 msgid "viewer.display.render.rendered" msgstr "レンダリング" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 msgid "viewer.display.render.renderedDetail" msgstr "アンビエントオクルージョンを完全表示" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 msgid "viewer.display.render.solid" msgstr "ソリッド" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 msgid "viewer.display.render.solidDetail" msgstr "軽量表示(アンビエントオクルージョンなし)" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 msgid "viewer.display.render.title" msgstr "レンダリング" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 msgid "viewer.display.shadows" msgstr "影" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 msgid "viewer.display.title" msgstr "表示設定" @@ -450,35 +450,35 @@ msgstr "ゾーン" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 msgid "viewer.levels.exploded" msgstr "分解" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 msgid "viewer.levels.label" msgstr "階層:{mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 msgid "viewer.levels.manual" msgstr "手動" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 msgid "viewer.levels.solo" msgstr "単独" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 msgid "viewer.levels.stacked" msgstr "積層" @@ -499,125 +499,125 @@ msgstr "分割" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 msgid "viewer.theme.blueprint" msgstr "青図" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 msgid "viewer.theme.mediterranean" msgstr "地中海" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 msgid "viewer.theme.night" msgstr "夜景" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 msgid "viewer.theme.overcast" msgstr "曇天" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 msgid "viewer.theme.paper" msgstr "ペーパー" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 msgid "viewer.theme.studio" msgstr "スタジオ" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 msgid "viewer.theme.sunset" msgstr "夕景" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 msgid "viewer.theme.title" msgstr "シーンテーマ" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 msgid "viewer.theme.twilight" msgstr "薄暮" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 msgid "viewer.theme.verdant" msgstr "緑景" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 msgid "viewer.visibility.guides" msgstr "ガイド" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 msgid "viewer.visibility.scans" msgstr "3Dスキャン" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 msgid "viewer.visibility.title" msgstr "表示" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 msgid "viewer.walkthrough.label" msgstr "ウォークスルー:{state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 msgid "viewer.walls.cutaway" msgstr "カット表示" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 msgid "viewer.walls.fullHeight" msgstr "全高" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 msgid "viewer.walls.label" msgstr "壁:{mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 msgid "viewer.walls.low" msgstr "低く表示" diff --git a/packages/editor/src/i18n/locales/zh-CN/messages.po b/packages/editor/src/i18n/locales/zh-CN/messages.po index 31b20848f9..d8aa8ebbb2 100644 --- a/packages/editor/src/i18n/locales/zh-CN/messages.po +++ b/packages/editor/src/i18n/locales/zh-CN/messages.po @@ -160,180 +160,180 @@ msgstr "墙体" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:511 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:519 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 msgid "viewer.camera.orbitLeft" msgstr "向左旋转" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:527 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:535 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 msgid "viewer.camera.orbitRight" msgstr "向右旋转" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:543 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:551 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 msgid "viewer.camera.topView" msgstr "俯视图" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:233 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:491 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 msgid "viewer.common.off" msgstr "关闭" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:232 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:490 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 msgid "viewer.common.on" msgstr "开启" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 msgid "viewer.display.camera" msgstr "相机" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:260 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 msgid "viewer.display.colored" msgstr "彩色" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 msgid "viewer.display.colors" msgstr "颜色" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:187 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 msgid "viewer.display.edges.off" msgstr "关闭" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:188 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 msgid "viewer.display.edges.offDetail" msgstr "不显示边线" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:192 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 msgid "viewer.display.edges.soft" msgstr "柔和" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:193 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 msgid "viewer.display.edges.softDetail" msgstr "淡化显示主要折线" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:200 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 msgid "viewer.display.edges.strong" msgstr "清晰" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:201 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 msgid "viewer.display.edges.strongDetail" msgstr "清晰、不透明的边线" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:333 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 msgid "viewer.display.edges.title" msgstr "边线" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:261 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 msgid "viewer.display.monochrome" msgstr "单色" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:250 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 msgid "viewer.display.orthographic" msgstr "正交" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:249 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 msgid "viewer.display.perspective" msgstr "透视" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:176 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 msgid "viewer.display.render.rendered" msgstr "渲染效果" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:177 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 msgid "viewer.display.render.renderedDetail" msgstr "完整环境光遮蔽" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:167 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 msgid "viewer.display.render.solid" msgstr "实体" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:168 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 msgid "viewer.display.render.solidDetail" msgstr "平整快速,不启用环境光遮蔽" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:270 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 msgid "viewer.display.render.title" msgstr "渲染" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 msgid "viewer.display.shadows" msgstr "阴影" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:215 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 msgid "viewer.display.title" msgstr "显示设置" @@ -450,35 +450,35 @@ msgstr "分区" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 msgid "viewer.levels.exploded" msgstr "分解" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:418 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 msgid "viewer.levels.label" msgstr "楼层:{mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:65 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 msgid "viewer.levels.manual" msgstr "手动" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:64 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 msgid "viewer.levels.solo" msgstr "单层" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 msgid "viewer.levels.stacked" msgstr "叠层" @@ -499,125 +499,125 @@ msgstr "分屏" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 msgid "viewer.theme.blueprint" msgstr "蓝图" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 msgid "viewer.theme.mediterranean" msgstr "地中海" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:84 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 msgid "viewer.theme.night" msgstr "夜景" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 msgid "viewer.theme.overcast" msgstr "阴天" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 msgid "viewer.theme.paper" msgstr "纸面" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 msgid "viewer.theme.studio" msgstr "影棚" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:78 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 msgid "viewer.theme.sunset" msgstr "日落" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:298 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 msgid "viewer.theme.title" msgstr "场景主题" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:83 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 msgid "viewer.theme.twilight" msgstr "暮色" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:85 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 msgid "viewer.theme.verdant" msgstr "绿意" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:141 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 msgid "viewer.visibility.guides" msgstr "参考图" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:128 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 msgid "viewer.visibility.scans" msgstr "扫描模型" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:113 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 msgid "viewer.visibility.title" msgstr "可见性" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:485 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 msgid "viewer.walkthrough.label" msgstr "漫游:{state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:72 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 msgid "viewer.walls.cutaway" msgstr "剖切墙" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:70 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 msgid "viewer.walls.fullHeight" msgstr "全高墙" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:447 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 msgid "viewer.walls.label" msgstr "墙体:{mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:71 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 msgid "viewer.walls.low" msgstr "矮墙" diff --git a/scripts/i18n.ts b/scripts/i18n.ts index 416469026b..e8a6cc31ff 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -203,7 +203,7 @@ function validateGlossary( const normalizedTerm = english.toLocaleLowerCase('en') if ( typeof expected === 'string' && - (normalizedSource === normalizedTerm || normalizedSource === `${normalizedTerm}s`) && + normalizedSource === normalizedTerm && !normalizedTranslation.includes(expected.toLocaleLowerCase(locale)) ) { errors.push(`expected glossary term "${expected}" for "${english}"`) From c93ff63e05cbd25f25dac35ecadd97b1b80d40a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 22:40:34 +0000 Subject: [PATCH 16/23] test(i18n): assert localized server output --- packages/core/src/i18n/i18n.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index 4d5cf7e483..ea93db2df8 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -192,7 +192,9 @@ test('server rendering uses the selected locale for text and numbers', async () , ) - expect(serverHtml).toContain('Bonjour:2,75') + expect(serverHtml).toContain('Bonjour') + expect(serverHtml).toContain('2,75') + expect(serverHtml).not.toContain('Hello') scope.destroy() }) From f91ad4c10d5ebd1870000789ebcdca76466b180f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 23:04:35 +0000 Subject: [PATCH 17/23] fix(i18n): honor scoped locales in editor controls --- apps/editor/proxy.test.ts | 4 +- .../ui/controls/slider-control.test.tsx | 21 ++++++++++ .../components/ui/controls/slider-control.tsx | 42 +++++++++++++------ .../components/viewer/floorplan-preview.tsx | 38 ++--------------- .../components/viewer/viewer-scene-header.tsx | 30 +------------ packages/editor/src/i18n/level-name.ts | 36 ++++++++++++++++ packages/editor/src/i18n/locales.test.ts | 20 ++++++++- packages/editor/src/lib/measurement-parser.ts | 6 ++- packages/editor/src/lib/measurements.test.ts | 16 +++---- 9 files changed, 122 insertions(+), 91 deletions(-) create mode 100644 packages/editor/src/components/ui/controls/slider-control.test.tsx create mode 100644 packages/editor/src/i18n/level-name.ts diff --git a/apps/editor/proxy.test.ts b/apps/editor/proxy.test.ts index 59c62f2e61..441fe73513 100644 --- a/apps/editor/proxy.test.ts +++ b/apps/editor/proxy.test.ts @@ -31,10 +31,10 @@ test('an unconfigured network host sends pages to setup and APIs to 503', async test('a password gates pages behind login and keeps the requested path', () => { process.env.PASCAL_ADMIN_PASSWORD = 'secret' - const response = gateRequest(request('/project/abc?tab=build')) + const response = gateRequest(request('/project/abc?tab=build&lang=fr')) expect(response?.status).toBe(307) expect(response?.headers.get('location')).toBe( - 'https://pascal.example/login?next=%2Fproject%2Fabc%3Ftab%3Dbuild', + 'https://pascal.example/login?next=%2Fproject%2Fabc%3Ftab%3Dbuild%26lang%3Dfr', ) expect(gateRequest(request('/api/plugins/mint/session'))?.status).toBe(401) }) diff --git a/packages/editor/src/components/ui/controls/slider-control.test.tsx b/packages/editor/src/components/ui/controls/slider-control.test.tsx new file mode 100644 index 0000000000..bfb964d811 --- /dev/null +++ b/packages/editor/src/components/ui/controls/slider-control.test.tsx @@ -0,0 +1,21 @@ +import { expect, test } from 'bun:test' +import { createI18n, PascalI18nProvider } from '@pascal-app/core' +import { renderToString } from 'react-dom/server' +import { parseSliderBareValue, SliderControl } from './slider-control' + +test('slider numbers use the scoped locale for display and parsing', () => { + const i18n = createI18n({ locale: 'fr' }) + try { + const html = renderToString( + + {}} precision={2} value={2.75} /> + , + ) + + expect(html).toContain('2,75') + expect(parseSliderBareValue('2,75', i18n.getLocale())).toBe(2.75) + expect(parseSliderBareValue('1e2', i18n.getLocale())).toBe(100) + } finally { + i18n.destroy() + } +}) diff --git a/packages/editor/src/components/ui/controls/slider-control.tsx b/packages/editor/src/components/ui/controls/slider-control.tsx index bdfc13e270..995c02a494 100644 --- a/packages/editor/src/components/ui/controls/slider-control.tsx +++ b/packages/editor/src/components/ui/controls/slider-control.tsx @@ -1,6 +1,6 @@ 'use client' -import { parseLocalizedNumber, useScene } from '@pascal-app/core' +import { formatNumber, parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core' import { useCallback, useEffect, useRef, useState } from 'react' import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser' import { useLinearDisplay } from '../../../lib/use-linear-display' @@ -23,6 +23,10 @@ interface SliderControlProps { mixed?: boolean } +export function parseSliderBareValue(text: string, locale: string): number | null { + return parseLocalizedNumber(text, locale) +} + function stepPrecision(s: number): number { if (s <= 0) return 0 return Math.max(0, Math.ceil(-Math.log10(s))) @@ -67,14 +71,24 @@ export function SliderControl({ restoreOnCommit = true, mixed = false, }: SliderControlProps) { + const locale = useLocale() // Values and bounds stay in meters; gestures and input use the displayed unit. const { isImperial, displayUnit, parseUnit, precision, step, toDisplay, toStored } = useLinearDisplay(unit, storedPrecision, storedStep) + const formatDisplay = useCallback( + (displayValue: number) => + formatNumber(displayValue, { + locale, + min: precision, + max: precision, + }), + [locale, precision], + ) const [isEditing, setIsEditing] = useState(false) const [isDragging, setIsDragging] = useState(false) const [isHovered, setIsHovered] = useState(false) - const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision)) + const [inputValue, setInputValue] = useState(() => formatDisplay(toDisplay(value))) // Live readout while dragging. Multi-edit previews write live overrides // instead of the scene, so `value` would otherwise stay frozen even though // the meshes are moving. @@ -116,9 +130,9 @@ export function SliderControl({ useEffect(() => { if (!isEditing) { - setInputValue(toDisplay(value).toFixed(precision)) + setInputValue(formatDisplay(toDisplay(value))) } - }, [value, precision, isEditing, toDisplay]) + }, [value, isEditing, toDisplay, formatDisplay]) // Wheel support on the label useEffect(() => { @@ -258,8 +272,8 @@ export function SliderControl({ const handleValueClick = useCallback(() => { editingRef.current = true setIsEditing(true) - setInputValue(toDisplay(value).toFixed(precision)) - }, [value, precision, toDisplay]) + setInputValue(formatDisplay(toDisplay(value))) + }, [value, toDisplay, formatDisplay]) const parseInputValue = useCallback( (text: string) => { @@ -267,16 +281,17 @@ export function SliderControl({ const parsed = spec ? parseMeasurement(text, spec, { bareUnit: parseUnit ?? spec.unitId, + locale, system: isImperial ? 'us' : 'metric', }) : null - const bare = parseLocalizedNumber(text) + const bare = parseSliderBareValue(text, locale) const stored = parsed ?? (bare === null ? null : toStored(bare)) return stored === null ? null : clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision)))) }, - [unit, parseUnit, isImperial, toStored, clamp, toDisplay, precision], + [unit, parseUnit, locale, isImperial, toStored, clamp, toDisplay, precision], ) const submitValue = useCallback(() => { @@ -285,13 +300,13 @@ export function SliderControl({ const nextValue = parseInputValue(inputValue) if (nextValue === null) { onCancel?.() - setInputValue(toDisplay(value).toFixed(precision)) + setInputValue(formatDisplay(toDisplay(value))) } else { onChange(nextValue) onCommit?.(nextValue) } setIsEditing(false) - }, [inputValue, parseInputValue, onCancel, onChange, onCommit, toDisplay, value, precision]) + }, [inputValue, parseInputValue, onCancel, onChange, onCommit, toDisplay, value, formatDisplay]) const spec = lingoUnitSpec(unit) const hint = @@ -299,6 +314,7 @@ export function SliderControl({ ? measurementHint(inputValue, spec, { bareUnit: parseUnit ?? spec.unitId, system: isImperial ? 'us' : 'metric', + locale, displayUnit: parseUnit ?? spec.unitId, precision, clamp, @@ -317,10 +333,10 @@ export function SliderControl({ const adjustedStep = getAdjustedStep(step, e) const newV = applyDisplayDelta(value, direction * adjustedStep, adjustedStep) onChange(newV) - setInputValue(toDisplay(newV).toFixed(precision)) + setInputValue(formatDisplay(toDisplay(newV))) } }, - [submitValue, cancelEdit, value, precision, step, applyDisplayDelta, onChange, toDisplay], + [submitValue, cancelEdit, value, step, applyDisplayDelta, onChange, toDisplay, formatDisplay], ) const displayValue = toDisplay(shown) @@ -414,7 +430,7 @@ export function SliderControl({ onClick={handleValueClick} > - {Number(displayValue.toFixed(precision)).toFixed(precision)} + {formatDisplay(displayValue)} {displayUnit && {displayUnit}}
diff --git a/packages/editor/src/components/viewer/floorplan-preview.tsx b/packages/editor/src/components/viewer/floorplan-preview.tsx index 144755c29d..d4fefda9a3 100644 --- a/packages/editor/src/components/viewer/floorplan-preview.tsx +++ b/packages/editor/src/components/viewer/floorplan-preview.tsx @@ -6,11 +6,8 @@ import { type FloorplanGeometry, type FloorplanPalette, type GeometryContext, - getLevelDisplayName, isNodeKindEnabled, - levelDisplayNumber, nodeRegistry, - type PascalI18n, useLocale, usePascalI18n, useScene, @@ -31,6 +28,7 @@ import { useState, } from 'react' import { createPortal } from 'react-dom' +import { localizedLevelName } from '../../i18n/level-name' import { FLOORPLAN_VIEW_ROTATION_DEG, floorplanLocalToWorldPoint, @@ -147,36 +145,6 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox { } } -function levelLabel(level: AnyNode, i18n: PascalI18n, locale: string): string { - const levelNumber = (level as { level?: number }).level ?? 0 - const levelName = (level as { name?: string }).name - const english = getLevelDisplayName({ - name: levelName, - level: levelNumber, - }) - const authoredName = levelName?.trim() - if (authoredName && authoredName !== `Level ${levelNumber}`) return authoredName - if (levelNumber === 0) { - return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' }) - } - if (levelNumber === -1) return i18n._({ id: 'core.level.basement', message: 'Basement' }) - if (levelNumber < -1) { - return i18n._({ - id: 'core.level.basementNumber', - message: 'Basement {number}', - values: { number: -levelNumber }, - }) - } - if (levelNumber > 0) { - return i18n._({ - id: 'core.level.floorNumber', - message: 'Floor {number}', - values: { number: levelDisplayNumber(levelNumber, locale) }, - }) - } - return english -} - function collectLevelTree(root: AnyNode, nodes: Record): AnyNode[] { const result: AnyNode[] = [root] const seen = new Set() @@ -919,7 +887,7 @@ export function FloorplanPreview({ message: '{floor} 2D view', values: { floor: activeLevel - ? levelLabel(activeLevel, i18n, locale) + ? localizedLevelName(activeLevel, i18n, locale) : i18n._({ id: 'viewer.floorplan.floorPlan', message: 'Floor plan' }), }, })} @@ -1025,7 +993,7 @@ export function FloorplanPreview({ > {levels.map((level) => ( ))} diff --git a/packages/editor/src/components/viewer/viewer-scene-header.tsx b/packages/editor/src/components/viewer/viewer-scene-header.tsx index e08582c9f8..0afa0fe492 100644 --- a/packages/editor/src/components/viewer/viewer-scene-header.tsx +++ b/packages/editor/src/components/viewer/viewer-scene-header.tsx @@ -1,9 +1,7 @@ 'use client' import { - getLevelDisplayName, type LevelNode, - levelDisplayNumber, type PascalI18n, sceneRegistry, useLocale, @@ -14,6 +12,7 @@ import { markPerfAction, useViewer } from '@pascal-app/viewer' import { ArrowLeft, ChevronRight, Layers } from 'lucide-react' import Link from 'next/link' import { type ReactNode, useMemo } from 'react' +import { localizedLevelName } from '../../i18n/level-name' import { cn } from '../../lib/utils' import { applyViewerClick, @@ -51,31 +50,6 @@ function sectionTitle(kind: ViewerSidebarSection['kind'], i18n: PascalI18n): str return i18n._({ id: 'viewer.header.zones', message: 'Zones' }) } -function translatedLevelName(level: LevelNode, i18n: PascalI18n, locale: string): string { - const english = getLevelDisplayName(level) - const authoredName = level.name?.trim() - if (authoredName && authoredName !== `Level ${level.level}`) return authoredName - if (level.level === 0) { - return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' }) - } - if (level.level === -1) return i18n._({ id: 'core.level.basement', message: 'Basement' }) - if (level.level < -1) { - return i18n._({ - id: 'core.level.basementNumber', - message: 'Basement {number}', - values: { number: -level.level }, - }) - } - if (level.level > 0) { - return i18n._({ - id: 'core.level.floorNumber', - message: 'Floor {number}', - values: { number: levelDisplayNumber(level.level, locale) }, - }) - } - return english -} - const parametricObject = (id: string) => sceneRegistry.nodes.get(id) /** @@ -252,7 +226,7 @@ export const ViewerSceneHeader = ({
- {translatedLevelName(lvl, i18n, locale)} + {localizedLevelName(lvl, i18n, locale)}
diff --git a/packages/editor/src/i18n/level-name.ts b/packages/editor/src/i18n/level-name.ts new file mode 100644 index 0000000000..7f38af079f --- /dev/null +++ b/packages/editor/src/i18n/level-name.ts @@ -0,0 +1,36 @@ +import { getDefaultLevelName, levelDisplayNumber, type PascalI18n } from '@pascal-app/core' + +type DisplayLevel = { + level?: number + name?: string +} + +export function localizedLevelName(level: DisplayLevel, i18n: PascalI18n, locale: string): string { + const levelNumber = level.level ?? 0 + const authoredName = level.name?.trim() + if ( + authoredName && + authoredName !== `Level ${levelNumber}` && + authoredName !== getDefaultLevelName(levelNumber) + ) { + return authoredName + } + if (levelNumber === 0) { + return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' }) + } + if (levelNumber === -1) { + return i18n._({ id: 'core.level.basement', message: 'Basement' }) + } + if (levelNumber < -1) { + return i18n._({ + id: 'core.level.basementNumber', + message: 'Basement {number}', + values: { number: -levelNumber }, + }) + } + return i18n._({ + id: 'core.level.floorNumber', + message: 'Floor {number}', + values: { number: levelDisplayNumber(levelNumber, locale) }, + }) +} diff --git a/packages/editor/src/i18n/locales.test.ts b/packages/editor/src/i18n/locales.test.ts index e9f8bdd84d..b191a31b81 100644 --- a/packages/editor/src/i18n/locales.test.ts +++ b/packages/editor/src/i18n/locales.test.ts @@ -1,5 +1,6 @@ import { afterEach, expect, test } from 'bun:test' -import { setLocale, translate } from '@pascal-app/core' +import { createI18n, setLocale, translate } from '@pascal-app/core' +import { localizedLevelName } from './level-name' import { registerBundledLocales } from './locales' afterEach(() => setLocale('en')) @@ -36,3 +37,20 @@ test('Simplified Chinese aliases use zh-CN without affecting Traditional Chinese setLocale('zh-TW') expect(translate('viewer.display.title', 'Display settings')).toBe('Display settings') }) + +test('generated stored level names localize while authored names remain unchanged', () => { + registerBundledLocales() + const french = createI18n({ locale: 'fr' }) + const japanese = createI18n({ locale: 'ja' }) + try { + expect(localizedLevelName({ level: 0, name: 'Ground floor' }, french, 'fr')).toBe( + 'Rez-de-chaussée', + ) + expect(localizedLevelName({ level: 1, name: 'Floor 1' }, french, 'fr')).toBe('Étage 1') + expect(localizedLevelName({ level: 1, name: 'Floor 1' }, japanese, 'ja')).toBe('2階') + expect(localizedLevelName({ level: 1, name: 'Mezzanine' }, french, 'fr')).toBe('Mezzanine') + } finally { + french.destroy() + japanese.destroy() + } +}) diff --git a/packages/editor/src/lib/measurement-parser.ts b/packages/editor/src/lib/measurement-parser.ts index d0ed429bbb..edc18810fd 100644 --- a/packages/editor/src/lib/measurement-parser.ts +++ b/packages/editor/src/lib/measurement-parser.ts @@ -57,6 +57,8 @@ export interface ParseMeasurementOptions { bareUnit?: string /** Disambiguates gal/ton/cup families; harmless for length/angle. */ system?: 'metric' | 'us' | 'imperial' + /** Locale whose decimal separator is accepted in bare values and quantities. */ + locale?: string } /** @@ -69,7 +71,7 @@ export function parseMeasurement( spec: LingoUnitSpec, options: ParseMeasurementOptions = {}, ): number | null { - const result = parseQuantity(normalizeDecimalInput(raw), { + const result = parseQuantity(normalizeDecimalInput(raw, options.locale), { kind: spec.kind, unit: options.bareUnit ?? spec.unitId, system: options.system, @@ -106,7 +108,7 @@ export function measurementHint( options: MeasurementHintOptions = {}, ): string | null { const trimmed = raw.trim() - const normalized = normalizeDecimalInput(trimmed) + const normalized = normalizeDecimalInput(trimmed, options.locale) if (!normalized || PLAIN_NUMBER.test(normalized)) return null const parsed = parseMeasurement(normalized, spec, options) diff --git a/packages/editor/src/lib/measurements.test.ts b/packages/editor/src/lib/measurements.test.ts index b3d4fd88de..3617bafa05 100644 --- a/packages/editor/src/lib/measurements.test.ts +++ b/packages/editor/src/lib/measurements.test.ts @@ -1,5 +1,4 @@ -import { afterEach, describe, expect, test } from 'bun:test' -import { setLocale } from '@pascal-app/core' +import { describe, expect, test } from 'bun:test' import { measurementHint, parseMeasurement } from '../index' import { buildMeasurementAngleArcPoints, @@ -194,8 +193,6 @@ describe('volume measurements', () => { }) describe('typed measurement fields', () => { - afterEach(() => setLocale('en')) - test('canonicalizes explicit, compound, and scientific dimensions', () => { const length = { kind: 'length', unitId: 'm' } as const expect(parseMeasurement('180cm', length)).toBeCloseTo(1.8) @@ -219,12 +216,11 @@ describe('typed measurement fields', () => { test('only treats the active locale decimal separator as a plain decimal', () => { const length = { kind: 'length', unitId: 'm' } as const - setLocale('en') - expect(measurementHint('1,000', length)).not.toBeNull() - expect(measurementHint('-1,000', length)).not.toBeNull() - expect(measurementHint('1,5', length)).not.toBeNull() + expect(measurementHint('1,000', length, { locale: 'en' })).not.toBeNull() + expect(measurementHint('-1,000', length, { locale: 'en' })).not.toBeNull() + expect(measurementHint('1,5', length, { locale: 'en' })).not.toBeNull() - setLocale('fr') - expect(measurementHint('1,5', length)).toBeNull() + expect(measurementHint('1,5', length, { locale: 'fr' })).toBeNull() + expect(parseMeasurement('2,75 m', length, { locale: 'fr' })).toBe(2.75) }) }) From 10415155e45def2d51d13a97063d43db72a3b26d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 23:05:36 +0000 Subject: [PATCH 18/23] fix(i18n): make catalog checks deterministic --- AGENTS.md | 2 +- bun.lock | 11 +- i18n/README.md | 5 +- lingui.config.ts | 2 +- package.json | 4 +- packages/core/package.json | 5 +- packages/core/src/i18n/use-translate.ts | 7 +- .../editor/src/i18n/locales/en/messages.po | 184 ++++++++---------- .../editor/src/i18n/locales/es/messages.po | 184 ++++++++---------- .../editor/src/i18n/locales/fr/messages.po | 184 ++++++++---------- .../editor/src/i18n/locales/ja/messages.po | 184 ++++++++---------- .../editor/src/i18n/locales/zh-CN/messages.po | 184 ++++++++---------- scripts/i18n.test.ts | 40 ++++ scripts/i18n.ts | 114 ++++++----- 14 files changed, 542 insertions(+), 568 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1024520f24..b13680b1cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Public, open-source home of `@pascal-app/{core,viewer,editor,nodes,mcp,cli,ifc-c ## Commands -`bun run ci` is exactly what CI runs (check, skills:validate, check-types, test, build). The git hooks (installed by `bun install`) are fast feedback: Biome on staged files, typecheck of the packages you changed. +`bun run ci` is exactly what CI runs (check, i18n:check, skills:validate, check-types, test, build). Localization catalog and language-pack guidance lives in `i18n/README.md`. The git hooks (installed by `bun install`) are fast feedback: Biome on staged files, typecheck of the packages you changed. ## Working agreements diff --git a/bun.lock b/bun.lock index bd077dcc75..9afeaaf62a 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,9 @@ "@ai-sdk/anthropic": "^4.0.89", "@ai-sdk/openai": "^4.0.103", "@biomejs/biome": "^2.6.3", - "@lingui/cli": "^6.10.1", + "@lingui/cli": "6.10.1", "@lingui/conf": "6.10.1", - "@lingui/format-po": "^6.10.1", + "@lingui/format-po": "6.10.1", "@lingui/message-utils": "6.10.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "@typescript/typescript6": "6.0.2", @@ -136,9 +136,8 @@ "name": "@pascal-app/core", "version": "1.0.6", "dependencies": { - "@lingui/core": "^6.10.1", - "@lingui/message-utils": "^6.10.1", - "@lingui/react": "^6.10.1", + "@lingui/core": "6.10.1", + "@lingui/message-utils": "6.10.1", "@pascal-app/lingo": "^0.5.0", "dedent": "^1.7.2", "idb-keyval": "^6.3.0", @@ -685,8 +684,6 @@ "@lingui/native-tools-win32-x64-msvc": ["@lingui/native-tools-win32-x64-msvc@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-5KvuSTFtDB8UGHS3Rxd+juYZgc51isQSx0hPukDObre6UxZGuUVCBmJ9YwoGVzXedViiNGmsQvrnCTBbVqI75g=="], - "@lingui/react": ["@lingui/react@6.10.1", "", { "dependencies": { "@lingui/babel-plugin-lingui-macro": "6.10.1", "@lingui/core": "6.10.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "babel-plugin-macros": "2 || 3", "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-r0e4T7WpkeKeVxT+qt56u7i8z1ykRIotpsCXaA6Vm2K0iVkE0SebcWpBaEFpBX8FyHEzXSA5nshSH6dm50pL1Q=="], - "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.3", "", {}, "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg=="], "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], diff --git a/i18n/README.md b/i18n/README.md index d19f78fcbb..0d317721fc 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -44,8 +44,9 @@ After adding or changing a runtime message: `i18n:check` needs no API key. It fails for extraction/compiled-catalog drift, missing or stale translations, unreviewed entries, malformed ICU, changed placeholders or tags, invalid target-locale plural branches, and glossary -violations. It only warns when a new literal bypasses Lingui on the converted -viewer/build-panel surfaces. +violations. It is read-only: use `i18n:extract` or `i18n:compile` when catalogs +need to be written. It only warns when a new literal bypasses Lingui on the +converted viewer/build-panel surfaces. `bun run i18n:compile` runs Lingui’s strict compiler directly when only the generated runtime catalogs need refreshing. diff --git a/lingui.config.ts b/lingui.config.ts index 9097895c79..8be0f48ace 100644 --- a/lingui.config.ts +++ b/lingui.config.ts @@ -19,7 +19,7 @@ const config: LinguiConfig = { fallbackLocales: false, format: formatter({ explicitIdAsDefault: true, - lineNumbers: true, + lineNumbers: false, origins: true, }), locales: ['en', 'zh-CN', 'fr', 'es', 'ja'], diff --git a/package.json b/package.json index 4e139d543a..8ade84c620 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "@ai-sdk/anthropic": "^4.0.89", "@ai-sdk/openai": "^4.0.103", "@biomejs/biome": "^2.6.3", - "@lingui/cli": "^6.10.1", + "@lingui/cli": "6.10.1", "@lingui/conf": "6.10.1", - "@lingui/format-po": "^6.10.1", + "@lingui/format-po": "6.10.1", "@lingui/message-utils": "6.10.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "@typescript/typescript6": "6.0.2", diff --git a/packages/core/package.json b/packages/core/package.json index c35cb46613..9b19655432 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -158,9 +158,8 @@ "three": "^0.186" }, "dependencies": { - "@lingui/core": "^6.10.1", - "@lingui/message-utils": "^6.10.1", - "@lingui/react": "^6.10.1", + "@lingui/core": "6.10.1", + "@lingui/message-utils": "6.10.1", "@pascal-app/lingo": "^0.5.0", "dedent": "^1.7.2", "idb-keyval": "^6.3.0", diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts index 2b0452eadd..f1e32a5131 100644 --- a/packages/core/src/i18n/use-translate.ts +++ b/packages/core/src/i18n/use-translate.ts @@ -1,6 +1,5 @@ 'use client' -import { I18nProvider as LinguiI18nProvider } from '@lingui/react' import { createContext, createElement, @@ -18,11 +17,7 @@ const PascalI18nContext = createContext(defaultI18n) * @experimental Unreleased API; signatures may change before the first i18n release. */ export function PascalI18nProvider({ children, i18n }: { children: ReactNode; i18n: PascalI18n }) { - return createElement( - PascalI18nContext.Provider, - { value: i18n }, - createElement(LinguiI18nProvider, { i18n: i18n.i18n }, children), - ) + return createElement(PascalI18nContext.Provider, { value: i18n }, children) } /** @experimental Unreleased API; signatures may change before the first i18n release. */ diff --git a/packages/editor/src/i18n/locales/en/messages.po b/packages/editor/src/i18n/locales/en/messages.po index bb377a46d4..96b505b693 100644 --- a/packages/editor/src/i18n/locales/en/messages.po +++ b/packages/editor/src/i18n/locales/en/messages.po @@ -7,356 +7,342 @@ msgstr "" "X-Generator: @lingui/cli\n" "Language: en\n" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basement" msgstr "Basement" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basementNumber" msgstr "Basement {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.floorNumber" msgstr "Floor {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.groundFloor" msgstr "Ground floor" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.advanced" msgstr "Advanced" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.hint" msgstr "walls · floor · ceiling" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.idle" msgstr "Pick a shape to start. Every room brings its own walls, floor and ceiling." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.caption" msgstr "{lead} then the first one again to close. For L-shapes and angled rooms." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.label" msgstr "Polygon" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.lead" msgstr "Click each corner," -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.title" msgstr "Polygon room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.caption" msgstr "{lead} on the grid. Walls, floor and ceiling are created together." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.label" msgstr "Rectangle" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.lead" msgstr "Click two corners" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.title" msgstr "Rectangle room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.title" msgstr "Rooms" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.caption" msgstr "{lead} Close a loop and it becomes a room; leave it open for partitions." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.label" msgstr "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.lead" msgstr "Draw one wall at a time." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.title" msgstr "Walls" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitLeft" msgstr "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitRight" msgstr "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.topView" msgstr "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.off" msgstr "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.on" msgstr "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.camera" msgstr "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colored" msgstr "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colors" msgstr "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.off" msgstr "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.offDetail" msgstr "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.soft" msgstr "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.softDetail" msgstr "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strong" msgstr "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strongDetail" msgstr "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.title" msgstr "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.monochrome" msgstr "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.orthographic" msgstr "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.perspective" msgstr "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.rendered" msgstr "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.renderedDetail" msgstr "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solid" msgstr "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solidDetail" msgstr "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.title" msgstr "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.shadows" msgstr "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.title" msgstr "Display settings" -#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx msgid "viewer.floorplan.alignNorth" msgstr "Align view to north" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.empty" msgstr "No floor plans are available for this scene." -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.fit" msgstr "Fit floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floor" msgstr "Floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floorPlan" msgstr "Floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.viewLabel" msgstr "{floor} 2D view" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomIn" msgstr "Zoom in" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomOut" msgstr "Zoom out" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.back" msgstr "Back" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.levels" msgstr "Levels" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.rooms" msgstr "Rooms" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.selection" msgstr "Selection" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.units" msgstr "Units" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.untitled" msgstr "Untitled" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.zones" msgstr "Zones" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.exploded" msgstr "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.label" msgstr "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.manual" msgstr "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.solo" msgstr "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.stacked" msgstr "Stacked" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.layout" msgstr "Viewer layout" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.split" msgstr "Split" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.blueprint" msgstr "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.mediterranean" msgstr "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.night" msgstr "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.overcast" msgstr "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.paper" msgstr "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.studio" msgstr "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.sunset" msgstr "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.title" msgstr "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.twilight" msgstr "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.verdant" msgstr "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.guides" msgstr "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.scans" msgstr "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.title" msgstr "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walkthrough.label" msgstr "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.cutaway" msgstr "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.fullHeight" msgstr "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.label" msgstr "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.low" msgstr "Low" diff --git a/packages/editor/src/i18n/locales/es/messages.po b/packages/editor/src/i18n/locales/es/messages.po index 0826c19ae2..9d86e70e75 100644 --- a/packages/editor/src/i18n/locales/es/messages.po +++ b/packages/editor/src/i18n/locales/es/messages.po @@ -16,608 +16,594 @@ msgstr "" # Source-Hash: 8caa959c0ae800c3 # Review: human # English: "Basement" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basement" msgstr "Sótano" # Source-Hash: 14927085ee91ba05 # Review: human # English: "Basement {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basementNumber" msgstr "Sótano {number}" # Source-Hash: e59074a7b9b3f56e # Review: human # English: "Floor {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.floorNumber" msgstr "Planta {number}" # Source-Hash: cde3efc9b7ddfa41 # Review: human # English: "Ground floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.groundFloor" msgstr "Planta baja" # Source-Hash: f571cb2d06ae01ae # Review: human # English: "Advanced" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.advanced" msgstr "Avanzado" # Source-Hash: fb40866c2d6db5fb # Review: human # English: "walls · floor · ceiling" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.hint" msgstr "muros · suelo · techo" # Source-Hash: 271039bd919dc4c1 # Review: human # English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.idle" msgstr "Elige una forma para empezar. Cada estancia incluye sus muros, suelo y techo." # Source-Hash: ae2800b6c9e2b6ed # Review: human # English: "{lead} then the first one again to close. For L-shapes and angled rooms." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.caption" msgstr "{lead} y vuelve a pulsar la primera para cerrar. Para estancias en L o con ángulos." # Source-Hash: 4e186fc057733162 # Review: human # English: "Polygon" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.label" msgstr "Polígono" # Source-Hash: 345e3aca3879b5c5 # Review: human # English: "Click each corner," -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.lead" msgstr "Haz clic en cada esquina" # Source-Hash: 8ec6152e139b0e3f # Review: human # English: "Polygon room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.title" msgstr "Estancia poligonal" # Source-Hash: 9b46295eedcbedae # Review: human # English: "{lead} on the grid. Walls, floor and ceiling are created together." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.caption" msgstr "{lead} en la cuadrícula. Los muros, el suelo y el techo se crean a la vez." # Source-Hash: 2c2c20272b0370cc # Review: human # English: "Rectangle" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.label" msgstr "Rectángulo" # Source-Hash: 060faa48e299d7c9 # Review: human # English: "Click two corners" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.lead" msgstr "Haz clic en dos esquinas" # Source-Hash: 83a45603ac7f41e8 # Review: human # English: "Rectangle room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.title" msgstr "Estancia rectangular" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.title" msgstr "Estancias" # Source-Hash: 8a21a03c4acf396b # Review: human # English: "{lead} Close a loop and it becomes a room; leave it open for partitions." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.caption" msgstr "{lead} Cierra el contorno para crear una estancia o déjalo abierto para tabiques." # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.label" msgstr "Muros" # Source-Hash: 9843edd68426fa2d # Review: human # English: "Draw one wall at a time." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.lead" msgstr "Dibuja un muro cada vez." # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.title" msgstr "Muros" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitLeft" msgstr "Orbitar a la izquierda" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitRight" msgstr "Orbitar a la derecha" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.topView" msgstr "Vista superior" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.off" msgstr "Desactivado" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.on" msgstr "Activado" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.camera" msgstr "Cámara" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colored" msgstr "A color" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colors" msgstr "Colores" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.off" msgstr "Desactivadas" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.offDetail" msgstr "Sin líneas de arista" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.soft" msgstr "Suaves" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.softDetail" msgstr "Contorno tenue de los pliegues principales" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strong" msgstr "Marcadas" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strongDetail" msgstr "Líneas de arista nítidas y opacas" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.title" msgstr "Aristas" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.monochrome" msgstr "Monocromo" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.orthographic" msgstr "Ortográfica" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.perspective" msgstr "Perspectiva" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.rendered" msgstr "Realista" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.renderedDetail" msgstr "Oclusión ambiental completa" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solid" msgstr "Sólido" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solidDetail" msgstr "Plano y rápido, sin oclusión ambiental" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.title" msgstr "Renderizado" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.shadows" msgstr "Sombras" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.title" msgstr "Ajustes de visualización" # Source-Hash: 2bdf83801d4a40af # Review: human # English: "Align view to north" -#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx msgid "viewer.floorplan.alignNorth" msgstr "Orientar la vista al norte" # Source-Hash: 286a757d8b52bb02 # Review: human # English: "No floor plans are available for this scene." -#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.empty" msgstr "No hay planos de planta disponibles para esta escena." # Source-Hash: bf63c679ab94dbcd # Review: human # English: "Fit floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.fit" msgstr "Ajustar plano" # Source-Hash: d7148fa065bc709b # Review: human # English: "Floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floor" msgstr "Planta" # Source-Hash: fee6203bb9046929 # Review: human # English: "Floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floorPlan" msgstr "Plano de planta" # Source-Hash: 28b255b7bf5f1084 # Review: human # English: "{floor} 2D view" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.viewLabel" msgstr "Vista 2D de {floor}" # Source-Hash: d17804412f8dd081 # Review: human # English: "Zoom in" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomIn" msgstr "Acercar" # Source-Hash: 23b012a283b2c14a # Review: human # English: "Zoom out" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomOut" msgstr "Alejar" # Source-Hash: 7ceade8f25b58fe9 # Review: human # English: "Back" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.back" msgstr "Volver" # Source-Hash: cc3d601d59e2e26c # Review: human # English: "Levels" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.levels" msgstr "Plantas" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.rooms" msgstr "Estancias" # Source-Hash: 170b2049ef0f9131 # Review: human # English: "Selection" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.selection" msgstr "Selección" # Source-Hash: b404dded35241274 # Review: human # English: "Units" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.units" msgstr "Unidades" # Source-Hash: 6547a4c0c368042a # Review: human # English: "Untitled" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.untitled" msgstr "Sin título" # Source-Hash: 665db49a26f81425 # Review: human # English: "Zones" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.zones" msgstr "Zonas" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.exploded" msgstr "Separadas" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.label" msgstr "Plantas: {mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.manual" msgstr "Manual" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.solo" msgstr "Aislada" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.stacked" msgstr "Apiladas" # Source-Hash: 9dd21ca47cffa3b8 # Review: human # English: "Viewer layout" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.layout" msgstr "Disposición de vistas" # Source-Hash: e4cd64b1680ac0d2 # Review: human # English: "Split" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.split" msgstr "Dividida" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.blueprint" msgstr "Plano técnico" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.mediterranean" msgstr "Mediterráneo" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.night" msgstr "Noche" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.overcast" msgstr "Nublado" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.paper" msgstr "Papel" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.studio" msgstr "Estudio" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.sunset" msgstr "Atardecer" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.title" msgstr "Ambiente" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.twilight" msgstr "Crepúsculo" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.verdant" msgstr "Verde" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.guides" msgstr "Guías" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.scans" msgstr "Escaneos 3D" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.title" msgstr "Visibilidad" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walkthrough.label" msgstr "Recorrido: {state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.cutaway" msgstr "Vista seccionada" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.fullHeight" msgstr "Altura completa" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.label" msgstr "Muros: {mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.low" msgstr "Bajos" diff --git a/packages/editor/src/i18n/locales/fr/messages.po b/packages/editor/src/i18n/locales/fr/messages.po index d9ee69fa63..89431ce1c1 100644 --- a/packages/editor/src/i18n/locales/fr/messages.po +++ b/packages/editor/src/i18n/locales/fr/messages.po @@ -16,608 +16,594 @@ msgstr "" # Source-Hash: 8caa959c0ae800c3 # Review: human # English: "Basement" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basement" msgstr "Sous-sol" # Source-Hash: 14927085ee91ba05 # Review: human # English: "Basement {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basementNumber" msgstr "Sous-sol {number}" # Source-Hash: e59074a7b9b3f56e # Review: human # English: "Floor {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.floorNumber" msgstr "Étage {number}" # Source-Hash: cde3efc9b7ddfa41 # Review: human # English: "Ground floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.groundFloor" msgstr "Rez-de-chaussée" # Source-Hash: f571cb2d06ae01ae # Review: human # English: "Advanced" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.advanced" msgstr "Avancé" # Source-Hash: fb40866c2d6db5fb # Review: human # English: "walls · floor · ceiling" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.hint" msgstr "murs · sol · plafond" # Source-Hash: 271039bd919dc4c1 # Review: human # English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.idle" msgstr "Choisissez une forme pour commencer. Chaque pièce crée ses murs, son sol et son plafond." # Source-Hash: ae2800b6c9e2b6ed # Review: human # English: "{lead} then the first one again to close. For L-shapes and angled rooms." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.caption" msgstr "{lead} puis de nouveau sur le premier pour fermer. Pour les pièces en L et les angles obliques." # Source-Hash: 4e186fc057733162 # Review: human # English: "Polygon" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.label" msgstr "Polygone" # Source-Hash: 345e3aca3879b5c5 # Review: human # English: "Click each corner," -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.lead" msgstr "Cliquez sur chaque angle," # Source-Hash: 8ec6152e139b0e3f # Review: human # English: "Polygon room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.title" msgstr "Pièce polygonale" # Source-Hash: 9b46295eedcbedae # Review: human # English: "{lead} on the grid. Walls, floor and ceiling are created together." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.caption" msgstr "{lead} sur la grille. Les murs, le sol et le plafond sont créés ensemble." # Source-Hash: 2c2c20272b0370cc # Review: human # English: "Rectangle" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.label" msgstr "Rectangle" # Source-Hash: 060faa48e299d7c9 # Review: human # English: "Click two corners" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.lead" msgstr "Cliquez sur deux angles" # Source-Hash: 83a45603ac7f41e8 # Review: human # English: "Rectangle room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.title" msgstr "Pièce rectangulaire" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.title" msgstr "Pièces" # Source-Hash: 8a21a03c4acf396b # Review: human # English: "{lead} Close a loop and it becomes a room; leave it open for partitions." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.caption" msgstr "{lead} Fermez la boucle pour créer une pièce, ou laissez-la ouverte pour une cloison." # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.label" msgstr "Murs" # Source-Hash: 9843edd68426fa2d # Review: human # English: "Draw one wall at a time." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.lead" msgstr "Tracez un mur à la fois." # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.title" msgstr "Murs" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitLeft" msgstr "Pivoter à gauche" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitRight" msgstr "Pivoter à droite" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.topView" msgstr "Vue de dessus" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.off" msgstr "Désactivé" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.on" msgstr "Activé" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.camera" msgstr "Caméra" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colored" msgstr "En couleur" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colors" msgstr "Couleurs" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.off" msgstr "Désactivées" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.offDetail" msgstr "Aucune ligne d’arête" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.soft" msgstr "Douces" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.softDetail" msgstr "Contour léger des plis principaux" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strong" msgstr "Marquées" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strongDetail" msgstr "Lignes d’arête nettes et opaques" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.title" msgstr "Arêtes" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.monochrome" msgstr "Monochrome" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.orthographic" msgstr "Orthographique" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.perspective" msgstr "Perspective" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.rendered" msgstr "Réaliste" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.renderedDetail" msgstr "Occlusion ambiante complète" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solid" msgstr "Solide" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solidDetail" msgstr "Rapide et uniforme, sans occlusion ambiante" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.title" msgstr "Rendu" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.shadows" msgstr "Ombres" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.title" msgstr "Paramètres d’affichage" # Source-Hash: 2bdf83801d4a40af # Review: human # English: "Align view to north" -#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx msgid "viewer.floorplan.alignNorth" msgstr "Orienter la vue vers le nord" # Source-Hash: 286a757d8b52bb02 # Review: human # English: "No floor plans are available for this scene." -#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.empty" msgstr "Aucun plan d’étage n’est disponible pour cette scène." # Source-Hash: bf63c679ab94dbcd # Review: human # English: "Fit floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.fit" msgstr "Ajuster le plan" # Source-Hash: d7148fa065bc709b # Review: human # English: "Floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floor" msgstr "Niveau" # Source-Hash: fee6203bb9046929 # Review: human # English: "Floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floorPlan" msgstr "Plan d’étage" # Source-Hash: 28b255b7bf5f1084 # Review: human # English: "{floor} 2D view" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.viewLabel" msgstr "Vue 2D — {floor}" # Source-Hash: d17804412f8dd081 # Review: human # English: "Zoom in" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomIn" msgstr "Zoom avant" # Source-Hash: 23b012a283b2c14a # Review: human # English: "Zoom out" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomOut" msgstr "Zoom arrière" # Source-Hash: 7ceade8f25b58fe9 # Review: human # English: "Back" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.back" msgstr "Retour" # Source-Hash: cc3d601d59e2e26c # Review: human # English: "Levels" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.levels" msgstr "Niveaux" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.rooms" msgstr "Pièces" # Source-Hash: 170b2049ef0f9131 # Review: human # English: "Selection" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.selection" msgstr "Sélection" # Source-Hash: b404dded35241274 # Review: human # English: "Units" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.units" msgstr "Unités" # Source-Hash: 6547a4c0c368042a # Review: human # English: "Untitled" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.untitled" msgstr "Sans titre" # Source-Hash: 665db49a26f81425 # Review: human # English: "Zones" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.zones" msgstr "Zones" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.exploded" msgstr "Éclatés" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.label" msgstr "Niveaux : {mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.manual" msgstr "Manuel" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.solo" msgstr "Isolé" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.stacked" msgstr "Superposés" # Source-Hash: 9dd21ca47cffa3b8 # Review: human # English: "Viewer layout" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.layout" msgstr "Disposition des vues" # Source-Hash: e4cd64b1680ac0d2 # Review: human # English: "Split" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.split" msgstr "Partagé" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.blueprint" msgstr "Plan" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.mediterranean" msgstr "Méditerranéen" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.night" msgstr "Nuit" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.overcast" msgstr "Ciel couvert" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.paper" msgstr "Papier" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.studio" msgstr "Studio" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.sunset" msgstr "Coucher de soleil" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.title" msgstr "Ambiance" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.twilight" msgstr "Crépuscule" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.verdant" msgstr "Verdoyant" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.guides" msgstr "Repères" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.scans" msgstr "Relevés 3D" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.title" msgstr "Visibilité" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walkthrough.label" msgstr "Visite : {state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.cutaway" msgstr "Vue en coupe" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.fullHeight" msgstr "Pleine hauteur" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.label" msgstr "Murs : {mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.low" msgstr "Bas" diff --git a/packages/editor/src/i18n/locales/ja/messages.po b/packages/editor/src/i18n/locales/ja/messages.po index 62b46d5f82..7fe420abfb 100644 --- a/packages/editor/src/i18n/locales/ja/messages.po +++ b/packages/editor/src/i18n/locales/ja/messages.po @@ -16,608 +16,594 @@ msgstr "" # Source-Hash: 8caa959c0ae800c3 # Review: human # English: "Basement" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basement" msgstr "地下1階" # Source-Hash: 14927085ee91ba05 # Review: human # English: "Basement {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basementNumber" msgstr "地下{number}階" # Source-Hash: e59074a7b9b3f56e # Review: human # English: "Floor {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.floorNumber" msgstr "{number}階" # Source-Hash: cde3efc9b7ddfa41 # Review: human # English: "Ground floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.groundFloor" msgstr "1階" # Source-Hash: f571cb2d06ae01ae # Review: human # English: "Advanced" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.advanced" msgstr "詳細" # Source-Hash: fb40866c2d6db5fb # Review: human # English: "walls · floor · ceiling" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.hint" msgstr "壁 · 床 · 天井" # Source-Hash: 271039bd919dc4c1 # Review: human # English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.idle" msgstr "形状を選んで開始します。部屋ごとに壁・床・天井が作成されます。" # Source-Hash: ae2800b6c9e2b6ed # Review: human # English: "{lead} then the first one again to close. For L-shapes and angled rooms." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.caption" msgstr "{lead}最後に始点をクリックして閉じます。L字型や斜めの部屋に使用します。" # Source-Hash: 4e186fc057733162 # Review: human # English: "Polygon" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.label" msgstr "多角形" # Source-Hash: 345e3aca3879b5c5 # Review: human # English: "Click each corner," -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.lead" msgstr "各頂点をクリックし、" # Source-Hash: 8ec6152e139b0e3f # Review: human # English: "Polygon room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.title" msgstr "多角形の部屋" # Source-Hash: 9b46295eedcbedae # Review: human # English: "{lead} on the grid. Walls, floor and ceiling are created together." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.caption" msgstr "グリッド上で{lead}します。壁・床・天井をまとめて作成します。" # Source-Hash: 2c2c20272b0370cc # Review: human # English: "Rectangle" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.label" msgstr "長方形" # Source-Hash: 060faa48e299d7c9 # Review: human # English: "Click two corners" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.lead" msgstr "対角の2点をクリック" # Source-Hash: 83a45603ac7f41e8 # Review: human # English: "Rectangle room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.title" msgstr "長方形の部屋" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.title" msgstr "部屋" # Source-Hash: 8a21a03c4acf396b # Review: human # English: "{lead} Close a loop and it becomes a room; leave it open for partitions." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.caption" msgstr "{lead}輪郭を閉じると部屋になり、開いたままにすると間仕切りになります。" # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.label" msgstr "壁" # Source-Hash: 9843edd68426fa2d # Review: human # English: "Draw one wall at a time." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.lead" msgstr "壁を1面ずつ描画します。" # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.title" msgstr "壁" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitLeft" msgstr "左に回転" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitRight" msgstr "右に回転" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.topView" msgstr "上面図" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.off" msgstr "オフ" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.on" msgstr "オン" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.camera" msgstr "カメラ" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colored" msgstr "カラー表示" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colors" msgstr "カラー" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.off" msgstr "オフ" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.offDetail" msgstr "エッジ線を表示しない" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.soft" msgstr "ソフト" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.softDetail" msgstr "主要な稜線を薄く表示" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strong" msgstr "強調" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strongDetail" msgstr "エッジ線をくっきり表示" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.title" msgstr "エッジ" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.monochrome" msgstr "モノクロ" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.orthographic" msgstr "平行投影" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.perspective" msgstr "透視投影" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.rendered" msgstr "レンダリング" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.renderedDetail" msgstr "アンビエントオクルージョンを完全表示" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solid" msgstr "ソリッド" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solidDetail" msgstr "軽量表示(アンビエントオクルージョンなし)" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.title" msgstr "レンダリング" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.shadows" msgstr "影" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.title" msgstr "表示設定" # Source-Hash: 2bdf83801d4a40af # Review: human # English: "Align view to north" -#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx msgid "viewer.floorplan.alignNorth" msgstr "北を上に表示" # Source-Hash: 286a757d8b52bb02 # Review: human # English: "No floor plans are available for this scene." -#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.empty" msgstr "このシーンには表示できる平面図がありません。" # Source-Hash: bf63c679ab94dbcd # Review: human # English: "Fit floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.fit" msgstr "全体を表示" # Source-Hash: d7148fa065bc709b # Review: human # English: "Floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floor" msgstr "階" # Source-Hash: fee6203bb9046929 # Review: human # English: "Floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floorPlan" msgstr "平面図" # Source-Hash: 28b255b7bf5f1084 # Review: human # English: "{floor} 2D view" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.viewLabel" msgstr "{floor}の2Dビュー" # Source-Hash: d17804412f8dd081 # Review: human # English: "Zoom in" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomIn" msgstr "拡大" # Source-Hash: 23b012a283b2c14a # Review: human # English: "Zoom out" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomOut" msgstr "縮小" # Source-Hash: 7ceade8f25b58fe9 # Review: human # English: "Back" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.back" msgstr "戻る" # Source-Hash: cc3d601d59e2e26c # Review: human # English: "Levels" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.levels" msgstr "階層" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.rooms" msgstr "部屋" # Source-Hash: 170b2049ef0f9131 # Review: human # English: "Selection" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.selection" msgstr "選択" # Source-Hash: b404dded35241274 # Review: human # English: "Units" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.units" msgstr "住戸" # Source-Hash: 6547a4c0c368042a # Review: human # English: "Untitled" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.untitled" msgstr "名称未設定" # Source-Hash: 665db49a26f81425 # Review: human # English: "Zones" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.zones" msgstr "ゾーン" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.exploded" msgstr "分解" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.label" msgstr "階層:{mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.manual" msgstr "手動" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.solo" msgstr "単独" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.stacked" msgstr "積層" # Source-Hash: 9dd21ca47cffa3b8 # Review: human # English: "Viewer layout" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.layout" msgstr "ビュー配置" # Source-Hash: e4cd64b1680ac0d2 # Review: human # English: "Split" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.split" msgstr "分割" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.blueprint" msgstr "青図" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.mediterranean" msgstr "地中海" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.night" msgstr "夜景" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.overcast" msgstr "曇天" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.paper" msgstr "ペーパー" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.studio" msgstr "スタジオ" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.sunset" msgstr "夕景" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.title" msgstr "シーンテーマ" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.twilight" msgstr "薄暮" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.verdant" msgstr "緑景" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.guides" msgstr "ガイド" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.scans" msgstr "3Dスキャン" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.title" msgstr "表示" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walkthrough.label" msgstr "ウォークスルー:{state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.cutaway" msgstr "カット表示" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.fullHeight" msgstr "全高" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.label" msgstr "壁:{mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.low" msgstr "低く表示" diff --git a/packages/editor/src/i18n/locales/zh-CN/messages.po b/packages/editor/src/i18n/locales/zh-CN/messages.po index d8aa8ebbb2..9b059286fe 100644 --- a/packages/editor/src/i18n/locales/zh-CN/messages.po +++ b/packages/editor/src/i18n/locales/zh-CN/messages.po @@ -16,608 +16,594 @@ msgstr "" # Source-Hash: 8caa959c0ae800c3 # Review: human # English: "Basement" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:162 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:61 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basement" msgstr "地下层" # Source-Hash: 14927085ee91ba05 # Review: human # English: "Basement {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:164 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:63 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.basementNumber" msgstr "地下 {number} 层" # Source-Hash: e59074a7b9b3f56e # Review: human # English: "Floor {number}" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:171 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:70 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.floorNumber" msgstr "{number} 层" # Source-Hash: cde3efc9b7ddfa41 # Review: human # English: "Ground floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:160 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:59 +#: packages/editor/src/i18n/level-name.ts msgid "core.level.groundFloor" msgstr "首层" # Source-Hash: f571cb2d06ae01ae # Review: human # English: "Advanced" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:99 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.advanced" msgstr "高级" # Source-Hash: fb40866c2d6db5fb # Review: human # English: "walls · floor · ceiling" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:236 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.hint" msgstr "墙体 · 楼板 · 天花板" # Source-Hash: 271039bd919dc4c1 # Review: human # English: "Pick a shape to start. Every room brings its own walls, floor and ceiling." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:265 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.idle" msgstr "选择一种形状开始绘制。每个房间都会同时创建墙体、楼板和天花板。" # Source-Hash: ae2800b6c9e2b6ed # Review: human # English: "{lead} then the first one again to close. For L-shapes and angled rooms." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:324 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.caption" msgstr "{lead}然后再次点击起点闭合。适用于 L 形和斜角房间。" # Source-Hash: 4e186fc057733162 # Review: human # English: "Polygon" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:315 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.label" msgstr "多边形" # Source-Hash: 345e3aca3879b5c5 # Review: human # English: "Click each corner," -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:320 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.lead" msgstr "依次点击各个角点," # Source-Hash: 8ec6152e139b0e3f # Review: human # English: "Polygon room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:316 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.polygon.title" msgstr "多边形房间" # Source-Hash: 9b46295eedcbedae # Review: human # English: "{lead} on the grid. Walls, floor and ceiling are created together." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:292 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.caption" msgstr "在网格上{lead}。墙体、楼板和天花板将一并创建。" # Source-Hash: 2c2c20272b0370cc # Review: human # English: "Rectangle" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:280 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.label" msgstr "矩形" # Source-Hash: 060faa48e299d7c9 # Review: human # English: "Click two corners" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:288 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.lead" msgstr "点击两个对角点" # Source-Hash: 83a45603ac7f41e8 # Review: human # English: "Rectangle room" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:284 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.rectangle.title" msgstr "矩形房间" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:241 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.title" msgstr "房间" # Source-Hash: 8a21a03c4acf396b # Review: human # English: "{lead} Close a loop and it becomes a room; leave it open for partitions." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:307 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.caption" msgstr "{lead}闭合轮廓后会形成房间;保持开口可绘制隔墙。" # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:301 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.label" msgstr "墙体" # Source-Hash: 9843edd68426fa2d # Review: human # English: "Draw one wall at a time." -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:303 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.lead" msgstr "逐面绘制墙体。" # Source-Hash: aac9a732d2159ccc # Review: human # English: "Walls" -#: packages/editor/src/components/ui/build-panel/build-panel.tsx:302 +#: packages/editor/src/components/ui/build-panel/build-panel.tsx msgid "editor.buildPanel.rooms.walls.title" msgstr "墙体" # Source-Hash: 8bf25afa3cb8baea # Review: human # English: "Orbit left" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:508 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:516 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitLeft" msgstr "向左旋转" # Source-Hash: b9cc64c1d1bb0d46 # Review: human # English: "Orbit right" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:524 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:532 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.orbitRight" msgstr "向右旋转" # Source-Hash: 1ab1eb3a14f4b0e0 # Review: human # English: "Top view" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:540 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:548 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.camera.topView" msgstr "俯视图" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:230 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:488 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.off" msgstr "关闭" # Source-Hash: 9cc01faad79c28da # Review: human # English: "On" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:229 -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:487 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.common.on" msgstr "开启" # Source-Hash: 14622745dbf93f4c # Review: human # English: "Camera" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:243 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.camera" msgstr "相机" # Source-Hash: d3d478fa6c6d7ddc # Review: human # English: "Colored" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:257 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colored" msgstr "彩色" # Source-Hash: 82b74c4f5ef43336 # Review: human # English: "Colors" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:254 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.colors" msgstr "颜色" # Source-Hash: 50accd740fa2e736 # Review: human # English: "Off" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:184 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.off" msgstr "关闭" # Source-Hash: 4ff58913a4492199 # Review: human # English: "No edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:185 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.offDetail" msgstr "不显示边线" # Source-Hash: d266d83ec490df4e # Review: human # English: "Soft" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:189 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.soft" msgstr "柔和" # Source-Hash: 0c583f1d4201293b # Review: human # English: "Faint outline of major creases" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:190 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.softDetail" msgstr "淡化显示主要折线" # Source-Hash: f82927a01d4c5e45 # Review: human # English: "Strong" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:197 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strong" msgstr "清晰" # Source-Hash: 2416a48df28d6205 # Review: human # English: "Crisp, opaque edge lines" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:198 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.strongDetail" msgstr "清晰、不透明的边线" # Source-Hash: 027bbc8b7287c06a # Review: human # English: "Edges" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:330 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.edges.title" msgstr "边线" # Source-Hash: ecde2f7c26914be3 # Review: human # English: "Monochrome" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:258 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.monochrome" msgstr "单色" # Source-Hash: ed59dd8ce7797a66 # Review: human # English: "Orthographic" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:247 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.orthographic" msgstr "正交" # Source-Hash: 316e25993ff72b88 # Review: human # English: "Perspective" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:246 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.perspective" msgstr "透视" # Source-Hash: f363702a10aedaeb # Review: human # English: "Rendered" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:173 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.rendered" msgstr "渲染效果" # Source-Hash: 8bb9e54fc786ca77 # Review: human # English: "Full ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:174 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.renderedDetail" msgstr "完整环境光遮蔽" # Source-Hash: ef3e765e692faf4c # Review: human # English: "Solid" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:164 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solid" msgstr "实体" # Source-Hash: 8827df1cdeb02ed9 # Review: human # English: "Flat and fast — no ambient occlusion" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:165 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.solidDetail" msgstr "平整快速,不启用环境光遮蔽" # Source-Hash: 5e58af99d6d4fa6a # Review: human # English: "Render" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:267 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.render.title" msgstr "渲染" # Source-Hash: 4c6bcb30b40047fc # Review: human # English: "Shadows" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:226 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.shadows" msgstr "阴影" # Source-Hash: d0db46f206a88e2e # Review: human # English: "Display settings" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:212 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.display.title" msgstr "显示设置" # Source-Hash: 2bdf83801d4a40af # Review: human # English: "Align view to north" -#: packages/editor/src/components/viewer/floorplan-compass-button.tsx:23 +#: packages/editor/src/components/viewer/floorplan-compass-button.tsx msgid "viewer.floorplan.alignNorth" msgstr "视图对齐北向" # Source-Hash: 286a757d8b52bb02 # Review: human # English: "No floor plans are available for this scene." -#: packages/editor/src/components/viewer/floorplan-preview.tsx:901 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.empty" msgstr "此场景没有可用的平面图。" # Source-Hash: bf63c679ab94dbcd # Review: human # English: "Fit floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1065 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1068 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.fit" msgstr "适合窗口" # Source-Hash: d7148fa065bc709b # Review: human # English: "Floor" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1012 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1014 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floor" msgstr "楼层" # Source-Hash: fee6203bb9046929 # Review: human # English: "Floor plan" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:923 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.floorPlan" msgstr "平面图" # Source-Hash: 28b255b7bf5f1084 # Review: human # English: "{floor} 2D view" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:917 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.viewLabel" msgstr "{floor}二维视图" # Source-Hash: d17804412f8dd081 # Review: human # English: "Zoom in" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1074 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1077 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomIn" msgstr "放大" # Source-Hash: 23b012a283b2c14a # Review: human # English: "Zoom out" -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1056 -#: packages/editor/src/components/viewer/floorplan-preview.tsx:1059 +#: packages/editor/src/components/viewer/floorplan-preview.tsx msgid "viewer.floorplan.zoomOut" msgstr "缩小" # Source-Hash: 7ceade8f25b58fe9 # Review: human # English: "Back" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:152 -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:161 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.back" msgstr "返回" # Source-Hash: cc3d601d59e2e26c # Review: human # English: "Levels" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:225 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.levels" msgstr "楼层" # Source-Hash: 3e7408ed44676176 # Review: human # English: "Rooms" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:50 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.rooms" msgstr "房间" # Source-Hash: 170b2049ef0f9131 # Review: human # English: "Selection" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:190 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.selection" msgstr "当前选择" # Source-Hash: b404dded35241274 # Review: human # English: "Units" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:49 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.units" msgstr "单元" # Source-Hash: 6547a4c0c368042a # Review: human # English: "Untitled" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:171 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.untitled" msgstr "未命名" # Source-Hash: 665db49a26f81425 # Review: human # English: "Zones" -#: packages/editor/src/components/viewer/viewer-scene-header.tsx:51 +#: packages/editor/src/components/viewer/viewer-scene-header.tsx msgid "viewer.header.zones" msgstr "分区" # Source-Hash: be12f8788f1f0a05 # Review: human # English: "Exploded" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:61 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.exploded" msgstr "分解" # Source-Hash: 4bf4a6861d7d633e # Review: human # English: "Levels: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:415 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.label" msgstr "楼层:{mode}" # Source-Hash: 387082cd5f118bed # Review: human # English: "Manual" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:63 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.manual" msgstr "手动" # Source-Hash: e9f59b513f2f45ac # Review: human # English: "Solo" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:62 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.solo" msgstr "单层" # Source-Hash: c203c84eee18c0f6 # Review: human # English: "Stacked" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:60 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.levels.stacked" msgstr "叠层" # Source-Hash: 9dd21ca47cffa3b8 # Review: human # English: "Viewer layout" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:31 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.layout" msgstr "视图布局" # Source-Hash: e4cd64b1680ac0d2 # Review: human # English: "Split" -#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx:59 +#: packages/editor/src/components/viewer/viewer-stage-switcher.tsx msgid "viewer.stage.split" msgstr "分屏" # Source-Hash: f14b89d713d2528a # Review: human # English: "Blueprint" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:77 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.blueprint" msgstr "蓝图" # Source-Hash: 142bcb2841f423c5 # Review: human # English: "Mediterranean" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:79 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.mediterranean" msgstr "地中海" # Source-Hash: b399942ec5d7e1ba # Review: human # English: "Night" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:81 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.night" msgstr "夜景" # Source-Hash: f512d1b947b8ae5a # Review: human # English: "Overcast" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:76 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.overcast" msgstr "阴天" # Source-Hash: 583542b3bc837192 # Review: human # English: "Paper" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:74 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.paper" msgstr "纸面" # Source-Hash: 7abc2dc6e39b1747 # Review: human # English: "Studio" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:73 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.studio" msgstr "影棚" # Source-Hash: efe94d3ec8d8c44e # Review: human # English: "Sunset" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:75 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.sunset" msgstr "日落" # Source-Hash: 82720864e5034398 # Review: human # English: "Scene theme" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:295 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.title" msgstr "场景主题" # Source-Hash: 4c10ea0aa0e2ea41 # Review: human # English: "Twilight" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:80 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.twilight" msgstr "暮色" # Source-Hash: f9592fb0672740c0 # Review: human # English: "Verdant" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:82 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.theme.verdant" msgstr "绿意" # Source-Hash: 892d5dd34372028b # Review: human # English: "Guides" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:138 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.guides" msgstr "参考图" # Source-Hash: 3260bfa69cca50e5 # Review: human # English: "Scans" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:125 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.scans" msgstr "扫描模型" # Source-Hash: 2cefbb554eadbb6e # Review: human # English: "Visibility" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:110 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.visibility.title" msgstr "可见性" # Source-Hash: 31afd9dee47ba943 # Review: human # English: "Walkthrough: {state}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:482 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walkthrough.label" msgstr "漫游:{state}" # Source-Hash: 4da8b3678ed77b45 # Review: human # English: "Cutaway" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:69 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.cutaway" msgstr "剖切墙" # Source-Hash: 131bac2467f4fdf6 # Review: human # English: "Full height" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:67 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.fullHeight" msgstr "全高墙" # Source-Hash: 32f3d058cf2526d2 # Review: human # English: "Walls: {mode}" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:444 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.label" msgstr "墙体:{mode}" # Source-Hash: 803532407fb5962b # Review: human # English: "Low" -#: packages/editor/src/components/viewer/viewer-controls-bar.tsx:68 +#: packages/editor/src/components/viewer/viewer-controls-bar.tsx msgid "viewer.walls.low" msgstr "矮墙" diff --git a/scripts/i18n.test.ts b/scripts/i18n.test.ts index 9b8de68277..ebf42e4d3c 100644 --- a/scripts/i18n.test.ts +++ b/scripts/i18n.test.ts @@ -1,5 +1,9 @@ import { expect, test } from 'bun:test' +import { readFileSync } from 'node:fs' +import type { CatalogType } from '@lingui/conf' +import linguiConfig from '../lingui.config' import { + checkCatalogs, hashSource, refreshTranslations, type SourceMessage, @@ -8,6 +12,42 @@ import { validateTranslation, } from './i18n' +test('PO output is stable when an unrelated source line is added', async () => { + const format = linguiConfig.format + if (!format) throw new Error('Lingui PO formatter is not configured') + const serialize = (line: number) => + format.serialize( + { + 'viewer.example': { + message: 'Example', + translation: 'Exemple', + origin: [['packages/editor/src/example.tsx', line]], + }, + } satisfies CatalogType, + { + existing: undefined, + filename: 'messages.po', + locale: 'fr', + sourceLocale: 'en', + }, + ) + + expect(await serialize(10)).toBe(await serialize(11)) +}) + +test('catalog checking does not rewrite PO or compiled catalogs', async () => { + const paths = [ + ...new Bun.Glob('packages/editor/src/i18n/locales/*/messages.po').scanSync(), + ...new Bun.Glob('packages/editor/src/i18n/locales/*/messages.ts').scanSync(), + ] + const before = new Map(paths.map((path) => [path, readFileSync(path, 'utf8')])) + + expect(await checkCatalogs()).toEqual([]) + expect(paths.map((path) => readFileSync(path, 'utf8'))).toEqual( + paths.map((path) => before.get(path)), + ) +}) + test('ICU plural validation follows the target locale categories', () => { const english = '{count, plural, one {# item} other {# items}}' diff --git a/scripts/i18n.ts b/scripts/i18n.ts index e8a6cc31ff..dd16ea9c6a 100644 --- a/scripts/i18n.ts +++ b/scripts/i18n.ts @@ -2,13 +2,10 @@ import { createHash } from 'node:crypto' import { readFileSync, writeFileSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { createAnthropic } from '@ai-sdk/anthropic' -import { createOpenAI } from '@ai-sdk/openai' +import { createCompiledCatalog } from '@lingui/cli/api' import { compileMessageOrThrow } from '@lingui/message-utils/compileMessage' -import { generateText, Output } from 'ai' import { type PoFile, type PoItem, parsePo, stringifyPo } from 'pofile-ts' import ts from 'typescript' -import { z } from 'zod' export type SourceMessage = { id: string @@ -328,17 +325,6 @@ export async function refreshTranslations( return updated } -const translationOutputSchema = z.object({ - locale: z.string(), - entries: z.array( - z.object({ - id: z.string(), - translation: z.string(), - reviewNotes: z.string(), - }), - ), -}) - function promptFor(request: TranslationRequest): string { return [ `Translate these Pascal architecture/CAD interface messages into ${request.locale}.`, @@ -351,36 +337,45 @@ function promptFor(request: TranslationRequest): string { ].join('\n\n') } -function configuredProvider(): TranslationProvider { +async function configuredProvider(): Promise { const name = process.env.I18N_PROVIDER ?? (process.env.OPENAI_API_KEY ? 'openai' : process.env.ANTHROPIC_API_KEY ? 'anthropic' : '') const modelName = process.env.I18N_MODEL if (!modelName) throw new Error('Set I18N_MODEL to the translation model') - const model = - name === 'openai' - ? createOpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL, - })(modelName) - : name === 'anthropic' - ? createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, - baseURL: process.env.ANTHROPIC_BASE_URL, - })(modelName) - : null - if (name === 'openai' && !process.env.OPENAI_API_KEY) { throw new Error('Set OPENAI_API_KEY for the OpenAI translation provider') } if (name === 'anthropic' && !process.env.ANTHROPIC_API_KEY) { throw new Error('Set ANTHROPIC_API_KEY for the Anthropic translation provider') } - if (!model) { + if (name !== 'openai' && name !== 'anthropic') { throw new Error('Set I18N_PROVIDER to "openai" or "anthropic" and provide its API key') } + const [{ generateText, Output }, { z }] = await Promise.all([import('ai'), import('zod')]) + const model = + name === 'openai' + ? (await import('@ai-sdk/openai')).createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + })(modelName) + : (await import('@ai-sdk/anthropic')).createAnthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, + baseURL: process.env.ANTHROPIC_BASE_URL, + })(modelName) + const translationOutputSchema = z.object({ + locale: z.string(), + entries: z.array( + z.object({ + id: z.string(), + translation: z.string(), + reviewNotes: z.string(), + }), + ), + }) + return { name, model: modelName, @@ -411,6 +406,18 @@ function runLingui(command: 'extract' | 'compile', strict = false): void { if (result.exitCode !== 0) throw new Error(`Lingui ${command} failed`) } +function checkLinguiCatalogSync(): string[] { + const result = Bun.spawnSync( + [process.execPath, 'x', 'lingui', 'check', 'sync', '--clean', '--workers', '1'], + { + cwd: ROOT, + stdout: 'inherit', + stderr: 'inherit', + }, + ) + return result.exitCode === 0 ? [] : ['PO catalogs are not synchronized; run bun run i18n:extract'] +} + function writePo(path: string, po: PoFile): void { writeFileSync(path, stringifyPo(po, { foldLength: 0 })) } @@ -519,20 +526,6 @@ function bypassedLiterals(): string[] { return warnings } -function catalogFiles(): string[] { - return ALL_LOCALES.flatMap((locale) => [catalogPath(locale), compiledCatalogPath(locale)]) -} - -function snapshot(paths: string[]): Map { - return new Map(paths.map((path) => [path, readFileSync(path, 'utf8')])) -} - -function changedSince(before: Map): string[] { - return [...before].flatMap(([path, contents]) => - readFileSync(path, 'utf8') === contents ? [] : [relative(ROOT, path)], - ) -} - function checkCatalogMessages(sources: SourceMessage[], glossary: Glossary): string[] { const errors: string[] = [] const sourceById = new Map(sources.map((source) => [source.id, source])) @@ -571,17 +564,36 @@ function checkCatalogMessages(sources: SourceMessage[], glossary: Glossary): str return errors } +function checkCompiledCatalogs(): string[] { + return ALL_LOCALES.flatMap((locale) => { + const po = parsePo(readFileSync(catalogPath(locale), 'utf8')) + const messages = Object.fromEntries( + po.items.filter((item) => !item.obsolete).map((item) => [item.msgid, item.msgstr[0] ?? '']), + ) + const compiled = createCompiledCatalog(locale, messages, { + namespace: 'ts', + outputPrefix: '/*eslint-disable*/', + strict: true, + }) + const errors = compiled.errors.map( + ({ id, error }) => `${locale}: failed to compile "${id}": ${error.message}`, + ) + if (compiled.source !== readFileSync(compiledCatalogPath(locale), 'utf8')) { + errors.push( + `${relative(ROOT, compiledCatalogPath(locale))} is not synchronized; run bun run i18n:compile`, + ) + } + return errors + }) +} + export async function checkCatalogs(): Promise { - const before = snapshot(catalogFiles()) - runLingui('extract') - annotateCatalogs() - runLingui('compile', true) - const changed = changedSince(before) const sources = readSourceMessages() const glossary = readJson(GLOSSARY_PATH) return [ - ...changed.map((path) => `${path} is not synchronized; run bun run i18n:extract`), + ...checkLinguiCatalogSync(), ...checkCatalogMessages(sources, glossary), + ...checkCompiledCatalogs(), ] } @@ -622,7 +634,7 @@ async function main(): Promise { const locales = requested.length > 0 ? requested : [...TARGET_LOCALES] const sources = readSourceMessages() const glossary = readJson(GLOSSARY_PATH) - const provider = configuredProvider() + const provider = await configuredProvider() for (const locale of locales) { if (!TARGET_LOCALES.includes(locale as (typeof TARGET_LOCALES)[number])) { throw new Error(`Unknown bundled locale "${locale}"`) From 13e9095fa25223f3188f839ea675d75b4e91ca3d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Oct 2026 23:48:14 +0000 Subject: [PATCH 19/23] fix(i18n): isolate locale scopes and localized inputs --- apps/editor/app/client-bootstrap.tsx | 5 +- packages/core/src/i18n/format.ts | 7 +- packages/core/src/i18n/i18n.test.tsx | 68 +++++++++++++++++++ packages/core/src/i18n/registry.ts | 40 +++++++++-- packages/core/src/i18n/use-translate.ts | 2 + .../floorplan-measurement-tool-layer.tsx | 20 +++--- .../floorplan-dimension-edit-overlay.tsx | 7 +- .../components/editor/floorplan-panel.test.ts | 7 ++ .../src/components/editor/floorplan-panel.tsx | 30 ++++++-- .../editor/src/components/editor/index.tsx | 1 - .../ui/controls/metric-control.test.ts | 19 ++++++ .../components/ui/controls/metric-control.tsx | 32 ++++++--- .../components/ui/floating-level-selector.tsx | 23 +++++-- .../editor/src/i18n/published-entry.test.ts | 17 +++++ packages/editor/src/index.tsx | 9 +++ .../src/lib/floorplan/dimension-drive.test.ts | 5 ++ .../src/lib/floorplan/dimension-drive.ts | 5 +- packages/editor/src/lib/measurements.test.ts | 6 ++ packages/editor/src/lib/measurements.ts | 11 ++- .../cabinet/__tests__/dimension-input.test.ts | 19 ++++++ packages/nodes/src/cabinet/dimension-input.ts | 21 ++++++ packages/nodes/src/cabinet/tool.tsx | 17 +++-- packages/nodes/src/measurement/tool.test.ts | 2 + packages/nodes/src/measurement/tool.tsx | 19 ++++-- 24 files changed, 323 insertions(+), 69 deletions(-) create mode 100644 packages/editor/src/components/editor/floorplan-panel.test.ts create mode 100644 packages/editor/src/components/ui/controls/metric-control.test.ts create mode 100644 packages/editor/src/i18n/published-entry.test.ts create mode 100644 packages/nodes/src/cabinet/__tests__/dimension-input.test.ts create mode 100644 packages/nodes/src/cabinet/dimension-input.ts diff --git a/apps/editor/app/client-bootstrap.tsx b/apps/editor/app/client-bootstrap.tsx index ae89a75ba7..0c307cd874 100644 --- a/apps/editor/app/client-bootstrap.tsx +++ b/apps/editor/app/client-bootstrap.tsx @@ -40,11 +40,8 @@ export function ClientBootstrap({ }, [i18n]) useEffect(() => { - const syncDefaultLocale = () => i18n.setLocale(defaultI18n.getLocale()) - return defaultI18n.subscribe(syncDefaultLocale) + return defaultI18n.subscribeLocale((nextLocale) => i18n.setLocale(nextLocale)) }, [i18n]) - useEffect(() => () => i18n.destroy(), [i18n]) - return {children} } diff --git a/packages/core/src/i18n/format.ts b/packages/core/src/i18n/format.ts index 5dc5894588..85c974d391 100644 --- a/packages/core/src/i18n/format.ts +++ b/packages/core/src/i18n/format.ts @@ -39,8 +39,9 @@ function fractionDigits(value: number | undefined, fallback: number): number { function decimalSeparator(locale: string): string { try { return ( - new Intl.NumberFormat(locale).formatToParts(1.1).find((part) => part.type === 'decimal') - ?.value ?? '.' + new Intl.NumberFormat([locale, 'en']) + .formatToParts(1.1) + .find((part) => part.type === 'decimal')?.value ?? '.' ) } catch { return '.' @@ -77,7 +78,7 @@ export function formatNumber(value: number, options: NumberFormatOptions = {}): ) try { - return new Intl.NumberFormat(options.locale ?? 'en', { + return new Intl.NumberFormat([options.locale ?? 'en', 'en'], { minimumFractionDigits, maximumFractionDigits, useGrouping: options.useGrouping ?? false, diff --git a/packages/core/src/i18n/i18n.test.tsx b/packages/core/src/i18n/i18n.test.tsx index ea93db2df8..c03847c382 100644 --- a/packages/core/src/i18n/i18n.test.tsx +++ b/packages/core/src/i18n/i18n.test.tsx @@ -3,6 +3,7 @@ import { renderToString } from 'react-dom/server' import { BuildingNode, createI18n, + defaultI18n, formatLength, formatNumber, LevelNode, @@ -102,16 +103,39 @@ describe('message registry', () => { test('notifies subscribers for locale and active-catalog changes', async () => { let changes = 0 + let localeChanges = 0 const unsubscribe = subscribe(() => { changes += 1 }) + const unsubscribeLocale = defaultI18n.subscribeLocale(() => { + localeChanges += 1 + }) setLocale('ja') await registerMessages('ja', 'viewer', { test: 'テスト' }) unsubscribe() + unsubscribeLocale() setLocale('es') expect(changes).toBe(2) + expect(localeChanges).toBe(1) + }) + + test('keeps a scoped locale across catalog refreshes and accepts repeated global locale intent', async () => { + const scope = createI18n({ locale: 'fr' }) + const release = scope.retain() + const unsubscribe = defaultI18n.subscribeLocale((locale) => scope.setLocale(locale)) + + await registerMessages('fr', 'viewer', { 'test.catalog-refresh': 'Actualisé' }) + expect(scope.getLocale()).toBe('fr') + expect(scope._({ id: 'viewer.test.catalog-refresh', message: 'Refreshed' })).toBe('Actualisé') + + setLocale('en') + expect(scope.getLocale()).toBe('en') + + unsubscribe() + release() + scope.destroy() }) test('keeps request-local scopes isolated', async () => { @@ -129,6 +153,45 @@ describe('message registry', () => { japanese.destroy() }) + test('does not retain server scopes and can reattach after Strict Mode effect replay', async () => { + const serverScope = createI18n({ locale: 'fr' }) + let serverRefreshes = 0 + serverScope.subscribe(() => { + serverRefreshes += 1 + }) + renderToString( + + server + , + ) + + await registerMessages('fr', 'viewer', { 'test.after-server-render': 'Après' }) + expect(serverRefreshes).toBe(0) + expect( + serverScope._({ id: 'viewer.test.after-server-render', message: 'After server render' }), + ).toBe('After server render') + + const clientScope = createI18n({ locale: 'fr' }) + let clientRefreshes = 0 + clientScope.subscribe(() => { + clientRefreshes += 1 + }) + const firstCleanup = clientScope.retain() + firstCleanup() + firstCleanup() + const secondCleanup = clientScope.retain() + + await registerMessages('fr', 'viewer', { 'test.after-effect-replay': 'Rattaché' }) + expect(clientRefreshes).toBe(1) + expect(clientScope._({ id: 'viewer.test.after-effect-replay', message: 'Reattached' })).toBe( + 'Rattaché', + ) + + secondCleanup() + serverScope.destroy() + clientScope.destroy() + }) + test('warns once in development and never exposes the missing key', () => { const previous = process.env.NODE_ENV process.env.NODE_ENV = 'development' @@ -153,6 +216,11 @@ describe('locale formatting', () => { expect(formatLength(2.75, 'm', { locale: 'en' })).toBe('2.75\u00a0m') }) + test('falls back to English formatting for an unsupported locale', () => { + expect(formatNumber(2.75, { locale: 'zz', max: 2 })).toBe('2.75') + expect(formatLength(2.75, 'm', { locale: 'zz' })).toBe('2.75\u00a0m') + }) + test('accepts the locale decimal separator as well as a dot', () => { expect(parseLocalizedNumber('2,75', 'fr')).toBe(2.75) expect(parseLocalizedNumber('2.75', 'fr')).toBe(2.75) diff --git a/packages/core/src/i18n/registry.ts b/packages/core/src/i18n/registry.ts index 046a32a900..4cc2be8cfa 100644 --- a/packages/core/src/i18n/registry.ts +++ b/packages/core/src/i18n/registry.ts @@ -95,6 +95,8 @@ export class PascalI18n { readonly i18n: LinguiI18n private readonly englishI18n: LinguiI18n private readonly listeners = new Set<() => void>() + private readonly localeListeners = new Set<(locale: string) => void>() + private retentionCount = 0 private locale: string private version = 0 @@ -102,7 +104,6 @@ export class PascalI18n { this.locale = canonicalLocale(initialLocale) this.i18n = setupI18n() this.englishI18n = setupI18n() - scopes.add(this) this.refresh(false) } @@ -116,9 +117,11 @@ export class PascalI18n { setLocale(nextLocale: string): void { const normalized = canonicalLocale(nextLocale) - if (normalized === this.locale) return - this.locale = normalized - this.refresh() + if (normalized !== this.locale) { + this.locale = normalized + this.refresh() + } + for (const listener of [...this.localeListeners]) listener(normalized) } subscribe(listener: () => void): () => void { @@ -128,9 +131,37 @@ export class PascalI18n { } } + /** Subscribe only to locale selection, excluding catalog refreshes. */ + subscribeLocale(listener: (locale: string) => void): () => void { + this.localeListeners.add(listener) + return () => { + this.localeListeners.delete(listener) + } + } + + /** + * Retain this scope for shared catalog updates while a client provider is + * mounted. Server rendering never calls effects, so request scopes are not + * held by the module registry. + */ + retain(): () => void { + this.retentionCount += 1 + if (this.retentionCount === 1) scopes.add(this) + let retained = true + return () => { + if (!retained) return + retained = false + if (this.retentionCount === 0) return + this.retentionCount -= 1 + if (this.retentionCount === 0) scopes.delete(this) + } + } + destroy(): void { + this.retentionCount = 0 scopes.delete(this) this.listeners.clear() + this.localeListeners.clear() } _(descriptor: MessageDescriptor): string @@ -172,6 +203,7 @@ export function createI18n(options: { locale?: string } = {}): PascalI18n { /** @experimental Unreleased API; signatures may change before the first i18n release. */ export const defaultI18n = createI18n() +defaultI18n.retain() /** * Register a raw ICU MessageFormat pack. The Lingui compiler is loaded only diff --git a/packages/core/src/i18n/use-translate.ts b/packages/core/src/i18n/use-translate.ts index f1e32a5131..214db0d3d9 100644 --- a/packages/core/src/i18n/use-translate.ts +++ b/packages/core/src/i18n/use-translate.ts @@ -5,6 +5,7 @@ import { createElement, type ReactNode, useContext, + useEffect, useSyncExternalStore, } from 'react' import { defaultI18n, type PascalI18n } from './registry' @@ -17,6 +18,7 @@ const PascalI18nContext = createContext(defaultI18n) * @experimental Unreleased API; signatures may change before the first i18n release. */ export function PascalI18nProvider({ children, i18n }: { children: ReactNode; i18n: PascalI18n }) { + useEffect(() => i18n.retain(), [i18n]) return createElement(PascalI18nContext.Provider, { value: i18n }, children) } diff --git a/packages/editor/src/components/editor-2d/floorplan-measurement-tool-layer.tsx b/packages/editor/src/components/editor-2d/floorplan-measurement-tool-layer.tsx index 70840020e3..e24fc9fca4 100644 --- a/packages/editor/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-measurement-tool-layer.tsx @@ -16,6 +16,7 @@ import { measurementPerimeter, measurementPrismVolume, nodeRegistry, + useLocale, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' @@ -27,9 +28,9 @@ import { formatLinearMeasurement, formatVolumeLabel, getLinearUnitLabel, - linearUnitToMeters, MEASUREMENT_ACTIVE_COLOR, metersToLinearUnit, + parseLinearMeasurementInput, } from '../../lib/measurements' import { clearSurfacePlanSnapFeedback, @@ -636,6 +637,7 @@ function FloorplanExtrusionControl({ unitsPerPixel: number }) { const unit = useViewer((state) => state.unit) + const locale = useLocale() const extrusionHeight = useMeasurementDraft((state) => state.extrusionHeight) const points = useMeasurementDraft((state) => state.points) const baseNormal = useMeasurementDraft((state) => state.baseNormal) @@ -654,12 +656,12 @@ function FloorplanExtrusionControl({ const y = center[2] - height / 2 const commit = () => { - const numericValue = Number.parseFloat(value) - if (!Number.isFinite(numericValue)) { + const meters = parseLinearMeasurementInput(value, unit, locale) + if (meters === null) { useMeasurementDraft.getState().setExtrusionHeight('2d', 0) return } - useMeasurementDraft.getState().setExtrusionHeight('2d', linearUnitToMeters(numericValue, unit)) + useMeasurementDraft.getState().setExtrusionHeight('2d', meters) const draft = useMeasurementDraft.getState() if (draft.finishExtrusion('2d')) commitMeasurementDraft('2d') } @@ -710,11 +712,9 @@ function FloorplanExtrusionControl({ onChange={(event) => { const next = event.target.value setValue(next) - const numericValue = Number.parseFloat(next) - if (Number.isFinite(numericValue)) { - useMeasurementDraft - .getState() - .setExtrusionHeight('2d', linearUnitToMeters(numericValue, unit)) + const meters = parseLinearMeasurementInput(next, unit, locale) + if (meters !== null) { + useMeasurementDraft.getState().setExtrusionHeight('2d', meters) } else { useMeasurementDraft.getState().setExtrusionHeight('2d', 0) } @@ -728,7 +728,7 @@ function FloorplanExtrusionControl({ }} placeholder="0" step="0.1" - type="number" + type="text" value={value} /> diff --git a/packages/editor/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx b/packages/editor/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx index a28e419eba..640164afe1 100644 --- a/packages/editor/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +++ b/packages/editor/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx @@ -1,7 +1,7 @@ 'use client' import type { AnyNode, AnyNodeId } from '@pascal-app/core' -import { useScene } from '@pascal-app/core' +import { useLocale, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' @@ -78,6 +78,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null { const [error, setError] = useState(null) const inputRef = useRef(null) const unit = useViewer((state) => state.unit) + const locale = useLocale() useEffect(() => { let down: { x: number; y: number } | null = null @@ -154,7 +155,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null { const commit = useCallback(() => { if (!target) return - const nextLength = parseDimensionInput(draft, unit) + const nextLength = parseDimensionInput(draft, unit, locale) if (nextLength === null) { setError(`Cannot read "${draft.trim()}"`) return @@ -166,7 +167,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null { } setTarget(null) setError(null) - }, [draft, target, unit]) + }, [draft, locale, target, unit]) if (!target) return null if (typeof document === 'undefined') return null diff --git a/packages/editor/src/components/editor/floorplan-panel.test.ts b/packages/editor/src/components/editor/floorplan-panel.test.ts new file mode 100644 index 0000000000..f4482247c6 --- /dev/null +++ b/packages/editor/src/components/editor/floorplan-panel.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'bun:test' +import { parseReferenceScaleLength } from './floorplan-panel' + +test('floor-plan reference scales parse with the scoped locale', () => { + expect(parseReferenceScaleLength('2,75', 'meters', 'fr')).toBe(2.75) + expect(parseReferenceScaleLength('2,75', 'meters', 'en')).toBeNull() +}) diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index d6fa6c9074..b257b63f69 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -50,6 +50,7 @@ import { useInteractive, useLiveNodeOverrides, useLiveTransforms, + useLocale, useScene, type WallNode, WallNode as WallNodeSchema, @@ -526,7 +527,7 @@ type GuideTransformDraft = { rotation: number } -type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches' +export type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches' // The in-flight reference-scale measurement. Only the per-CLICK fields live // here (guide + start anchor); the rubber-band's moving END is the shared @@ -2465,22 +2466,35 @@ const REFERENCE_SCALE_LINGO_UNIT: Record(null) const svgRef = useRef(null) const floorplanBackgroundRef = useRef(null) @@ -7161,7 +7176,7 @@ export function FloorplanPanel({ return } - const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit) + const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) if (!(displayLength && displayLength > 0)) { return } @@ -7215,6 +7230,7 @@ export function FloorplanPanel({ setPendingReferenceScale(null) }, [ guideById, + locale, pendingReferenceScale, referenceScaleUnit, referenceScaleValue, @@ -11205,7 +11221,7 @@ export function FloorplanPanel({ isPanning || isRotatingFloorplan ? 'grabbing' : isSpacePanPressed ? 'grab' : null const isFloorplanNavigationOverlayVisible = isSpacePanPressed || isPanning || isRotatingFloorplan const pendingReferenceDisplayLength = - parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit) ?? Number.NaN + parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) ?? Number.NaN const pendingReferenceRealLengthMeters = pendingReferenceScale && pendingReferenceDisplayLength > 0 ? convertReferenceLengthToMeters(pendingReferenceDisplayLength, referenceScaleUnit) @@ -11228,7 +11244,7 @@ export function FloorplanPanel({ : 'Length must be greater than 0.' const referenceScaleHint = referenceScaleInputError ? null - : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit) + : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit, locale) return (
createI18n({ locale: props.locale }), [props.locale]) - useEffect(() => () => i18n.destroy(), [i18n]) return ( diff --git a/packages/editor/src/components/ui/controls/metric-control.test.ts b/packages/editor/src/components/ui/controls/metric-control.test.ts new file mode 100644 index 0000000000..f7d5dee4ff --- /dev/null +++ b/packages/editor/src/components/ui/controls/metric-control.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'bun:test' +import { parseMetricInput } from './metric-control' + +test('metric controls parse stair dimensions with the scoped locale', () => { + expect( + parseMetricInput('1,5', 'm', { + isImperial: false, + locale: 'fr', + parseUnit: 'm', + }), + ).toBe(1.5) + expect( + parseMetricInput('1,5', 'm', { + isImperial: false, + locale: 'en', + parseUnit: 'm', + }), + ).toBeNull() +}) diff --git a/packages/editor/src/components/ui/controls/metric-control.tsx b/packages/editor/src/components/ui/controls/metric-control.tsx index 749ad84378..b810e96560 100644 --- a/packages/editor/src/components/ui/controls/metric-control.tsx +++ b/packages/editor/src/components/ui/controls/metric-control.tsx @@ -1,6 +1,6 @@ 'use client' -import { useScene } from '@pascal-app/core' +import { parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core' import { useCallback, useEffect, useRef, useState } from 'react' import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser' import { useLinearDisplay } from '../../../lib/use-linear-display' @@ -20,6 +20,21 @@ interface MetricControlProps { restoreOnCommit?: boolean } +export function parseMetricInput( + text: string, + unit: string, + options: { isImperial: boolean; locale: string; parseUnit?: string }, +): number | null { + const spec = lingoUnitSpec(unit) + return spec + ? parseMeasurement(text, spec, { + bareUnit: options.parseUnit ?? spec.unitId, + locale: options.locale, + system: options.isImperial ? 'us' : 'metric', + }) + : parseLocalizedNumber(text, options.locale) +} + export function MetricControl({ label, value, @@ -33,6 +48,7 @@ export function MetricControl({ unit = '', restoreOnCommit = true, }: MetricControlProps) { + const locale = useLocale() const { isImperial, displayUnit, @@ -230,16 +246,8 @@ export function MetricControl({ const submitValue = useCallback(() => { const spec = lingoUnitSpec(unit) - let stored = spec - ? parseMeasurement(inputValue, spec, { - bareUnit: parseUnit ?? spec.unitId, - system: isImperial ? 'us' : 'metric', - }) - : null - if (stored === null) { - const numValue = Number.parseFloat(inputValue) - stored = Number.isFinite(numValue) ? toStoredValue(numValue) : null - } + const parsed = parseMetricInput(inputValue, unit, { isImperial, locale, parseUnit }) + const stored = parsed === null ? null : spec ? parsed : toStoredValue(parsed) if (stored === null) { setInputValue(toDisplayValue(value).toFixed(precision)) } else { @@ -249,6 +257,7 @@ export function MetricControl({ }, [ inputValue, unit, + locale, isImperial, parseUnit, applyCommittedValue, @@ -265,6 +274,7 @@ export function MetricControl({ ? measurementHint(inputValue, spec, { bareUnit: parseUnit ?? spec.unitId, system: isImperial ? 'us' : 'metric', + locale, displayUnit: parseUnit ?? spec.unitId, precision, clamp, diff --git a/packages/editor/src/components/ui/floating-level-selector.tsx b/packages/editor/src/components/ui/floating-level-selector.tsx index 47e34bab33..40d6b176d3 100644 --- a/packages/editor/src/components/ui/floating-level-selector.tsx +++ b/packages/editor/src/components/ui/floating-level-selector.tsx @@ -55,11 +55,15 @@ import { type LevelDuplicatePreset, } from '@pascal-app/core/building' import { + formatNumber, getDefaultLevelName, getLevelDisplayName, roofLevelOrderBreach, servesAsRoofLevel, + useLocale, + usePascalI18n, } from '@pascal-app/core' +import { localizedLevelName } from '../../i18n/level-name' import { addStoreyBelow, addStoreyOnTop, setGroundFloor } from '../../lib/add-storey' import { deleteLevelWithFallbackSelection } from '../../lib/level-selection' import { unitMemberLevels, leaveUnitFocus } from '../../lib/units' @@ -173,14 +177,19 @@ function LevelRow({ const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false) const [isEditing, setIsEditing] = useState(false) const updateNode = useScene((s) => s.updateNode) + const i18n = usePascalI18n() + const locale = useLocale() const { isImperial, toDisplay, displayUnit, precision: displayPrecision } = useLinearDisplay('m', 2) const storeyHeight = getStoredLevelHeight(level) // Decimal units keep the compact readout; integer millimeters must retain trailing zeroes. - const formattedStoreyHeight = toDisplay(storeyHeight).toFixed(displayPrecision) - const storeyHeightLabel = `${ - displayPrecision > 0 ? formattedStoreyHeight.replace(/0$/, '') : formattedStoreyHeight - } ${displayUnit}` + const formattedStoreyHeight = formatNumber(toDisplay(storeyHeight), { + locale, + min: Math.max(0, displayPrecision - 1), + max: displayPrecision, + }) + const storeyHeightLabel = `${formattedStoreyHeight} ${displayUnit}` + const levelName = localizedLevelName(level, i18n, locale) // Same rule as the site panel and command palette: the ordinal-0 ground // floor is the vertical model's zero anchor and must never be deletable. const canDeleteLevel = level.level !== 0 @@ -219,7 +228,7 @@ function LevelRow({ >