fix: localize loading states

This commit is contained in:
flash555588 2026-05-11 19:41:14 +08:00
parent cc9677b9cf
commit 8bfa94475e
10 changed files with 49 additions and 14 deletions

View file

@ -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",

View file

@ -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": {

View file

@ -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;

View file

@ -11,9 +11,13 @@ const dictionaries: Record<Locale, Record<TranslationKey, string>> = {
};
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, string>): string {
return template.replace(/\{(\w+)\}/g, (_, key: string) => values[key] ?? "");
}

View file

@ -178,4 +178,6 @@ export const zhCN: Record<TranslationKey, string> = {
"codeBlock.presetRequiresModels": "预设 “{preset}” 需要 {min}-{max} 个模型,当前为 {count} 个。",
"codeBlock.gridFailed": "网格渲染失败:{reason}",
"loading.default": "加载中...",
"loading.preparingModel": "正在准备模型...",
"loading.loadingModel": "正在加载模型...",
};

View file

@ -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)`);

View file

@ -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 {

View file

@ -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,

View file

@ -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 (0100). 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);
},

View file

@ -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"}