diff --git a/package-lock.json b/package-lock.json index 6a12e7e..896337c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,8 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "eslint": "^8.56.0", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", "jest": "^29.7.0", "playcanvas": "^1.74.0", "postcss": "^8.4.41", @@ -4525,6 +4527,38 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "23.15.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.1.tgz", + "integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index a086460..bc76a8b 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "eslint": "^8.56.0", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", "jest": "^29.7.0", "playcanvas": "^1.74.0", "postcss": "^8.4.41", diff --git a/src/scene.ts b/src/scene.ts index 45abf90..93dbe3a 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -19,6 +19,7 @@ import { Camera } from './camera'; import { CustomShadow as Shadow } from './custom-shadow'; // import { Grid } from './grid'; import { InfiniteGrid as Grid } from './infinite-grid'; +import { localize } from './ui/localization'; class Scene { events: Events; @@ -217,7 +218,7 @@ class Scene { } catch (err) { this.events.invoke('showPopup', { type: 'error', - header: 'ERROR LOADING FILE', + header: localize('popup.error-loading'), message: `${err.message ?? err} while loading '${filename}'` }); } diff --git a/src/ui/bottom-toolbar.ts b/src/ui/bottom-toolbar.ts index b87c752..3403dff 100644 --- a/src/ui/bottom-toolbar.ts +++ b/src/ui/bottom-toolbar.ts @@ -1,6 +1,7 @@ import { Button, Element, Container } from 'pcui'; import { Events } from '../events'; import { Tooltips } from './tooltips'; +import { localize } from './localization'; import undoSvg from '../svg/undo.svg'; import redoSvg from '../svg/redo.svg'; @@ -139,17 +140,17 @@ class BottomToolbar extends Container { }); // register tooltips - tooltips.register(undo, 'Undo ( Ctrl + Z )'); - tooltips.register(redo, 'Redo ( Ctrl + Shift + Z )'); - tooltips.register(picker, 'Picker Select ( P )'); - tooltips.register(brush, 'Brush Select ( B )'); + tooltips.register(undo, localize('tooltip.undo')); + tooltips.register(redo, localize('tooltip.redo')); + tooltips.register(picker, localize('tooltip.picker')); + tooltips.register(brush, localize('tooltip.brush')); // tooltips.register(lasso, 'Lasso Select'); - tooltips.register(sphere, 'Sphere Select'); + tooltips.register(sphere, localize('tooltip.sphere')); // tooltips.register(crop, 'Crop'); - tooltips.register(translate, 'Translate ( 1 )'); - tooltips.register(rotate, 'Rotate ( 2 )'); - tooltips.register(scale, 'Scale ( 3 )'); - tooltips.register(coordSpace, 'Local Space Gizmo'); + tooltips.register(translate, localize('tooltip.translate')); + tooltips.register(rotate, localize('tooltip.rotate')); + tooltips.register(scale, localize('tooltip.scale')); + tooltips.register(coordSpace, localize('tooltip.local-space')); } } diff --git a/src/ui/data-panel.ts b/src/ui/data-panel.ts index 2675708..ab4b660 100644 --- a/src/ui/data-panel.ts +++ b/src/ui/data-panel.ts @@ -3,6 +3,7 @@ import { Events } from '../events'; import { Splat } from '../splat'; import { Histogram } from './histogram'; import { State } from '../edit-ops'; +import { localize } from './localization'; import { rgb2hsv } from './color'; const SH_C0 = 0.28209479177387814; @@ -78,7 +79,7 @@ class DataPanel extends Panel { constructor(events: Events, args = { }) { args = { ...args, - headerText: 'SPLAT DATA', + headerText: localize('data'), id: 'data-panel', resizable: 'top', resizeMax: 1000, @@ -107,19 +108,19 @@ class DataPanel extends Panel { { v: 'x', t: 'X' }, { v: 'y', t: 'Y' }, { v: 'z', t: 'Z' }, - { v: 'distance', t: 'Distance' }, - { v: 'volume', t: 'Volume' }, - { v: 'surface-area', t: 'Surface Area' }, - { v: 'scale_0', t: 'Scale X' }, - { v: 'scale_1', t: 'Scale Y' }, - { v: 'scale_2', t: 'Scale Z' }, - { v: 'f_dc_0', t: 'Red' }, - { v: 'f_dc_1', t: 'Green' }, - { v: 'f_dc_2', t: 'Blue' }, - { v: 'opacity', t: 'Opacity' }, - { v: 'hue', t: 'Hue' }, - { v: 'saturation', t: 'Saturation' }, - { v: 'value', t: 'Value' } + { v: 'distance', t: localize('data.distance') }, + { v: 'volume', t: localize('data.volume') }, + { v: 'surface-area', t: localize('data.surface-area') }, + { v: 'scale_0', t: localize('data.scale-x') }, + { v: 'scale_1', t: localize('data.scale-y') }, + { v: 'scale_2', t: localize('data.scale-z') }, + { v: 'f_dc_0', t: localize('data.red') }, + { v: 'f_dc_1', t: localize('data.green') }, + { v: 'f_dc_2', t: localize('data.blue') }, + { v: 'opacity', t: localize('data.opacity') }, + { v: 'hue', t: localize('data.hue') }, + { v: 'saturation', t: localize('data.saturation') }, + { v: 'value', t: localize('data.value') } ] }); @@ -129,7 +130,7 @@ class DataPanel extends Panel { const logScaleLabel = new Label({ class: 'control-label', - text: 'Log Scale' + text: localize('data.log-scale') }); const logScaleValue = new BooleanInput({ @@ -143,12 +144,12 @@ class DataPanel extends Panel { controls.append(dataSelector); controls.append(logScale); - controls.append(sepLabel('Totals')); + controls.append(sepLabel(localize('data.totals'))); - const splatsValue = dataLabel(controls, 'Splats'); - const selectedValue = dataLabel(controls, 'Selected'); - const hiddenValue = dataLabel(controls, 'Hidden'); - const deletedValue = dataLabel(controls, 'Deleted'); + const splatsValue = dataLabel(controls, localize('data.totals.splats')); + const selectedValue = dataLabel(controls, localize('data.totals.selected')); + const hiddenValue = dataLabel(controls, localize('data.totals.hidden')); + const deletedValue = dataLabel(controls, localize('data.totals.deleted')); controlsContainer.append(controls); diff --git a/src/ui/editor.ts b/src/ui/editor.ts index fb44d24..8157d2d 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -12,6 +12,7 @@ import { RightToolbar } from './right-toolbar'; import { ModeToggle } from './mode-toggle'; import { Tooltips } from './tooltips'; import { ShortcutsPopup } from './shortcuts-popup'; +import { localizeInit } from './localization'; import { version } from '../../package.json'; import logo from './playcanvas-logo.png'; @@ -25,6 +26,8 @@ class EditorUI { popup: Popup; constructor(events: Events, remoteStorageMode: boolean) { + localizeInit(); + // favicon const link = document.createElement('link'); link.rel = 'icon'; diff --git a/src/ui/localization.ts b/src/ui/localization.ts new file mode 100644 index 0000000..593f8df --- /dev/null +++ b/src/ui/localization.ts @@ -0,0 +1,422 @@ +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +const localizeInit = () => { + i18next + .use(LanguageDetector) + .init({ + fallbackLng: 'en', + debug: true, + resources: { + en: { + translation: { + // Scene menu + "scene": "Scene", + "scene.new": "New", + "scene.open": "Open", + "scene.import": "Import", + "scene.load-all-data": "Load all PLY data", + "scene.save": "Save", + "scene.save-as": "Save As...", + "scene.export": "Export", + "scene.export.compressed-ply": "Compressed PLY", + "scene.export.splat": "Splat file", + + // Selection menu + "selection": "Selection", + "selection.all": "All", + "selection.none": "None", + "selection.invert": "Inverse", + "selection.lock": "Lock Selection", + "selection.unlock": "Unlock All", + "selection.delete": "Delete Selection", + "selection.reset": "Reset Splat", + + // Help menu + "help": "Help", + "help.shortcuts": "Keyboard Shortcuts", + "help.user-guide": "User Guide", + "help.log-issue": "Log an Issue", + "help.github-repo": "GitHub Repo", + "help.discord": "Discord Server", + "help.forum": "Forum", + "help.about": "About SuperSplat", + + // Modes + "mode.centers": "Centers Mode", + "mode.rings": "Rings Mode", + + // Scene panel + "scene-manager": "SCENE MANAGER", + "transform": "TRANSFORM", + "position": "Position", + "rotation": "Rotation", + "scale": "Scale", + + // Options panel + "options": "VIEW OPTIONS", + "options.fov": "Field of View", + "options.sh-bands": "SH Bands", + "options.centers-size": "Centers Size", + "options.show-grid": "Show Grid", + "options.show-bound": "Show Bound", + + // Data panel + "data": "SPLAT DATA", + "data.distance": "Distance", + "data.volume": "Volume", + "data.surface-area": "Surface Area", + "data.scale-x": "Scale X", + "data.scale-y": "Scale Y", + "data.scale-z": "Scale Z", + "data.red": "Red", + "data.green": "Green", + "data.blue": "Blue", + "data.opacity": "Opacity", + "data.hue": "Hue", + "data.saturation": "Saturation", + "data.value": "Value", + "data.log-scale": "Log Scale", + "data.totals": "Totals", + "data.totals.splats": "Splats", + "data.totals.selected": "Selected", + "data.totals.hidden": "Hidden", + "data.totals.deleted": "Deleted", + + // Popup + "popup.ok": "OK", + "popup.cancel": "Cancel", + "popup.yes": "Yes", + "popup.no": "No", + "popup.error-loading": "ERROR LOADING FILE", + + // Right toolbar + "tooltip.splat-mode": "Splat Mode ( M )", + "tooltip.show-hide": "Show/Hide Splats ( Space )", + "tooltip.frame-selection": "Frame Selection ( F )", + "tooltip.view-options": "View Options", + + // Bottom toolbar + "tooltip.undo": "Undo ( Ctrl + Z )", + "tooltip.redo": "Redo ( Ctrl + Shift + Z )", + "tooltip.picker": "Picker Select ( P )", + "tooltip.brush": "Brush Select ( B )", + "tooltip.sphere": "Sphere Select", + "tooltip.translate": "Translate ( 1 )", + "tooltip.rotate": "Rotate ( 2 )", + "tooltip.scale": "Scale ( 3 )", + "tooltip.local-space": "Local Space Gizmo" + } + }, + ja: { + translation: { + // Scene menu + "scene": "シーン", + "scene.new": "新規作成", + "scene.open": "開く", + "scene.import": "インポート", + "scene.load-all-data": "全てのPLYデータを読み込む", + "scene.save": "保存", + "scene.save-as": "名前を付けて保存", + "scene.export": "エクスポート", + "scene.export.compressed-ply": "Compressed PLY ( .ply )", + "scene.export.splat": "Splat ( .splat )", + + // Selection menu + "selection": "選択", + "selection.all": "全て", + "selection.none": "選択を解除", + "selection.invert": "反転", + "selection.lock": "選択をロック", + "selection.unlock": "ロックを解除", + "selection.delete": "選択を削除", + "selection.reset": "変更を全てリセット", + + // Help menu + "help": "ヘルプ", + "help.shortcuts": "キーボードショートカット", + "help.user-guide": "ユーザーガイド", + "help.log-issue": "問題を報告", + "help.github-repo": "GitHubリポジトリ", + "help.discord": "Discordサーバー", + "help.forum": "フォーラム", + "help.about": "SuperSplatについて", + + // Modes + "mode.centers": "センターモード", + "mode.rings": "リングモード", + + // Scene panel + "scene-manager": "シーンマネージャー", + "transform": "トランスフォーム", + "position": "位置", + "rotation": "回転", + "scale": "スケール", + + // Options panel + "options": "表示オプション", + "options.fov": "視野 ( FOV )", + "options.sh-bands": "球面調和関数のバンド", + "options.centers-size": "センターサイズ", + "options.show-grid": "グリッド", + "options.show-bound": "バウンディングボックス", + + // Data panel + "data": "スプラットの統計", + "data.distance": "距離", + "data.volume": "体積", + "data.surface-area": "表面積", + "data.scale-x": "スケール X", + "data.scale-y": "スケール Y", + "data.scale-z": "スケール Z", + "data.red": "赤", + "data.green": "緑", + "data.blue": "青", + "data.opacity": "不透明度", + "data.hue": "色相", + "data.saturation": "彩度", + "data.value": "明度", + "data.log-scale": "対数スケール", + "data.totals": "合計", + "data.totals.splats": "スプラット数", + "data.totals.selected": "選択中", + "data.totals.hidden": "非表示", + "data.totals.deleted": "削除", + + // Popup + "popup.ok": "OK", + "popup.cancel": "キャンセル", + "popup.yes": "はい", + "popup.no": "いいえ", + "popup.error-loading": "ファイルの読み込みエラー", + + // Right toolbar + "tooltip.splat-mode": "スプラットモード ( M )", + "tooltip.show-hide": "スプラットの表示/非表示 ( Space )", + "tooltip.frame-selection": "選択をフレームイン ( F )", + "tooltip.view-options": "表示オプション", + + // Bottom toolbar + "tooltip.undo": "元に戻す ( Ctrl + Z )", + "tooltip.redo": "やり直し ( Ctrl + Shift + Z )", + "tooltip.picker": "ピッカー選択 ( P )", + "tooltip.brush": "ブラシ選択 ( B )", + "tooltip.sphere": "球で選択", + "tooltip.translate": "移動 ( 1 )", + "tooltip.rotate": "回転 ( 2 )", + "tooltip.scale": "スケール ( 3 )", + "tooltip.local-space": "ローカル座標へ切り替え" + } + }, + ko: { + translation: { + // Scene menu + "scene": "장면", + "scene.new": "새로 만들기", + "scene.open": "열기", + "scene.import": "가져오기", + "scene.load-all-data": "모든 PLY 데이터 불러오기", + "scene.save": "저장", + "scene.save-as": "다른 이름으로 저장...", + "scene.export": "내보내기", + "scene.export.compressed-ply": "압축된 PLY", + "scene.export.splat": "Splat 파일", + + // Selection menu + "selection": "선택", + "selection.all": "모두", + "selection.none": "없음", + "selection.invert": "반전", + "selection.lock": "선택 잠금", + "selection.unlock": "모두 잠금 해제", + "selection.delete": "선택 삭제", + "selection.reset": "Splat 재설정", + + // Help menu + "help": "도움말", + "help.shortcuts": "키보드 단축키", + "help.user-guide": "사용자 가이드", + "help.log-issue": "문제 보고", + "help.github-repo": "GitHub 저장소", + "help.discord": "Discord 서버", + "help.forum": "포럼", + "help.about": "SuperSplat 정보", + + // Modes + "mode.centers": "센터 모드", + "mode.rings": "링 모드", + + // Scene panel + "scene-manager": "장면 관리자", + "transform": "변환", + "position": "위치", + "rotation": "회전", + "scale": "크기", + + // Options panel + "options": "보기 옵션", + "options.fov": "시야각", + "options.sh-bands": "SH 밴드", + "options.centers-size": "센터 크기", + "options.show-grid": "그리드 표시", + "options.show-bound": "경계 표시", + + // Data panel + "data": "SPLAT 데이터", + "data.distance": "거리", + "data.volume": "부피", + "data.surface-area": "표면적", + "data.scale-x": "크기 X", + "data.scale-y": "크기 Y", + "data.scale-z": "크기 Z", + "data.red": "빨강", + "data.green": "녹색", + "data.blue": "파랑", + "data.opacity": "불투명도", + "data.hue": "색조", + "data.saturation": "채도", + "data.value": "명도", + "data.log-scale": "로그 크기", + "data.totals": "합계", + "data.totals.splats": "Splat", + "data.totals.selected": "선택", + "data.totals.hidden": "숨겨진", + "data.totals.deleted": "삭제된", + + // Popup + "popup.ok": "확인", + "popup.cancel": "취소", + "popup.yes": "예", + "popup.no": "아니요", + "popup.error-loading": "파일 로드 오류", + + // Right toolbar + "tooltip.splat-mode": "Splat 모드 ( M )", + "tooltip.show-hide": "스플래츠 표시/숨기기 ( Space )", + "tooltip.frame-selection": "프레임 선택 ( F )", + "tooltip.view-options": "보기 옵션", + + // Bottom toolbar + "tooltip.undo": "실행 취소 ( Ctrl + Z )", + "tooltip.redo": "다시 실행 ( Ctrl + Shift + Z )", + "tooltip.picker": "피커 선택 ( P )", + "tooltip.brush": "브러시 선택 ( B )", + "tooltip.sphere": "구 선택", + "tooltip.translate": "이동 ( 1 )", + "tooltip.rotate": "회전 ( 2 )", + "tooltip.scale": "크기 조정 ( 3 )", + "tooltip.local-space": "로컬 공간" + } + }, + "zh-CN": { + translation: { + // Scene menu + "scene": "场景", + "scene.new": "新建", + "scene.open": "打开", + "scene.import": "导入", + "scene.load-all-data": "加载所有 PLY 数据", + "scene.save": "保存", + "scene.save-as": "另存为...", + "scene.export": "导出", + "scene.export.compressed-ply": "压缩 PLY", + "scene.export.splat": "Splat 文件", + + // Selection menu + "selection": "选择", + "selection.all": "全部", + "selection.none": "无", + "selection.invert": "反选", + "selection.lock": "锁定选择", + "selection.unlock": "解锁全部", + "selection.delete": "删除选择", + "selection.reset": "重置 Splat", + + // Help menu + "help": "帮助", + "help.shortcuts": "键盘快捷键", + "help.user-guide": "用户指南", + "help.log-issue": "报告问题", + "help.github-repo": "GitHub 仓库", + "help.discord": "Discord 服务器", + "help.forum": "论坛", + "help.about": "关于 SuperSplat", + + // Modes + "mode.centers": "中心模式", + "mode.rings": "环模式", + + // Scene panel + "scene-manager": "场景管理器", + "transform": "变换", + "position": "位置", + "rotation": "旋转", + "scale": "缩放", + + // Options panel + "options": "视图选项", + "options.fov": "视野角", + "options.sh-bands": "SH 带", + "options.centers-size": "中心大小", + "options.show-grid": "显示网格", + "options.show-bound": "显示边界", + + // Data panel + "data": "SPLAT 数据", + "data.distance": "距离", + "data.volume": "体积", + "data.surface-area": "表面积", + "data.scale-x": "缩放 X", + "data.scale-y": "缩放 Y", + "data.scale-z": "缩放 Z", + "data.red": "红", + "data.green": "绿", + "data.blue": "蓝", + "data.opacity": "不透明度", + "data.hue": "色相", + "data.saturation": "饱和度", + "data.value": "明度", + "data.log-scale": "对数缩放", + "data.totals": "总计", + "data.totals.splats": "Splat", + "data.totals.selected": "选择", + "data.totals.hidden": "隐藏", + "data.totals.deleted": "删除", + + // Popup + "popup.ok": "确定", + "popup.cancel": "取消", + "popup.yes": "是", + "popup.no": "否", + "popup.error-loading": "加载文件错误", + + // Right toolbar + "tooltip.splat-mode": "Splat 模式 ( M )", + "tooltip.show-hide": "显示/隐藏 Splats ( Space )", + "tooltip.frame-selection": "框选 ( F )", + "tooltip.view-options": "视图选项", + + // Bottom toolbar + "tooltip.undo": "撤销 ( Ctrl + Z )", + "tooltip.redo": "重做 ( Ctrl + Shift + Z )", + "tooltip.picker": "选择器 ( P )", + "tooltip.brush": "画笔 ( B )", + "tooltip.sphere": "球选择", + "tooltip.translate": "移动 ( 1 )", + "tooltip.rotate": "旋转 ( 2 )", + "tooltip.scale": "缩放 ( 3 )", + "tooltip.local-space": "局部坐标系" + } + } + }, + interpolation: { + escapeValue: false + } + }); +}; + +const localize = (key: string) => { + return i18next.t(key); +}; + +export { localizeInit, localize }; diff --git a/src/ui/menu.ts b/src/ui/menu.ts index 28805e3..b1781ca 100644 --- a/src/ui/menu.ts +++ b/src/ui/menu.ts @@ -1,6 +1,7 @@ import { Container, Element, Label, BooleanInput } from 'pcui'; import { Events } from '../events'; import { MenuPanel } from './menu-panel'; +import { localize } from './localization'; import logoSvg from '../svg/playcanvas-logo.svg'; import collapseSvg from '../svg/collapse.svg'; @@ -53,17 +54,17 @@ class Menu extends Container { }); const scene = new Label({ - text: 'Scene', + text: localize('scene'), class: 'menu-option' }); const selection = new Label({ - text: 'Selection', + text: localize('selection'), class: 'menu-option' }); const help = new Label({ - text: 'Help', + text: localize('help'), class: 'menu-option' }); @@ -100,12 +101,12 @@ class Menu extends Container { menubar.append(buttonsContainer); const exportMenuPanel = new MenuPanel([{ - text: 'Compressed Ply', + text: localize('scene.export.compressed-ply'), icon: createSvg(sceneExport), onSelect: () => events.invoke('scene.export', 'compressed-ply'), isEnabled: () => !events.invoke('scene.empty'), }, { - text: 'Splat file', + text: localize('scene.export.splat'), icon: createSvg(sceneExport), onSelect: () => events.invoke('scene.export', 'splat'), isEnabled: () => !events.invoke('scene.empty'), @@ -120,11 +121,11 @@ class Menu extends Container { }); const sceneMenuPanel = new MenuPanel([{ - text: 'New', + text: localize('scene.new'), icon: createSvg(sceneNew), onSelect: () => events.invoke('scene.new') }, { - text: 'Open', + text: localize('scene.open'), icon: createSvg(sceneOpen), onSelect: async () => { if (await events.invoke('scene.new')) { @@ -132,13 +133,13 @@ class Menu extends Container { } } }, { - text: 'Import', + text: localize('scene.import'), icon: createSvg(sceneImport), onSelect: () => events.fire('scene.open') }, { // separator }, { - text: 'Load all PLY data', + text: localize('scene.load-all-data'), extra: allDataToggle, onSelect: () => { events.fire('toggleAllData'); @@ -148,88 +149,88 @@ class Menu extends Container { }, { // separator }, { - text: 'Save', + text: localize('scene.save'), icon: createSvg(sceneSave), onSelect: () => events.fire('scene.save'), isEnabled: () => !events.invoke('scene.empty') }, { - text: 'Save As...', + text: localize('scene.save-as'), icon: createSvg(sceneSave), onSelect: () => events.fire('scene.saveAs'), isEnabled: () => !events.invoke('scene.empty') }, { - text: 'Export', + text: localize('scene.export'), icon: createSvg(sceneExport), subMenu: exportMenuPanel }]); const selectionMenuPanel = new MenuPanel([{ - text: 'All', + text: localize('selection.all'), icon: createSvg(selectAll), extra: 'A', onSelect: () => events.fire('select.all') }, { - text: 'None', + text: localize('selection.none'), icon: createSvg(selectNone), extra: 'Shift + A', onSelect: () => events.fire('select.none') }, { - text: 'Inverse', + text: localize('selection.invert'), icon: createSvg(selectInverse), extra: 'I', onSelect: () => events.fire('select.invert') }, { // separator }, { - text: 'Lock Selection', + text: localize('selection.lock'), icon: createSvg(selectLock), extra: 'H', onSelect: () => events.fire('select.hide') }, { - text: 'Unlock All', + text: localize('selection.unlock'), icon: createSvg(selectUnlock), extra: 'U', onSelect: () => events.fire('select.unhide') }, { - text: 'Delete Selection', + text: localize('selection.delete'), icon: createSvg(selectDelete), extra: 'Delete', onSelect: () => events.fire('select.delete') }, { - text: 'Reset Splat', + text: localize('selection.reset'), onSelect: () => events.fire('scene.reset') }]); const helpMenuPanel = new MenuPanel([{ - text: 'Keyboard Shortcuts', + text: localize('help.shortcuts'), icon: 'E136', onSelect: () => events.fire('show.shortcuts') }, { - text: 'User Guide', + text: localize('help.user-guide'), icon: 'E232', onSelect: () => window.open('https://github.com/playcanvas/supersplat/blob/main/docs/index.md#supersplat-user-guide', '_blank').focus() }, { - text: 'Log an Issue', + text: localize('help.log-issue'), icon: 'E336', onSelect: () => window.open('https://github.com/playcanvas/supersplat/issues', '_blank').focus() }, { - text: 'GitHub Repo', + text: localize('help.github-repo'), icon: 'E259', onSelect: () => window.open('https://github.com/playcanvas/supersplat', '_blank').focus() }, { // separator }, { - text: 'Discord Server', + text: localize('help.discord'), icon: 'E233', onSelect: () => window.open('https://discord.com/channels/408617316415307776/1275850227663769686', '_blank').focus() }, { - text: 'Forum', + text: localize('help.forum'), icon: 'E432', onSelect: () => window.open('https://forum.playcanvas.com', '_blank').focus() }, { // separator }, { - text: 'About SuperSplat', + text: localize('help.about'), icon: 'E138', onSelect: () => events.invoke('show.about') }]); diff --git a/src/ui/mode-toggle.ts b/src/ui/mode-toggle.ts index 6c982d2..6865b80 100644 --- a/src/ui/mode-toggle.ts +++ b/src/ui/mode-toggle.ts @@ -1,6 +1,7 @@ import { Container, Element, Label } from 'pcui'; import { Events } from '../events'; import { Tooltips } from './tooltips'; +import { localize } from './localization'; import centersSvg from '../svg/centers.svg'; import ringsSvg from '../svg/rings.svg'; @@ -32,12 +33,12 @@ class ModeToggle extends Container { const centersText = new Label({ id: 'centers-text', - text: 'Centers mode' + text: localize('mode.centers') }); const ringsText = new Label({ id: 'rings-text', - text: 'Rings mode' + text: localize('mode.rings') }); this.append(centersIcon); @@ -56,7 +57,7 @@ class ModeToggle extends Container { this.class[mode === 'rings' ? 'add' : 'remove']('rings-mode'); }); - tooltips.register(this, 'Splat Mode ( M )'); + tooltips.register(this, localize('tooltip.splat-mode')); } } diff --git a/src/ui/popup.ts b/src/ui/popup.ts index 65c8b6d..25d6557 100644 --- a/src/ui/popup.ts +++ b/src/ui/popup.ts @@ -1,4 +1,5 @@ import { Button, Container, Label, TextInput } from 'pcui'; +import { localize } from './localization'; interface ShowOptions { type: 'error' | 'info' | 'yesno' | 'okcancel'; @@ -40,22 +41,22 @@ class Popup extends Container { const okButton = new Button({ class: 'popup-button', - text: 'OK' + text: localize('popup.ok') }); const cancelButton = new Button({ class: 'popup-button', - text: 'Cancel' + text: localize('popup.cancel') }); const yesButton = new Button({ class: 'popup-button', - text: 'Yes' + text: localize('popup.yes') }); const noButton = new Button({ class: 'popup-button', - text: 'No' + text: localize('popup.no') }); const buttons = new Container({ diff --git a/src/ui/right-toolbar.ts b/src/ui/right-toolbar.ts index 718ce45..ef86abe 100644 --- a/src/ui/right-toolbar.ts +++ b/src/ui/right-toolbar.ts @@ -1,6 +1,7 @@ import { Button, Container, Element, Label } from 'pcui'; import { Events } from '../events'; import { Tooltips } from './tooltips'; +import { localize } from './localization'; import showHideSplatsSvg from '../svg/show-hide-splats.svg'; import frameSelectionSvg from '../svg/frame-selection.svg'; @@ -62,10 +63,10 @@ class RightToolbar extends Container { this.append(new Element({ class: 'right-toolbar-separator' })); this.append(options); - tooltips.register(ringsModeToggle, 'Splat Mode ( M )', 'left'); - tooltips.register(showHideSplats, 'Show/Hide Splats ( Space )', 'left'); - tooltips.register(frameSelection, 'Frame Selection ( F )', 'left'); - tooltips.register(options, 'View Options', 'left'); + tooltips.register(ringsModeToggle, localize('tooltip.splat-mode'), 'left'); + tooltips.register(showHideSplats, localize('tooltip.show-hide'), 'left'); + tooltips.register(frameSelection, localize('tooltip.frame-selection'), 'left'); + tooltips.register(options, localize('tooltip.view-options'), 'left'); // add event handlers diff --git a/src/ui/scene-panel.ts b/src/ui/scene-panel.ts index b738219..aec2e7f 100644 --- a/src/ui/scene-panel.ts +++ b/src/ui/scene-panel.ts @@ -3,6 +3,7 @@ import { Events } from '../events'; import { Tooltips } from './tooltips'; import { SplatList } from './splat-list'; import { Transform } from './transform'; +import { localize } from './localization'; import sceneImportSvg from '../svg/import.svg'; import sceneNewSvg from '../svg/new.svg'; @@ -36,7 +37,7 @@ class ScenePanel extends Container { }); const sceneLabel = new Label({ - text: 'SCENE MANAGER', + text: localize('scene-manager'), class: `panel-header-label` }); @@ -83,7 +84,7 @@ class ScenePanel extends Container { }); const transformLabel = new Label({ - text: 'TRANSFORM', + text: localize('transform'), class: `panel-header-label` }); diff --git a/src/ui/transform.ts b/src/ui/transform.ts index 1bd89a1..65358e0 100644 --- a/src/ui/transform.ts +++ b/src/ui/transform.ts @@ -3,6 +3,7 @@ import { Quat, Vec3 } from 'playcanvas'; import { Events } from '../events'; import { Splat } from '../splat'; import { EntityTransformOp } from '../edit-ops'; +import { localize } from './localization'; class Transform extends Container { constructor(events: Events, args: ContainerArgs = {}) { @@ -49,7 +50,7 @@ class Transform extends Container { const positionLabel = new Label({ class: 'transform-label', - text: 'Position' + text: localize('position') }); const positionVector = new VectorInput({ @@ -70,7 +71,7 @@ class Transform extends Container { const rotationLabel = new Label({ class: 'transform-label', - text: 'Rotation' + text: localize('rotation') }); const rotationVector = new VectorInput({ @@ -91,7 +92,7 @@ class Transform extends Container { const scaleLabel = new Label({ class: 'transform-label', - text: 'Scale' + text: localize('scale') }); const scaleInput = new NumericInput({ diff --git a/src/ui/view-panel.ts b/src/ui/view-panel.ts index 2478cf0..26e7836 100644 --- a/src/ui/view-panel.ts +++ b/src/ui/view-panel.ts @@ -1,6 +1,7 @@ import { BooleanInput, Container, Label, SliderInput } from 'pcui'; import { Events } from '../events'; import { Tooltips } from './tooltips'; +import { localize } from './localization'; class ViewPanel extends Container { constructor(events: Events, tooltips: Tooltips, args = {}) { @@ -29,7 +30,7 @@ class ViewPanel extends Container { }); const label = new Label({ - text: 'VIEW OPTIONS', + text: localize('options'), class: `panel-header-label` }); @@ -43,7 +44,7 @@ class ViewPanel extends Container { }); const fovLabel = new Label({ - text: 'Field of View', + text: localize('options.fov'), class: 'view-panel-row-label' }); @@ -64,7 +65,7 @@ class ViewPanel extends Container { }); const shBandsLabel = new Label({ - text: 'SH Bands', + text: localize('options.sh-bands'), class: 'view-panel-row-label' }); @@ -86,7 +87,7 @@ class ViewPanel extends Container { }); const centersSizeLabel = new Label({ - text: 'Centers Size', + text: localize('options.centers-size'), class: 'view-panel-row-label' }); @@ -108,7 +109,7 @@ class ViewPanel extends Container { }); const showGridLabel = new Label({ - text: 'Show Grid', + text: localize('options.show-grid'), class: 'view-panel-row-label' }); @@ -128,7 +129,7 @@ class ViewPanel extends Container { }); const showBoundLabel = new Label({ - text: 'Show Bound', + text: localize('options.show-bound'), class: 'view-panel-row-label' });