chore(release): bump plugin version to 1.0.1

中文: 将插件版本升至 1.0.1,恢复使用 Obsidian getLanguage() 作为语言来源,并把最低 Obsidian 版本同步提高到 1.8.7。

English: Bumps the plugin to 1.0.1, restores Obsidian getLanguage() as the locale source, and aligns the minimum Obsidian version with API usage.
This commit is contained in:
Dusk 2026-04-30 10:37:37 +08:00
parent 6fb8583feb
commit 3e5ce2c1d2
11 changed files with 67 additions and 75 deletions

View file

@ -26,7 +26,7 @@ Anki Heading Sync 是一个 Obsidian 桌面端插件,用来批量地制作 Ank
### 使用要求
- Obsidian 桌面端 1.5.0 或更高版本
- Obsidian 桌面端 1.8.7 或更高版本
- Anki 桌面端
- Anki 中已安装并启用 AnkiConnect
@ -148,7 +148,7 @@ Currently, the plugin mainly supports four card types:
### Requirements
- Obsidian desktop 1.5.0 or newer
- Obsidian desktop 1.8.7 or newer
- Anki desktop
- AnkiConnect enabled in Anki

View file

@ -35,7 +35,7 @@ Repository evidence was verified against these files and modules:
- The GitHub repository currently shows no published releases, so the first submission release still needs to be created.
- No exact `anki-heading-sync` ID match and no exact `Anki Heading Sync` name match were found in the current upstream Obsidian community-plugins.json.
- manifest.json contains the expected release-facing fields: `id`, `name`, `version`, `minAppVersion`, `description`, `author`, `authorUrl`, and `isDesktopOnly`.
- versions.json currently maps `1.0.0` to `1.5.0`, which matches manifest.json version and minAppVersion.
- versions.json currently maps `1.0.1` to `1.8.7`, which matches manifest.json version and minAppVersion.
- package.json defines the release-relevant commands: `lint`, `test`, `build`, and `build:obsidian`.
- package-lock.json is present, and no yarn.lock was found.
- LICENSE is present and uses MIT.
@ -87,7 +87,7 @@ Based on the current Obsidian docs, the repository must satisfy these release co
## 4. What still needs fixing
- A GitHub release for version `1.0.0` still needs to be created manually, with assets attached.
- A GitHub release for version `1.0.1` still needs to be created manually, with assets attached.
- Manual QA in real Obsidian and real Anki is still required before submission.
- The actual submission PR to obsidianmd/obsidian-releases still needs to be opened.
@ -137,7 +137,7 @@ Run these checks in a real desktop Obsidian vault with real Anki running:
- Run `npm test`.
- Run `npm run build`.
- Verify dist/plugin contains `main.js`, `manifest.json`, and `styles.css` when present.
- Create a GitHub release tag exactly matching `manifest.json.version`, currently `1.0.0`.
- Create a GitHub release tag exactly matching `manifest.json.version`, currently `1.0.1`.
- Upload these assets to the release:
- main.js
- manifest.json
@ -180,5 +180,5 @@ Repository-level release preparation is close to complete, but submission should
- local lint, test, and build validation remain green on the release branch
- manual QA is completed in real Obsidian and real Anki
- the GitHub release for version `1.0.0` exists with the correct assets
- the GitHub release for version `1.0.1` exists with the correct assets
- the submission PR to obsidianmd/obsidian-releases is opened with the correct metadata

