diff --git a/packages/app/src/components/reader/TranslationPopover.tsx b/packages/app/src/components/reader/TranslationPopover.tsx
index 1101675e..b424b281 100644
--- a/packages/app/src/components/reader/TranslationPopover.tsx
+++ b/packages/app/src/components/reader/TranslationPopover.tsx
@@ -169,7 +169,7 @@ export function TranslationPopover({ text, position, onClose }: TranslationPopov
const aiConfig = useSettingsStore((s) => s.aiConfig);
const endpointId = translationConfig.provider.endpointId || aiConfig.activeEndpointId;
const endpoint = aiConfig.endpoints.find((e) => e.id === endpointId);
- const providerName = provider === "ai" ? endpoint?.name || "AI" : "DeepL";
+ const providerName = provider === "ai" ? endpoint?.name || "AI" : translationConfig.provider.name;
return (
s.translationConfig);
const aiConfigFromStore = useSettingsStore((s) => s.aiConfig);
const translationConfig = translationConfigOverride || translationConfigFromStore;
@@ -92,6 +97,12 @@ export function useTranslator(options: UseTranslatorOptions = {}) {
apiKey,
translationConfig.provider.baseUrl,
);
+ } else if (providerId === "microsoft") {
+ translatedTexts = await microsoftTranslate(
+ needsTranslation.map((n) => n.text),
+ sourceLang,
+ targetLanguage,
+ );
} else {
throw new Error(`Unknown translation provider: ${providerId}`);
}
diff --git a/packages/core/src/translation/providers.test.ts b/packages/core/src/translation/providers.test.ts
index a63e9c1b..ccaf622b 100644
--- a/packages/core/src/translation/providers.test.ts
+++ b/packages/core/src/translation/providers.test.ts
@@ -1,5 +1,9 @@
-import { describe, expect, it } from "vitest";
-import { buildAITranslationPrompt } from "./providers";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { buildAITranslationPrompt, microsoftTranslate, toMicrosoftLangCode } from "./providers";
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
describe("buildAITranslationPrompt", () => {
it("asks AI to translate classical Chinese into modern vernacular Chinese", () => {
@@ -20,3 +24,40 @@ describe("buildAITranslationPrompt", () => {
expect(prompt).toContain("Do not add any explanation");
});
});
+
+describe("Microsoft translator", () => {
+ it("normalizes Chinese language variants to Microsoft script codes", () => {
+ expect(toMicrosoftLangCode("zh-CN")).toBe("zh-Hans");
+ expect(toMicrosoftLangCode("zh-cn")).toBe("zh-Hans");
+ expect(toMicrosoftLangCode("zh_Hans")).toBe("zh-Hans");
+ expect(toMicrosoftLangCode("zh")).toBe("zh-Hans");
+ expect(toMicrosoftLangCode("zh-TW")).toBe("zh-Hant");
+ expect(toMicrosoftLangCode("zh_hant")).toBe("zh-Hant");
+ expect(toMicrosoftLangCode("ja")).toBe("ja");
+ });
+
+ it("requests Simplified Chinese without an empty source language parameter", async () => {
+ const fetchMock = vi
+ .fn()
+ .mockResolvedValueOnce(new Response("token"))
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify([{ translations: [{ text: "你好" }] }]), {
+ headers: { "Content-Type": "application/json" },
+ }),
+ );
+ vi.stubGlobal("fetch", fetchMock);
+
+ await expect(microsoftTranslate(["hello"], "AUTO", "zh_Hans")).resolves.toEqual(["你好"]);
+
+ const [url, init] = fetchMock.mock.calls[1] as [string, RequestInit];
+ const requestUrl = new URL(url);
+ expect(requestUrl.searchParams.get("api-version")).toBe("3.0");
+ expect(requestUrl.searchParams.get("to")).toBe("zh-Hans");
+ expect(requestUrl.searchParams.has("from")).toBe(false);
+ expect(init.headers).toMatchObject({
+ "Content-Type": "application/json",
+ Authorization: "Bearer token",
+ });
+ expect(init.body).toBe(JSON.stringify([{ Text: "hello" }]));
+ });
+});
diff --git a/packages/core/src/translation/providers.ts b/packages/core/src/translation/providers.ts
index 8bfe8a24..26f3a20a 100644
--- a/packages/core/src/translation/providers.ts
+++ b/packages/core/src/translation/providers.ts
@@ -553,12 +553,18 @@ let _msToken: string | null = null;
let _msTokenExpiry = 0;
/** Language code mapping: our codes → Microsoft API codes */
-function toMicrosoftLangCode(lang: string): string {
- const map: Record = {
- "zh-CN": "zh-Hans",
- "zh-TW": "zh-Hant",
- };
- return map[lang] || lang;
+export function toMicrosoftLangCode(lang: string): string {
+ const normalized = lang.trim().replace(/_/g, "-");
+ const lower = normalized.toLowerCase();
+
+ if (lower === "zh" || lower === "zh-cn" || lower === "zh-sg" || lower === "zh-hans") {
+ return "zh-Hans";
+ }
+ if (lower === "zh-tw" || lower === "zh-hk" || lower === "zh-mo" || lower === "zh-hant") {
+ return "zh-Hant";
+ }
+
+ return normalized;
}
/** Microsoft supported source languages (subset for validation) */
@@ -601,11 +607,18 @@ export async function microsoftTranslate(
// If source lang is "auto"/"AUTO", empty, or not recognized by Microsoft, omit it for auto-detection
const from = (!sourceLang || sourceLang.toLowerCase() === "auto" || !MS_SUPPORTED_LANGS.has(mappedSource)) ? "" : mappedSource;
const to = toMicrosoftLangCode(targetLang);
+ const params = new URLSearchParams({
+ "api-version": "3.0",
+ to,
+ });
+ if (from) {
+ params.set("from", from);
+ }
const body = texts.map((t) => ({ Text: t }));
const resp = await fetch(
- `https://api-edge.cognitive.microsofttranslator.com/translate?from=${from}&to=${to}&api-version=3.0`,
+ `https://api-edge.cognitive.microsofttranslator.com/translate?${params.toString()}`,
{
method: "POST",
headers: {