Keep translation details out of startup path

This commit is contained in:
vorotamoroz
2026-07-25 04:34:53 +00:00
parent 99eb9cd46f
commit bea0e68091
4 changed files with 198 additions and 14 deletions
+69 -14
View File
@@ -1,4 +1,4 @@
import { getLanguage, requireApiVersion } from "@/deps";
import { getLanguage, Notice, requireApiVersion } from "@/deps";
import { createServiceFeature } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta";
import { $msg, __onMissingTranslation, setLang } from "@/common/translation";
@@ -15,7 +15,40 @@ function tryGetLanguage(onError: (error: unknown) => void) {
return "en";
}
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
class ObsidianLanguageAppliedNotice {
private reminder: Notice | undefined;
show(openDetails: () => void): void {
this.clear();
let reminderAnchor: HTMLAnchorElement | undefined;
const appliedMessage =
$msg("dialog.yourLanguageAvailable")
.split(/\r?\n\s*\r?\n/u, 1)[0]
?.trim() ?? $msg("Display Language");
const fragment = createFragment((documentFragment) => {
documentFragment.createSpan({
text: `${appliedMessage} `,
});
documentFragment.createEl("a", { text: $msg("Open the dialog") }, (anchor) => {
reminderAnchor = anchor;
anchor.addEventListener("click", (event) => {
event.preventDefault();
this.clear();
openDetails();
});
});
});
this.reminder = new Notice(fragment, 0);
reminderAnchor?.closest<HTMLElement>(".notice")?.classList.add("livesync-language-applied-notice");
}
clear(): void {
this.reminder?.hide();
this.reminder = undefined;
}
}
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API, appLifecycle } }) => {
// Clear missing translation handler to avoid unnecessary warnings.
__onMissingTranslation(() => {});
let isChanged = false;
@@ -36,26 +69,48 @@ export const enableI18nFeature = createServiceFeature(async ({ services: { setti
// settings.displayLanguage = obsidianLanguage as I18N_LANGS;
await setting.applyPartial({ displayLanguage: obsidianLanguage as I18N_LANGS });
isChanged = true;
setLang(settings.displayLanguage);
setLang(obsidianLanguage as I18N_LANGS);
} else if (settings.displayLanguage == "") {
// settings.displayLanguage = "def";
await setting.applyPartial({ displayLanguage: "def" });
setLang(settings.displayLanguage);
setLang("def");
await setting.saveSettingData();
}
}
if (isChanged) {
const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault");
if (
(await API.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], {
defaultAction: "OK",
title: $msg(`dialog.yourLanguageAvailable.Title`),
})) == revert
) {
await setting.applyPartial({ displayLanguage: "def" });
setLang(settings.displayLanguage);
}
await setting.saveSettingData();
const reminder = new ObsidianLanguageAppliedNotice();
appLifecycle.onUnload.addHandler(() => {
reminder.clear();
return Promise.resolve(true);
});
reminder.show(() => {
void (async () => {
try {
const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault");
if (
(await API.confirm.askSelectStringDialogue(
$msg(`dialog.yourLanguageAvailable`),
["OK", revert],
{
defaultAction: "OK",
title: $msg("Display Language"),
}
)) == revert
) {
await setting.applyPartial({ displayLanguage: "def" });
setLang("def");
await setting.saveSettingData();
}
} catch (error) {
API.addLog(
`Failed to open translation details: ${String(error)}`,
LOG_LEVEL_VERBOSE,
"i18n-language"
);
}
})();
});
}
return true;
});
@@ -0,0 +1,110 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const noticeState = vi.hoisted(() => ({
instances: [] as Array<{ hide: ReturnType<typeof vi.fn>; duration: number }>,
spanTexts: [] as string[],
}));
vi.mock("@/deps", () => ({
getLanguage: () => "ja",
requireApiVersion: () => true,
Notice: class {
hide = vi.fn();
constructor(_fragment: unknown, duration: number) {
noticeState.instances.push({ hide: this.hide, duration });
}
},
}));
vi.mock("@/common/translation", () => ({
$msg: (key: string) =>
({
"dialog.yourLanguageAvailable": "Translation has been applied.\n\nMore details.",
"dialog.yourLanguageAvailable.btnRevertToDefault": "Keep Default",
"dialog.yourLanguageAvailable.Title": "Translation is available!",
"Display Language": "Display language",
"Open the dialog": "Open the dialogue",
})[key] ?? key,
__onMissingTranslation: vi.fn(),
setLang: vi.fn(),
}));
import { enableI18nFeature } from "./enablei18n.ts";
describe("automatic display language", () => {
let clickDetails: ((event: { preventDefault(): void }) => void) | undefined;
beforeEach(() => {
noticeState.instances.length = 0;
noticeState.spanTexts.length = 0;
clickDetails = undefined;
vi.stubGlobal("createFragment", (build: (fragment: unknown) => void) => {
const anchor = {
addEventListener: (_event: string, listener: (event: { preventDefault(): void }) => void) => {
clickDetails = listener;
},
closest: () => ({ classList: { add: vi.fn() } }),
};
const fragment = {
createSpan: ({ text }: { text: string }) => noticeState.spanTexts.push(text),
createEl: (_tag: string, _options: unknown, configure: (element: typeof anchor) => void) => {
configure(anchor);
return anchor;
},
};
build(fragment);
return fragment;
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("lets start-up continue and opens translation details only from a persistent Notice", async () => {
const settings = { displayLanguage: "" };
const applyPartial = vi.fn(async (partial: Partial<typeof settings>) => Object.assign(settings, partial));
const saveSettingData = vi.fn().mockResolvedValue(undefined);
const askSelectStringDialogue = vi.fn().mockResolvedValue("Keep Default");
const unloadHandlers: Array<() => Promise<boolean>> = [];
const host = {
services: {
setting: {
currentSettings: () => settings,
applyPartial,
saveSettingData,
},
API: {
addLog: vi.fn(),
confirm: { askSelectStringDialogue },
},
appLifecycle: {
onUnload: {
addHandler: (handler: () => Promise<boolean>) => unloadHandlers.push(handler),
},
},
},
};
await expect(enableI18nFeature(host as never)).resolves.toBe(true);
expect(settings.displayLanguage).toBe("ja");
expect(saveSettingData).toHaveBeenCalledOnce();
expect(askSelectStringDialogue).not.toHaveBeenCalled();
expect(noticeState.instances).toHaveLength(1);
expect(noticeState.instances[0]?.duration).toBe(0);
expect(noticeState.spanTexts).toEqual(["Translation has been applied. "]);
expect(clickDetails).toBeTypeOf("function");
clickDetails?.({ preventDefault: vi.fn() });
await vi.waitFor(() => expect(askSelectStringDialogue).toHaveBeenCalledOnce());
expect(askSelectStringDialogue.mock.calls[0]?.[2]).toMatchObject({ title: "Display language" });
await vi.waitFor(() => expect(settings.displayLanguage).toBe("def"));
expect(saveSettingData).toHaveBeenCalledTimes(2);
await expect(unloadHandlers[0]?.()).resolves.toBe(true);
expect(noticeState.instances[0]?.hide).toHaveBeenCalled();
});
});