From 8bfa94475effb7198cd2dbf0e5d9fc4bf66e3398 Mon Sep 17 00:00:00 2001 From: flash555588 Date: Mon, 11 May 2026 19:41:14 +0800 Subject: [PATCH] fix: localize loading states --- manifest.json | 2 +- package.json | 2 +- src/i18n/en.ts | 2 ++ src/i18n/index.ts | 11 +++++++++++ src/i18n/zh-CN.ts | 2 ++ src/view/direct-view.ts | 4 ++-- src/view/inline/code-block.ts | 6 +++--- src/view/inline/live-preview.ts | 4 ++-- src/view/inline/loading-overlay.ts | 28 ++++++++++++++++++++++++---- versions.json | 2 +- 10 files changed, 49 insertions(+), 14 deletions(-) diff --git a/manifest.json b/manifest.json index 2f09105..ebe0783 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "ai-3d-model-workbench", "name": "AI 3D Model Workbench", - "version": "0.1.1", + "version": "0.1.2", "minAppVersion": "1.5.0", "description": "Render 3D models in Obsidian and connect them to linked knowledge notes.", "author": "flash", diff --git a/package.json b/package.json index c62f48f..f76d187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-ai-3d-model-workbench", - "version": "0.1.1", + "version": "0.1.2", "private": true, "description": "Obsidian plugin for viewing 3D assets, annotating them, and linking them to knowledge notes.", "scripts": { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7b1a2da..208956e 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -176,6 +176,8 @@ export const en = { "codeBlock.presetRequiresModels": "Preset \"{preset}\" requires {min}-{max} models, got {count}.", "codeBlock.gridFailed": "Grid failed: {reason}", "loading.default": "Loading...", + "loading.preparingModel": "Preparing model...", + "loading.loadingModel": "Loading model...", } as const; export type TranslationKey = keyof typeof en; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 73a4558..1ae2166 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -11,9 +11,13 @@ const dictionaries: Record> = { }; let currentLocale: Locale = "en"; +const localeListeners = new Set<() => void>(); export function setLocale(locale: Locale): void { currentLocale = locale; + for (const listener of localeListeners) { + listener(); + } } export function getLocale(): Locale { @@ -24,6 +28,13 @@ export function t(key: TranslationKey): string { return dictionaries[currentLocale]?.[key] ?? en[key] ?? key; } +export function onLocaleChange(listener: () => void): () => void { + localeListeners.add(listener); + return () => { + localeListeners.delete(listener); + }; +} + export function interpolate(template: string, values: Record): string { return template.replace(/\{(\w+)\}/g, (_, key: string) => values[key] ?? ""); } diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 3cddc32..bb36f94 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -178,4 +178,6 @@ export const zhCN: Record = { "codeBlock.presetRequiresModels": "预设 “{preset}” 需要 {min}-{max} 个模型,当前为 {count} 个。", "codeBlock.gridFailed": "网格渲染失败:{reason}", "loading.default": "加载中...", + "loading.preparingModel": "正在准备模型...", + "loading.loadingModel": "正在加载模型...", }; diff --git a/src/view/direct-view.ts b/src/view/direct-view.ts index 033d6b7..e57ab4e 100644 --- a/src/view/direct-view.ts +++ b/src/view/direct-view.ts @@ -142,7 +142,7 @@ export class DirectModelView extends FileView { const settings = this.getSettings(); const conversionManager = createConversionManager(settings); const absolutePath = resolveVaultAbsolutePath(this.app, file.path) ?? undefined; - loading.setPhase("Preparing model..."); + loading.setPhaseKey("loading.preparingModel"); const prepared = await prepareModelInput({ path: file.path, absolutePath, @@ -154,7 +154,7 @@ export class DirectModelView extends FileView { const source = toPreviewSource(prepared); this.preview = new BabylonModelPreview(canvas); - loading.setPhase("Loading model..."); + loading.setPhaseKey("loading.loadingModel"); const data = await readBinaryPath(this.app, source.path); if (gen !== this.loadGeneration) { this.preview.destroy(); this.preview = null; return; } console.debug(`[AI3D] DirectView loading: ${file.path} via ${source.path} (${source.ext}, ${data.byteLength} bytes)`); diff --git a/src/view/inline/code-block.ts b/src/view/inline/code-block.ts index a4d4873..012d9c1 100644 --- a/src/view/inline/code-block.ts +++ b/src/view/inline/code-block.ts @@ -246,7 +246,7 @@ export function registerCodeBlockProcessor( try { const absolutePath = resolveVaultAbsolutePath(app, modelPath) ?? undefined; const conversionManager = createConversionManager(settings); - loading.setPhase("Preparing model..."); + loading.setPhaseKey("loading.preparingModel"); const prepared = await prepareModelInput({ path: modelPath, absolutePath, @@ -256,7 +256,7 @@ export function registerCodeBlockProcessor( }); const source = toPreviewSource(prepared); preview = new BabylonModelPreview(canvas); - loading.setPhase("Loading model..."); + loading.setPhaseKey("loading.loadingModel"); const data = await readBinaryPath(app, source.path); const readFile = async (p: string) => readBinaryPath(app, p); @@ -500,7 +500,7 @@ export function registerGridCodeBlockProcessor( async function loadGrid() { if (loaded || destroyed) return; loaded = true; - gridLoading.setPhase(t("codeBlock.renderingGrid")); + gridLoading.setPhaseKey("codeBlock.renderingGrid"); gridLoading.setProgress(-1); try { diff --git a/src/view/inline/live-preview.ts b/src/view/inline/live-preview.ts index 366b68d..44882c1 100644 --- a/src/view/inline/live-preview.ts +++ b/src/view/inline/live-preview.ts @@ -109,7 +109,7 @@ class ModelEmbedWidget extends WidgetType { fbx2gltfCommand: this.fbx2gltfCommand, freecadcmdCommand: this.freecadcmdCommand, }); - loading.setPhase("Preparing model..."); + loading.setPhaseKey("loading.preparingModel"); const prepared = await prepareModelInput({ path: this.modelPath, absolutePath, @@ -120,7 +120,7 @@ class ModelEmbedWidget extends WidgetType { conversionManager, convertedAssetCache: this.convertedAssetCache, }); - loading.setPhase("Loading model..."); + loading.setPhaseKey("loading.loadingModel"); const data = await readBinaryPath(this.app, prepared.effectivePath); await this.preview.loadModel( data, diff --git a/src/view/inline/loading-overlay.ts b/src/view/inline/loading-overlay.ts index 64041b1..2c3e0b5 100644 --- a/src/view/inline/loading-overlay.ts +++ b/src/view/inline/loading-overlay.ts @@ -1,4 +1,4 @@ -import { t } from "../../i18n"; +import { onLocaleChange, t, type TranslationKey } from "../../i18n"; /** * Reusable loading overlay for 3D model preview hosts. @@ -11,6 +11,8 @@ import { t } from "../../i18n"; export interface LoadingOverlay { /** Update the phase description (e.g. "Converting CAD...", "Loading model..."). */ setPhase(text: string): void; + /** Update the phase description from a translation key. */ + setPhaseKey(key: TranslationKey): void; /** Set determinate progress (0–100). Pass -1 for indeterminate mode. */ setProgress(percent: number): void; /** Fade out and remove the overlay from DOM. Safe to call multiple times. */ @@ -25,18 +27,35 @@ export function createLoadingOverlay(host: HTMLElement): LoadingOverlay { overlay.createDiv({ cls: "ai3d-loading-spinner" }); const text = overlay.createDiv({ cls: "ai3d-loading-text" }); - text.textContent = t("loading.default"); + let currentPhaseKey: TranslationKey | null = "loading.default"; + let currentPhaseText = ""; + const renderPhase = () => { + text.textContent = currentPhaseKey ? t(currentPhaseKey) : currentPhaseText; + }; + renderPhase(); const track = overlay.createDiv({ cls: "ai3d-loading-bar-track" }); const fill = track.createDiv({ cls: "ai3d-loading-bar-fill is-indeterminate" }); let hidden = false; + const stopListening = onLocaleChange(() => { + if (!hidden && currentPhaseKey) { + renderPhase(); + } + }); return { el: overlay, - setPhase(t: string) { - if (!hidden) text.textContent = t; + setPhase(phaseText: string) { + currentPhaseKey = null; + currentPhaseText = phaseText; + if (!hidden) renderPhase(); + }, + + setPhaseKey(phaseKey: TranslationKey) { + currentPhaseKey = phaseKey; + if (!hidden) renderPhase(); }, setProgress(pct: number) { @@ -53,6 +72,7 @@ export function createLoadingOverlay(host: HTMLElement): LoadingOverlay { hide() { if (hidden) return; hidden = true; + stopListening(); overlay.classList.add("is-hiding"); activeWindow.setTimeout(() => overlay.remove(), 300); }, diff --git a/versions.json b/versions.json index 56c2833..1b5e687 100644 --- a/versions.json +++ b/versions.json @@ -1 +1 @@ -{"0.0.1":"1.5.0","0.0.2":"1.5.0","0.0.3":"1.5.0","0.0.4":"1.5.0","0.0.5-beta.0":"1.5.0","0.1.1":"1.5.0"} +{"0.0.1":"1.5.0","0.0.2":"1.5.0","0.0.3":"1.5.0","0.0.4":"1.5.0","0.0.5-beta.0":"1.5.0","0.1.1":"1.5.0","0.1.2":"1.5.0"}