Repository navigation
Conversation
The admin app gained Simplified Chinese in oddbit#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.
DennisAlund
left a comment
There was a problem hiding this comment.
Review of the Simplified Chinese locale for the browser extension. yarn test (165 cases) and yarn typecheck pass on the head commit. Inline comments cover the findings that land on diff lines.
One finding sits outside the diff hunks, so it goes here.
browser-extensions/src/i18n/index.ts:46: interpolation treats $ sequences in a parameter as replacement patterns. value.replace(regex, String(v)) passes a string replacement, so $&, $', $` and $1 in the value expand instead of printing. error.validation is a bare {message} passthrough for server text (ConfigForm.tsx:144, Popup.tsx:93), so a server message such as Invalid character '$' in slug renders as Invalid character ' in slug. The admin app's createTranslateFn in src/i18n/index.ts already passes a replacer, () => String(v), for this reason. This PR brings the extension's language set in line with the admin app, which makes it the place to carry that fix over too.
| * A Traditional locale would need its own entry here and would have to be | ||
| * matched before the primary subtag is taken, which this does not do. | ||
| */ | ||
| export function detectLanguage(): SupportedLanguage { |
There was a problem hiding this comment.
popup.html and options.html both ship <html lang="en">, and nothing sets document.documentElement.lang from the value this function returns. With zh registered, both surfaces now carry Chinese text under an English language tag.
- Screen readers pick the voice from
lang, so VoiceOver and NVDA read the Chinese labels with an English voice, or skip them. - Browsers pick the Han fallback font from
lang. On a system whose default CJK font is Japanese or Traditional (azh-TWreader, whom this PR routes to the Simplified table), Simplified-only glyphs such as 码 and 链 render from a mismatched font. - The options tab title stays the hardcoded English
shrtnr settingsfromoptions.html.
Setting document.documentElement.lang in the popup and options entry points fixes every locale at once. For the zh table, zh-Hans describes the strings on screen better than the browser's own tag. document.title = t("options.title") covers the tab title.
| /** | ||
| * The browser's language decides, by its primary subtag: "zh-CN", "zh-SG" and | ||
| * "zh-Hans" all resolve to "zh". A script variant this set does not carry, | ||
| * "zh-TW" or "zh-HK", also resolves to "zh" and reads the Simplified table. |
There was a problem hiding this comment.
Routing zh-TW, zh-HK and zh-Hant to the Simplified table departs from how the browser resolves extension locales: chrome.i18n treats zh_CN and zh_TW as separate locales and falls back to default_locale rather than crossing scripts. A reader in Taiwan or Hong Kong gets a script they did not choose, and the extension has no picker to undo it.
Deciding the script by the primary subtag also means a later zh-Hant table needs this function rewritten. Matching the full tag first, and sending zh-TW, zh-HK, zh-MO and any zh-Hant* tag to the fallback until a Traditional table exists, keeps both paths open. The case at tests/i18n.test.ts:154 pins the current choice, so it follows whichever way this is decided.
| ## i18n | ||
|
|
||
| Three languages, mirroring the admin app: English, Indonesian, Swedish. English is the source of truth and the fallback. Add a key to `src/i18n/en.ts` first; TypeScript will fail to compile until `id.ts` and `sv.ts` add the same key (the `Translations = typeof en` type is the contract). | ||
| The same languages as the admin app. `src/i18n/index.ts` lists them in `SUPPORTED_LANGUAGES`, which is where a new one is registered: the table map beside it is typed over that list, so a language without a table fails the typecheck. The language is the browser's, read from `navigator.language` by its primary subtag; there is no picker in the extension. English is the source of truth and the fallback. Add a key to `src/i18n/en.ts` first; TypeScript fails to compile until every other table adds the same key (the `Translations = typeof en` type is the contract). |
There was a problem hiding this comment.
"The same languages as the admin app." keeps the cross-package claim the second commit message names as the defect: it reads as false the moment the root src/i18n/index.ts registers a locale the extension lacks, or the reverse, and nothing checks it. Either drop the sentence or back it with a test that imports the admin app's SUPPORTED_LANGUAGES and asserts equality (see the comment on tests/i18n.test.ts:180).
| } | ||
| }); | ||
|
|
||
| it("carries exactly the languages it declares", () => { |
There was a problem hiding this comment.
This assertion hardcodes the list the README now refuses to enumerate. Every new locale edits it by hand, and it never catches the drift that matters: the extension falling behind the admin app. Both lists live in this repo, and the extension's vitest resolves import { SUPPORTED_LANGUAGES as ADMIN_LANGUAGES } from "../../src/i18n" (checked locally). Asserting [...SUPPORTED_LANGUAGES] equals [...ADMIN_LANGUAGES] pins the README claim and retires the literal.
| ## 4. Locale check | ||
|
|
||
| Switch the OS or browser locale to Indonesian, then to Swedish. Reload the popup and options page. | ||
| Switch the OS or browser locale to Indonesian, then Swedish, then Chinese (Simplified). Reload the popup and options page. |
There was a problem hiding this comment.
Root CLAUDE.md, Documentation: "Never enumerate plugins, skills, dependencies, components, or any list backed by a file/folder source of truth. Reference the source directly." This line enumerates the locales SUPPORTED_LANGUAGES holds, the same drift the README edit in this PR removes: the next locale drops out of the release check without anyone noticing. Suggest: "Switch the OS or browser locale to each non-English entry in SUPPORTED_LANGUAGES (src/i18n/index.ts)."
| // The language comes from the browser, with no picker in the extension, so | ||
| // this is the whole path from navigator.language to the strings on screen. | ||
| // Every other case in this file runs against the en-US pin in setup.ts. | ||
| Object.defineProperty(globalThis.navigator, "language", { configurable: true, value: "zh-CN" }); |
There was a problem hiding this comment.
This line repeats the browserLanguage() helper the PR adds at tests/i18n.test.ts:23. Exporting one setBrowserLanguage() from tests/setup.ts, beside the en-US pin it overrides, keeps the property descriptor in one place and puts the re-pin contract next to the pin.
|
|
||
| // Form | ||
| "form.baseUrl.label": "服务器地址", | ||
| "form.baseUrl.placeholder": "https://shrtnr.example.com", |
There was a problem hiding this comment.
en, id and sv all mark the example host as the reader's own (your-shrtnr, shrtnr-anda, din-shrtnr). This placeholder and the example in error.invalidUrl (line 71) drop that cue, so https://shrtnr.example.com reads as a real host to paste. https://your-shrtnr.example.com, or a pinyin form such as https://ni-de-shrtnr.example.com in the style of popup.slug.placeholder, keeps the tables aligned.
| "form.cancel": "取消", | ||
| "form.testing": "正在测试...", | ||
| "form.testOk": "已连接", | ||
| "form.testOkCreateOnly": "已连接。该密钥只能创建链接、不能读取链接,因此二维码需要具备读取权限的密钥。", |
There was a problem hiding this comment.
The 顿号 (、) separates items in a list, not two clauses, so 只能创建链接、不能读取链接 reads as a list of two items. 已连接。该密钥可以创建链接,但不能读取链接,因此二维码需要具备读取权限的密钥。 carries the English contrast ("can create links but not read them").
|
|
||
| Repairs the extension in real browsers, adds the parity features a shortener extension is expected to have, and hardens permissions. No new permissions. | ||
|
|
||
| - **Simplified Chinese.** The popup and the options page follow the browser's language, and Chinese joins English, Indonesian and Swedish so the extension's set matches the admin app's. A browser reporting `zh-TW` or `zh-HK` reads Simplified: the extension carries one Chinese table, and it has no picker to choose another. |
There was a problem hiding this comment.
The blank line after this bullet (line 10) turns the whole Unreleased list into a loose list in CommonMark: every item renders wrapped in <p> with paragraph spacing, unlike the 0.1.1 and 0.1.0 sections. Dropping it keeps the list tight.
| // key, in every language the extension offers. | ||
| const text = createTranslateFn(lang)("error.invalidUrl"); | ||
| expect(text).not.toBe("error.invalidUrl"); | ||
| if (lang !== DEFAULT_LANGUAGE) expect(text).not.toBe(en["error.invalidUrl"]); |
There was a problem hiding this comment.
The comment on translated (line 15) says a change to the default language or the exclusion rule lands in one place, but this case and the save-failure case below re-derive it inline with lang !== DEFAULT_LANGUAGE. Splitting each into it.each(SUPPORTED_LANGUAGES) for the not-the-key assertion and it.each(translated) for the not-English assertion keeps the rule where the comment promises.
What
Follow-up to the review of #71. The admin app gained Simplified Chinese there, and this extension's README promised the admin app's language set, so a browser reporting
zh-CNmet a Chinese dashboard next to an English popup.src/i18n/zh.tscarries the 69 keys the popup, the form, the error messages and the options page read.zhjoinsSUPPORTED_LANGUAGES. The table map beside it is typed over that list, so a language registered without a table fails the typecheck rather than resolving toundefinedat the firstt().zh-CN,zh-SGandzh-Hansall resolve tozh. A script variant the set does not carry,zh-TWorzh-HK, also resolves tozhand reads Simplified: the extension carries one Chinese table and has no picker to choose another.detectLanguage()documents that, and a test pins it as a decision rather than an accident. A Traditional table would have to be matched before the primary subtag is taken, which this does not do.SUPPORTED_LANGUAGES, which is also where a new locale is registered, so the claim cannot drift from the code.Tests
SUPPORTED_LANGUAGESinstead of namingidandsv, so this locale and any later one are covered the day they register. Theaccepts en, id, svcase became every-language acceptance plus an assertion on the exact array, and a new case checks that each table declares its own code in_lang.navigator.languageset tozh-CNand asserts the Chinese labels, which is the whole path from the browser to the strings on screen. Every other case in that file runs against theen-USpin intests/setup.ts.zhfails four cases; dropping a placeholder from thezhtable fails the placeholder parity case.yarn test: 9 files, 165 tests pass.yarn typecheck: clean.yarn build,scripts/verify-build.mjs: both targets build, 7 referenced files present, 45.1 KB zips.The Chinese labels are the widest strings the popup and options page carry, so
RELEASE.mdgains a step that checks them for clipping on those two surfaces before a tag.