From c007518ac224a6f5928efe5bdc805bf0db5c5235 Mon Sep 17 00:00:00 2001 From: wurongjie Date: Thu, 24 Sep 2026 17:42:19 +0800 Subject: [PATCH 1/6] feat(i18n): add Simplified Chinese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zh.ts carries all 375 keys and joins SUPPORTED_LANGUAGES, which is the only registration the settings picker, the per-language client script and the language clamping need. Every locale gains the lang.zh and langLocal.zh names the picker shows, and the README feature line and the assets.ts comment (one script per language, built at module init) follow. Placeholder and key parity now iterate SUPPORTED_LANGUAGES instead of a hardcoded id/sv list, so this locale and any later one are checked the day they register. New assertions pin zh where the runtime does the work: Intl.DisplayNames resolves 美国, Intl.NumberFormat groups 1,234,567, toLocaleDateString renders 7月, the picker labels the other languages in Chinese, and a lang=zh page links and serves its own client script. --- README.md | 2 +- src/__tests__/handler/admin-assets.test.ts | 13 + src/__tests__/page/settings-page.test.ts | 13 + src/__tests__/service/admin-service.test.ts | 3 +- src/__tests__/unit/big-chart.test.ts | 16 + src/__tests__/unit/country.test.ts | 7 + src/__tests__/unit/format.test.ts | 7 + src/__tests__/unit/i18n.test.ts | 35 +- src/assets.ts | 5 +- src/i18n/en.ts | 2 + src/i18n/id.ts | 2 + src/i18n/index.ts | 5 +- src/i18n/sv.ts | 2 + src/i18n/zh.ts | 410 ++++++++++++++++++++ 14 files changed, 506 insertions(+), 16 deletions(-) create mode 100644 src/i18n/zh.ts diff --git a/README.md b/README.md index 7c660b31..4d940e53 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The rest of the shortener: - **Labels from the page title.** A link created without a label gets the destination page's title fetched in the background, with private and internal hosts refused. - **QR codes** as SVG for any slug, from the admin UI, the API, the SDKs and MCP. Scans are tracked separately from link clicks. - **Edge redirects.** Slug lookups are cached in Workers KV in front of D1, and click recording runs after the redirect is sent. -- **Admin dashboard** in English, Indonesian and Swedish, with three themes, per-link and per-bundle analytics, API key management and settings. +- **Admin dashboard** in English, Indonesian, Swedish and Simplified Chinese, with three themes, per-link and per-bundle analytics, API key management and settings. - **Browser extension** for Chrome and Firefox that shortens the current tab against your own deployment. Source and store status in [browser-extensions/README.md](browser-extensions/README.md). - **One-click deploy** with automatic provisioning of the database and KV namespace, and migrations that the Worker applies on its first request. diff --git a/src/__tests__/handler/admin-assets.test.ts b/src/__tests__/handler/admin-assets.test.ts index a18b25e4..dfb490cd 100644 --- a/src/__tests__/handler/admin-assets.test.ts +++ b/src/__tests__/handler/admin-assets.test.ts @@ -10,6 +10,7 @@ import { SELF } from "cloudflare:test"; import { applyMigrations, resetData } from "../setup"; import { adminStyles } from "../../styles"; import id from "../../i18n/id"; +import zh from "../../i18n/zh"; function req(path: string, headers: Record = {}): Request { return new Request(`https://shrtnr.test${path}`, { headers }); @@ -85,7 +86,19 @@ describe("GET /_/assets/*", () => { it("gives each language its own script URL", async () => { const en = (await pageHtml("/_/admin/settings")).match(SCRIPT)![1]; const sv = (await pageHtml("/_/admin/settings", { Cookie: "lang=sv" })).match(SCRIPT)![1]; + const zh = (await pageHtml("/_/admin/settings", { Cookie: "lang=zh" })).match(SCRIPT)![1]; expect(en).not.toBe(sv); + expect(en).not.toBe(zh); + expect(sv).not.toBe(zh); + }); + + it("serves the Simplified Chinese client script to a lang=zh viewer", async () => { + const html = await pageHtml("/_/admin/settings", { Cookie: "lang=zh" }); + const m = html.match(SCRIPT); + expect(m![2]).toBe("zh"); + const res = await SELF.fetch(req(m![1])); + expect(res.status).toBe(200); + expect(await res.text()).toContain(zh["client.themeUpdated"]); }); it("serves this build's content for a hash from another build, uncached, so a deploy window never leaves a page unstyled", async () => { diff --git a/src/__tests__/page/settings-page.test.ts b/src/__tests__/page/settings-page.test.ts index 58a7d914..1f6beda8 100644 --- a/src/__tests__/page/settings-page.test.ts +++ b/src/__tests__/page/settings-page.test.ts @@ -66,6 +66,19 @@ describe("Settings page analytics filter toggles", () => { }); }); +describe("Settings page language picker", () => { + it("offers Simplified Chinese and names the other languages in the viewer's language", async () => { + const res = await SELF.fetch( + new Request("https://shrtnr.test/_/admin/settings", { headers: { Cookie: "lang=zh" } }), + ); + const html = await res.text(); + expect(html).toMatch(/