36
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,8 @@
{
"id": "anki-heading-sync",
"name": "Anki Heading Sync",
"version": "1.0.0",
"minAppVersion": "1.5.0",
"version": "1.0.1",
"minAppVersion": "1.8.7",
"description": "Focused heading-based Markdown to Anki sync plugin.",
"author": "Dusk",
"authorUrl": "https://github.com/panAtGitHub",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-anki-heading-sync",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-anki-heading-sync",
"version": "1.0.0",
"version": "1.0.1",
"license": "MIT",
"dependencies": {
"markdown-it": "^14.1.0"

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-anki-heading-sync",
"version": "1.0.0",
"version": "1.0.1",
"description": "Focused heading-based Markdown to Anki sync plugin.",
"author": "Dusk",
"main": "dist/plugin/main.js",

View file

@ -1,19 +1,26 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const { getLanguageMock } = vi.hoisted(() => ({
getLanguageMock: vi.fn(() => "en"),
}));
vi.mock("obsidian", () => ({
getLanguage: getLanguageMock,
}));
import { PluginUserError, renderPluginFileFailure, renderPluginFileFailuresInline, renderUnknownUserFacingError, renderUserFacingMessage, renderUserMessage } from "./PluginUserError";
function setNavigatorLanguage(language: string): void {
vi.stubGlobal("navigator", { language });
function setObsidianLanguage(language: string): void {
getLanguageMock.mockReturnValue(language);
}
describe("PluginUserError", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("renders plugin-owned errors in English", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const error = new PluginUserError("errors.currentFileOutOfScope", { filePath: "notes/example.md" });
@ -21,7 +28,7 @@ describe("PluginUserError", () => {
});
it("renders plugin-owned errors in Simplified Chinese", () => {
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
const error = new PluginUserError("errors.currentFileOutOfScope", { filePath: "notes/example.md" });
@ -29,19 +36,19 @@ describe("PluginUserError", () => {
});
it("renders scope-not-configured errors in English and Chinese", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
expect(renderUserMessage(new PluginUserError("errors.runScopeNotConfigured"))).toBe(
"Run scope is not configured. In include mode, select at least one folder before syncing.",
);
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
expect(renderUserMessage(new PluginUserError("errors.runScopeNotConfigured"))).toBe(
"运行范围尚未配置。当前是 include 模式,请至少选择一个文件夹后再同步。",
);
});
it("renders write-back failure summaries with localized detail lines", () => {
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
const error = new PluginUserError(
"errors.writeBack.summary",
@ -62,7 +69,7 @@ describe("PluginUserError", () => {
});
it("renders raw and keyed user-facing messages plus inline failure lists", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
expect(renderUserFacingMessage({ key: "notice.failedSavePluginSettings" })).toBe("Failed to save plugin settings.");
expect(renderUserFacingMessage({ rawMessage: "raw failure" })).toBe("raw failure");
@ -74,14 +81,14 @@ describe("PluginUserError", () => {
});
it("falls back to raw error messages for unknown errors", () => {
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
expect(renderUnknownUserFacingError(new Error("socket closed"), "notice.vaultSyncFailed")).toBe("socket closed");
});
it("falls back to a translated default when the value is not an Error", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
expect(renderUnknownUserFacingError(null, "notice.vaultSyncFailed")).toBe("Vault sync failed.");
});
});
});

View file

@ -1,6 +1,8 @@
import { getLanguage } from "obsidian";
export type PluginLocale = "en" | "zh";
export function resolvePluginLocale(languageCode = getPreferredLanguage()): PluginLocale {
export function resolvePluginLocale(languageCode = getLanguage()): PluginLocale {
const normalizedLanguage = languageCode.toLowerCase();
return normalizedLanguage === "zh" || normalizedLanguage.startsWith("zh-") ? "zh" : "en";
}
@ -8,11 +10,3 @@ export function resolvePluginLocale(languageCode = getPreferredLanguage()): Plug
export function formatList(locale: PluginLocale, items: string[]): string {
return items.join(locale === "zh" ? "、" : ", ");
}
function getPreferredLanguage(): string {
if (typeof navigator !== "undefined" && typeof navigator.language === "string") {
return navigator.language;
}
return "en";
}

View file

@ -173,12 +173,11 @@ type FakeButtonComponentInstance = InstanceType<typeof FakeButtonComponent>;
type FakeContainerElInstance = InstanceType<typeof FakeModal>["contentEl"];
type FakeToggleComponentInstance = InstanceType<typeof FakeToggleComponent>;
function setNavigatorLanguage(language: string): void {
vi.stubGlobal("navigator", { language });
function setObsidianLanguage(language: string): void {
getLanguageMock.mockReturnValue(language);
}
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
@ -204,7 +203,7 @@ function getFooterCountText(contentEl: FakeContainerElInstance): string | undefi
describe("EmptyDeckSelectionModal", () => {
it("renders the modal text in English when Obsidian language is not zh", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B", "Deck C"]);
void modal.openAndGetSelection();
@ -224,7 +223,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("starts with zero selected count and a disabled delete button", () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B", "Deck C"]);
void modal.openAndGetSelection();
@ -236,7 +235,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("selects every candidate when the select-all button is clicked", async () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B", "Deck C"]);
const selectionPromise = modal.openAndGetSelection();
@ -259,7 +258,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("clears every candidate when the clear-all button is clicked", async () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B", "Deck C"]);
void modal.openAndGetSelection();
@ -277,7 +276,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("inverts selected candidates when the invert button is clicked", async () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B", "Deck C"]);
void modal.openAndGetSelection();
@ -296,7 +295,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("enables and disables the delete button as manual selection changes", async () => {
setNavigatorLanguage("en");
setObsidianLanguage("en");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A"]);
void modal.openAndGetSelection();
@ -314,7 +313,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("returns null when cancel is clicked", async () => {
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B"]);
const selectionPromise = modal.openAndGetSelection();
@ -326,7 +325,7 @@ describe("EmptyDeckSelectionModal", () => {
});
it("renders selection text in Simplified Chinese when Obsidian language is zh", async () => {
setNavigatorLanguage("zh");
setObsidianLanguage("zh");
const modal = new EmptyDeckSelectionModal({} as never, ["Deck A", "Deck B"]);
void modal.openAndGetSelection();

View file

@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { getLanguageMock, noticeRecords } = vi.hoisted(() => {
const hoistedGetLanguage = vi.fn(() => "en");
@ -31,10 +31,6 @@ import type { ManualSyncResult } from "@/application/use-cases/manualSyncTypes";
import { NoticeService } from "./NoticeService";
function setNavigatorLanguage(language: string): void {
vi.stubGlobal("navigator", { language });
}
function createManualSyncResult(overrides: Partial<ManualSyncResult> = {}): ManualSyncResult {
return {
scannedFiles: 3,
@ -85,11 +81,6 @@ describe("NoticeService", () => {
noticeRecords.length = 0;
getLanguageMock.mockReset();
getLanguageMock.mockReturnValue("en");
setNavigatorLanguage("en");
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("renders sync summary and deck warnings in English", () => {
@ -111,7 +102,7 @@ describe("NoticeService", () => {
});
it("renders clear-current-file summaries with localized failures", () => {
setNavigatorLanguage("zh");
getLanguageMock.mockReturnValue("zh");
const service = new NoticeService();
service.showClearCurrentFileSummary(createClearResult({
@ -138,4 +129,4 @@ describe("NoticeService", () => {
"Empty-deck cleanup completed: candidates 4, selected 3, deleted 2, skipped 2. Skipped decks: Deck C, Deck D.",
]);
});
});
});

View file

@ -1,3 +1,4 @@
{
"1.0.0": "1.5.0"
}
"1.0.0": "1.8.7",
"1.0.1": "1.8.7"
}