Skip to content

Add Simplified Chinese to the browser extension - #84

Merged
DennisAlund merged 11 commits into
mainfrom
feat/ext-zh-cn
Nov 3, 2026
Merged

DennisAlund merged 11 commits into
mainfrom
feat/ext-zh-cn

Conversation

@DennisAlund

@DennisAlund DennisAlund commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Takes over #78 by @myml and addresses the review on it. The first two commits are #78's commits cherry-picked, authored by the contributor. Two docs conflicts with main were resolved in the cherry-pick: main had since condensed the README and pointed RELEASE.md at SUPPORTED_LANGUAGES. Both resolutions keep main's wording plus #78's additions.

What #78 adds

src/i18n/zh.ts with the popup, form, error and options strings, registered in SUPPORTED_LANGUAGES. The popup and options page follow navigator.language.

Review findings addressed

Finding Fix
$ sequences in a parameter expand as replacement patterns createTranslateFn passes a replacer function, as the admin app does (469ab01).
<html lang="en"> on Chinese text; English tab title Both entry points set <html lang> (zh-Hans for the zh table). Options titles its tab with options.title (7afb05a).
zh-TW/zh-HK/zh-Hant routed to Simplified detectLanguage() reads the script Intl.Locale#maximize() implies. Traditional tags fall back to English, as chrome.i18n does. zh-Hans-HK stays Simplified (7afb05a).
README "same languages as the admin app" unchecked Backed by a test: the extension's list equals the admin app's SUPPORTED_LANGUAGES (b332f70).
Hardcoded language list in the test Replaced by that equality (b332f70).
RELEASE.md enumerates locales Already fixed on main. The cherry-pick keeps main's wording.
getTranslations check weakened to _lang Asserts the table module by reference again; duplicate _lang loop removed (b332f70).
Options page untested in zh New zh-CN case asserts the shrtnr 设置 heading (b332f70).
browserLanguage() duplicated setBrowserLanguage() exported from tests/setup.ts beside the en-US pin (7afb05a).
zh example host drops the "your" cue ni-de-shrtnr.example.com, pinned for every table by a test (fb1e079).
顿号 joins two clauses 可以创建链接,但不能读取链接 (fb1e079).
Loose list in the CHANGELOG Blank line removed (7afb05a).
translated rule re-derived inline Split into it.each(SUPPORTED_LANGUAGES) and it.each(translated) (b332f70).

Second review round

Finding Commit
CLDR sends zh-US, zh-GB and others to Traditional 0c9cc1c: fixed rule. A script subtag wins, and TW/HK/MO are Traditional.
Comment describes a zh-Hant path the matcher cannot take 0c9cc1c: matchLanguageTag() matches language plus script first.
Extension tests import admin source dfaba99: the parity checks move to the root suite, and the extension imports nothing outside its folder.
Order-sensitive set check dfaba99: sorted comparison.
createTranslateFn duplicated dfaba99: one interpolation contract runs against both copies in the root suite.
Admin still tags lang="zh" aa226b2: documentLang() in src/i18n, and the layout uses it.
Language detected twice; Options rebuilds t per render dd08720: the entry point passes lang as a prop, and Options memoizes t.
Entry-point mounts never unmounted; vacuous seed dd08720: afterEach unmount and a sentinel seed.
Em dashes in test titles dd08720
只有…就足以 sentence 87c7253

This PR now touches the admin app: zh pages emit <html lang="zh-Hans">.

Tests

  • Extension: 10 files, 190 pass. yarn typecheck clean. yarn build and yarn verify-build pass for both targets.

  • Root: yarn test 109 files, 1690 pass. tsc --noEmit clean (it now also typechecks the extension's i18n module). yarn e2e 38 pass.

  • Each commit passes the extension suite, both typechecks and the touched root suites on its own.

  • Mutation checks: these fail the suite when broken:

    • the sentinel seed;
    • the root interpolation contract against the extension's copy;
    • set equality, which ignores a reorder but fails when zh is dropped.

    The region rule, matchLanguageTag, the lang prop, the Options memo and the admin documentLang each failed before their fix.

  • Not run in a real browser. RELEASE.md step 4 covers the locale check before a tag.

Supersedes #78.

myml and others added 6 commits November 3, 2026 08:41
The admin app gained Simplified Chinese in #71, and this extension's README
promised the admin app's set, so a browser reporting zh-CN met a Chinese
dashboard beside an English popup. zh.ts carries the 69 keys and joins
SUPPORTED_LANGUAGES, whose table map is typed over the list, so a language
registered without a table fails the typecheck instead of serving undefined.

The language is the browser's, read by its primary subtag, so zh-CN, zh-SG
and zh-Hans all resolve to zh. A script variant the set does not carry,
zh-TW or zh-HK, also resolves to zh and reads Simplified: the extension
carries one Chinese table and has no picker. detectLanguage() now says so,
and a test pins it as a decision rather than an accident.

The parity suites iterate SUPPORTED_LANGUAGES instead of naming id and sv, so
this locale and any later one are covered the day they register, and the
"accepts en, id, sv" case is replaced by every-language acceptance plus an
assertion on the exact array. A new popup case renders with
navigator.language set to zh-CN and asserts the Chinese labels, which is the
whole path from the browser to the strings on screen.

CHANGELOG.md gains the entry under Unreleased, RELEASE.md gains the locale
step and the Chinese clip check on the two narrowest surfaces.
The README enumerated three languages and claimed they mirrored the admin
app, which needed an edit for every locale on either side and read as false
the moment one landed on only one side. It points at SUPPORTED_LANGUAGES
now, which is also where a new locale is registered, and the test summary
line no longer counts the languages it covers.
createTranslateFn passed a string replacement to String#replace, so "$&",
"$'", "$`" and "$1" in a parameter expanded instead of printing.
error.validation is a bare {message} passthrough for server text, so a
server message such as "Invalid character '$' in slug" rendered as
"Invalid character ' in slug". The admin app's createTranslateFn already
passes a replacer function for this reason; the extension now does too.
Review of #78. The extension routed zh-TW, zh-HK and zh-Hant to the
Simplified table by primary subtag. chrome.i18n keeps zh_CN and zh_TW apart
and falls back to default_locale instead of crossing scripts, and the
extension has no picker to undo the choice. detectLanguage() now reads the
script Intl.Locale#maximize() implies: zh-CN and zh-SG write Hans and read
the zh table, while zh-TW, zh-HK, zh-MO and any Hant tag fall back to
English. zh-Hans-HK names its script and stays Simplified.

popup.html and options.html ship lang="en", so Chinese labels sat under
an English tag. Screen readers chose an English voice and browsers chose
the Han fallback font from the system default. Both entry points now set
<html lang> (zh-Hans for the zh table), and the options page titles its
tab with options.title instead of the hardcoded "shrtnr settings".

tests/setup.ts exports setBrowserLanguage() beside the en-US pin it
overrides, and the CHANGELOG entry drops the blank line that made the
Unreleased list loose.
Review of #78.

- The exact-array assertion on SUPPORTED_LANGUAGES becomes equality with
  the admin app's list, which backs the README's "same set as the admin
  app" and fails when either side registers a locale alone.
- getTranslations(lang) is asserted to return the table module by
  reference, restoring the strength of the toBe(id) case the PR replaced
  with a _lang check. The duplicate _lang loop is gone.
- The invalid-URL and save-failure cases split into it.each over every
  language (not the key) and over translated (not English), so the
  default-language rule stays in the translated list.
- Options gets a zh-CN render case. It builds its own t, apart from the
  popup's, and nothing covered it.
- popup.test.tsx uses setBrowserLanguage() instead of its own descriptor.
Review of #78. en, id and sv write your-shrtnr, shrtnr-anda and din-shrtnr
in the base URL placeholder and the invalid-URL example. zh wrote a bare
shrtnr.example.com, which reads as a real host to paste. It now writes
ni-de-shrtnr, in the pinyin style of popup.slug.placeholder. A test pins
the cue for every table and checks the invalid-URL example quotes the
placeholder host.

form.testOkCreateOnly joined two clauses with a 顿号, which separates list
items, so it read as a list. It now carries the English contrast: 可以创建链接,但不能读取链接.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
shrtnr 87c7253 Nov 03 2026, 01:43 AM

@DennisAlund DennisAlund left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review (xhigh). 13 findings: 11 inline, 2 below because they sit outside the diff. Only the first inline finding changes what users see: CLDR sends several script-less Chinese region tags (zh-US, zh-GB, zh-AU, ...) to Traditional, so those browsers read English. The rest are test hygiene, cleanup, altitude and convention.

Checked: tsc --noEmit clean and vitest run 181 passing in browser-extensions/. Intl.Locale#maximize() behavior tested in Node 24 (ICU 78, CLDR 48).

Outside the diff

  • browser-extensions/src/options/Options.tsx:60: t rebuilt on every render. Options calls createTranslateFn(detectLanguage()) in the component body, so since this PR every state change (loading→ready, shortcut resolved, saved, each ConfigForm re-render) constructs and maximizes an Intl.Locale again. Popup memoizes the same value (Popup.tsx:105). Use useMemo(..., []), a module-level constant, or the entry-point prop suggested on options/index.tsx:9.
  • src/pages/layout.tsx:32: altitude: the admin app still tags the same table lang="zh". The PR's reason for zh-Hans (screen-reader voice, Han font fallback) applies equally to the admin app, which renders the same Simplified table. On a system whose default Han font is Traditional or Japanese, the dashboard renders Simplified strings with the wrong glyph forms while the popup beside it is correct. The script mapping belongs beside the shared language registry, not in the extension alone.

Comment thread browser-extensions/src/i18n/index.ts Resolved
Comment thread browser-extensions/src/i18n/index.ts Resolved
Comment thread browser-extensions/src/i18n/index.ts Resolved
Comment thread browser-extensions/tests/i18n.test.ts Resolved
Comment thread browser-extensions/tests/i18n.test.ts Resolved
Comment thread browser-extensions/src/i18n/index.ts Resolved
Comment thread browser-extensions/src/i18n/zh.ts Resolved
Review of #84. The extension now tags its zh table zh-Hans so screen
readers and Han font fallback match the Simplified strings. The admin app
renders the same table and still emitted <html lang="zh">, so on a system
whose default Han font is Traditional or Japanese the dashboard drew
Simplified strings in the wrong glyph forms. documentLang() sits beside
SUPPORTED_LANGUAGES in src/i18n and the layout uses it.

The layout used to pass the same value to adminClientScriptPath(). It now
keeps the language code for the script URL and the tagged value for the
attribute. admin-assets.test.ts caught the first draft serving the English
script to a zh viewer.
Review of #84. The extension's tests imported ../../src/i18n, so its CI job
and store release depended on admin files under a tsconfig and install
they were never checked against. The extension now imports nothing from
outside browser-extensions/.

src/__tests__/unit/extension-i18n-parity.test.ts imports the extension's
i18n module instead. The root suite has every dependency installed. It
checks three things:

- the language set, compared as a set so a reorder does not fail it;
- the <html lang> tag per language;
- one interpolation contract run against both createTranslateFn copies
  ($ sequences, English fallback, missing key).

The contract stands in for sharing the function. Sharing it would put
admin source in the extension bundle, the coupling the move removes.

The extension's i18n/index.ts drops its DOM dependency so the root
typecheck accepts it: detectLanguage() reads navigator through globalThis,
and applyDocumentLanguage() moves to i18n/document.ts.
Review of #84. detectLanguage() took the script from CLDR likely subtags,
which send zh-US, zh-GB, zh-AU, zh-ID, zh-TH, zh-PH and zh-VN to Hant. Those
browsers read English though the docs promised Simplified outside Taiwan,
Hong Kong and Macau. The mapping also shifts between browser versions.

The rule is now explicit: a script subtag wins, Chinese in TW, HK and MO is
Traditional, and Chinese anywhere else is Simplified.

matchLanguageTag() matches a table registered by language and script
("zh-Hant") before the bare-language table. A Traditional table now plugs
in as the comment describes. Tests run the matcher against a list holding
"zh-Hant" to pin that path before the table exists.
Review of #84.

- Each entry point detects the language once and hands it to Popup or
  Options as a prop. <html lang>, the tab title and the strings on screen
  now come from one value instead of two calls.
- Options memoizes t like Popup. It rebuilt t on every render, which since
  this PR also parsed an Intl.Locale each time. A test counts the
  navigator.language reads per mount.
- document-language.test.tsx unmounts what each entry point renders, which
  cleanup() does not track. It also seeds <html lang> with a sentinel, so
  the English cases prove the entry point wrote "en".
- Test titles drop their em dashes (CLAUDE.md writing rules).
Review of #84. "只有创建权限的密钥就足以缩短链接" mixed 只有…才 ("only if")
with 就足以. It could read as "only a create-scoped key qualifies", which
contradicts the next sentence about adding read for QR codes. It now reads
"仅具备创建权限的密钥即可缩短链接", matching the English "A key with the
create scope is enough".
@DennisAlund

Copy link
Copy Markdown
Member Author

The two review findings outside the diff:

  • Options.tsx:60, t rebuilt on every render: fixed in dd08720. Options memoizes t on its lang prop, as Popup does. A test counts navigator.language reads per mount and expects one.
  • src/pages/layout.tsx:32, the admin still tagged lang="zh": fixed in aa226b2. documentLang() sits beside SUPPORTED_LANGUAGES in src/i18n, and every admin page tags zh as zh-Hans. The root parity suite checks the extension tags each language the same way. The first draft also fed the tagged value to adminClientScriptPath(), which served zh viewers the English script. admin-assets.test.ts caught it, and the layout now keeps the language code for the script URL.

@DennisAlund
DennisAlund merged commit 62fa475 into main Nov 3, 2026
10 checks passed
@DennisAlund
DennisAlund deleted the feat/ext-zh-cn branch November 3, 2026 01:50
DennisAlund added a commit that referenced this pull request Nov 3, 2026
…tches

Patch release for the fixes merged since 0.43.0: the chart axis formats
labels with Intl instead of string glue (#83), the admin's Chinese pages
declare lang="zh-Hans" (#84), and hono and zod pick up published
advisory fixes (#82) alongside the dependency upgrade (#81).

The spec embeds info.version, so the bump refreshes x-spec-hash in all
three SDKs. The emitted spec differs from 0.43.0 in info.version only:
spec change does not affect SDK surface; bump hash to record review.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants