{"_id":"@cheetah-labs/i18n","_rev":"6-fa334188524d49e9b26d30b13a78e18c","name":"@cheetah-labs/i18n","dist-tags":{"latest":"3.0.5"},"versions":{"2.0.0":{"name":"@cheetah-labs/i18n","version":"2.0.0","license":"Apache-2.0","_id":"@cheetah-labs/i18n@2.0.0","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"dist":{"shasum":"4e3d2acf9ddf43fe936a8f5c3ab14dcb7211547c","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-2.0.0.tgz","fileCount":7,"integrity":"sha512-biyne0OyFlTz1RhW20GUyzVv/E+RZXb7Zb3+X99rwWek3HYyWEphsvSHWO7R2rCWFGc8uJzmQWuP05eTiIQ3Sg==","signatures":[{"sig":"MEYCIQDY4239rJhPKkFX9IcxiZ1go5LQcLpmTI91lveb1ho3WQIhANaEmIbeRyGu/L92I2bbWNOO26JzYK5ZYGA43VdX2vAc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":81528},"type":"module","module":"fesm2022/cheetah-labs-i18n.mjs","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"exports":{".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"a47834930b5a40a139ccce52203f01c24af910c7","private":false,"typings":"types/cheetah-labs-i18n.d.ts","_npmUser":{"name":"hsing24","email":"twrandy24@gmail.com"},"repository":{"url":"git+https://github.com/Hsing24/cheetah-i18n.git","type":"git"},"_npmVersion":"11.17.0","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","dependencies":{"tslib":"^2.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.26.2","peerDependencies":{"rxjs":"^7.8.0","@angular/core":">=21.0.0 <22.0.0","@angular/common":">=21.0.0 <22.0.0","@angular/router":">=21.0.0 <22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n_2.0.0_1784003028480_0.3951084321981464","host":"s3://npm-registry-packages-npm-production"}},"3.0.1":{"name":"@cheetah-labs/i18n","version":"3.0.1","license":"Apache-2.0","_id":"@cheetah-labs/i18n@3.0.1","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"dist":{"shasum":"b7904a195da713023aa8fd133bcbccf81908d9e6","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-3.0.1.tgz","fileCount":7,"integrity":"sha512-jn3MksN4lKeVKDlR6rY9OL1AaCJpYY6b4dnHwNTC4LZkVxCtD0eD5HPRIBZsq7tJZ9G78zFNkpRXsyXHxym5GA==","signatures":[{"sig":"MEYCIQC2qaKL+S2y+06IdjVlgHSGNQOdFKEIOutc9ltzWB/O/wIhAP0emqDKnjbfH+7D43vspUcmChYoOwCme7KxXp1+xTUI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":83897},"type":"module","module":"fesm2022/cheetah-labs-i18n.mjs","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"exports":{".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"e4d19a93c8ef9c92386673971a7b64ed9404c8a6","private":false,"typings":"types/cheetah-labs-i18n.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:54704f03-f540-458d-aa4a-004804ac77bb"}},"repository":{"url":"git+https://github.com/Hsing24/cheetah-i18n.git","type":"git"},"_npmVersion":"11.17.0","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","dependencies":{"tslib":"^2.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.26.2","peerDependencies":{"rxjs":"^7.8.0","@angular/core":">=22.0.0 <23.0.0","@angular/common":">=22.0.0 <23.0.0","@angular/router":">=22.0.0 <23.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n_3.0.1_1784186604247_0.904129915669015","host":"s3://npm-registry-packages-npm-production"}},"3.0.2":{"name":"@cheetah-labs/i18n","version":"3.0.2","license":"Apache-2.0","_id":"@cheetah-labs/i18n@3.0.2","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"dist":{"shasum":"039e75659e7206b72087d5f664287a93b16044dd","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-3.0.2.tgz","fileCount":7,"integrity":"sha512-G94TvyMlGKfKFiXoaOlVXg8j+5E9nhInIRB8UhWXVmQt4j/48dXN6nGBeaXgMTB8hZq03WMW7Pl1DteHRlj+Sg==","signatures":[{"sig":"MEUCIQDSnTcAaAFRjKunwElhszVQJKnsAXy5JNM9FG12PUQE1QIgbgpQSbgGOCsYBFLEUfpFJUBjy01YrAIfopFiZbzUXbw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":84305},"type":"module","module":"fesm2022/cheetah-labs-i18n.mjs","engines":{"node":">=22.0.0","pnpm":">=10.0.0"},"exports":{".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"6e5cc9f645f762acf928a356776752d4da3dbc0e","private":false,"typings":"types/cheetah-labs-i18n.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:54704f03-f540-458d-aa4a-004804ac77bb"}},"repository":{"url":"git+https://github.com/Hsing24/cheetah-i18n.git","type":"git"},"_npmVersion":"11.17.0","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","dependencies":{"tslib":"^2.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.26.2","peerDependencies":{"rxjs":"^7.8.0","@angular/core":">=22.0.0 <23.0.0","@angular/common":">=22.0.0 <23.0.0","@angular/router":">=22.0.0 <23.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n_3.0.2_1784188402695_0.343559047895746","host":"s3://npm-registry-packages-npm-production"}},"3.0.3":{"name":"@cheetah-labs/i18n","version":"3.0.3","license":"Apache-2.0","_id":"@cheetah-labs/i18n@3.0.3","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"dist":{"shasum":"1468aaeda549f5ed7c954eafed4526077ad37003","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-3.0.3.tgz","fileCount":7,"integrity":"sha512-5CMYM/q1bxH0uqaf2Qn7AJsvtCtX+fJRpwBP2pjYbio5tfm/PtLpuPhD+kGp6ECDGq433H6uArvevRToQd3Y4A==","signatures":[{"sig":"MEYCIQDFHwr2d6+wP6ifoKsaq6ZqyiuFAELznGXA+Odd1pY7PgIhALcwVVg7jY7YZBZXd1oHPNL4iqnrOMKTlJFC9HxSMo7H","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":90199},"type":"module","module":"fesm2022/cheetah-labs-i18n.mjs","engines":{"node":">=22.0.0","pnpm":">=11.0.0"},"exports":{".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"6be5cfab0d6023ab785ac65edb47b33c7954eccc","private":false,"typings":"types/cheetah-labs-i18n.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:54704f03-f540-458d-aa4a-004804ac77bb"}},"repository":{"url":"git+https://github.com/Hsing24/cheetah-i18n.git","type":"git"},"_npmVersion":"11.17.0","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","dependencies":{"tslib":"^2.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.13.1","peerDependencies":{"rxjs":"^7.8.0","@angular/core":">=22.0.0 <23.0.0","@angular/common":">=22.0.0 <23.0.0","@angular/router":">=22.0.0 <23.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n_3.0.3_1784867908236_0.8362335628851365","host":"s3://npm-registry-packages-npm-production"}},"3.0.4":{"name":"@cheetah-labs/i18n","version":"3.0.4","license":"Apache-2.0","_id":"@cheetah-labs/i18n@3.0.4","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"dist":{"shasum":"72ffc0648e450fb32405fd872b69d4377a4b1b48","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-3.0.4.tgz","fileCount":7,"integrity":"sha512-Aw1o/GBffLF4It6fgTMqT+kuIhpM9OhE8GqnzamAMnqmEs6PTiSUtjz3iQVI9gITwZBmucB5gCiPuOw7OhS4gw==","signatures":[{"sig":"MEQCIBvgVI5TGix1ed9whyqF+Q9kEhkpb+Ik/xrv1MI+JNYWAiAOZgfT4AGGuveA4lLaIRsp0xDp290QEKRKyX62x3opxg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":90533},"type":"module","module":"fesm2022/cheetah-labs-i18n.mjs","engines":{"node":">=22.0.0","pnpm":">=11.0.0"},"exports":{".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"7cb109fddfd172a42946bda0bc50cde3c8a4ba0c","private":false,"typings":"types/cheetah-labs-i18n.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:54704f03-f540-458d-aa4a-004804ac77bb"}},"repository":{"url":"git+https://github.com/Hsing24/cheetah-i18n.git","type":"git"},"_npmVersion":"11.17.0","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","dependencies":{"tslib":"^2.6.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.13.1","peerDependencies":{"rxjs":"^7.8.0","@angular/core":">=22.0.0 <23.0.0","@angular/common":">=22.0.0 <23.0.0","@angular/router":">=22.0.0 <23.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n_3.0.4_1785293084094_0.4566504268970397","host":"s3://npm-registry-packages-npm-production"}},"3.0.5":{"name":"@cheetah-labs/i18n","version":"3.0.5","description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","license":"Apache-2.0","private":false,"type":"module","packageManager":"pnpm@11.13.1","engines":{"node":">=22.0.0","pnpm":">=11.0.0"},"publishConfig":{"registry":"https://registry.npmjs.org/","access":"public"},"repository":{"type":"git","url":"git+https://github.com/Hsing24/cheetah-i18n.git"},"peerDependencies":{"@angular/common":">=22.0.0 <23.0.0","@angular/core":">=22.0.0 <23.0.0","@angular/router":">=22.0.0 <23.0.0","rxjs":"^7.8.0"},"dependencies":{"tslib":"^2.6.0"},"module":"fesm2022/cheetah-labs-i18n.mjs","typings":"types/cheetah-labs-i18n.d.ts","exports":{"./package.json":{"default":"./package.json"},".":{"types":"./types/cheetah-labs-i18n.d.ts","default":"./fesm2022/cheetah-labs-i18n.mjs"}},"sideEffects":false,"gitHead":"a9b92333ab8fdba9bd84f9b1e21d048b8547b899","_id":"@cheetah-labs/i18n@3.0.5","bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"homepage":"https://github.com/Hsing24/cheetah-i18n#readme","_nodeVersion":"26.5.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-dqdwrLoI9rUb7wZRXnkMUfStXPZemWwiPKZ1yz10a8q+TEhb8FMmX/L9VfHEEbAN0d1cgKaLxJqcCiXmurXVhg==","shasum":"db3bedba45b246e21d1558ce9be5536f6ea4fb44","tarball":"https://registry.npmjs.org/@cheetah-labs/i18n/-/i18n-3.0.5.tgz","fileCount":7,"unpackedSize":91331,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDfxm2Pzer697qjbQPuM+Wj9uBYPvHiw6qqjZn8eBpEMAiEAtgb4ktultsP/DsE+5GKJDa3gRZxyPrIr+AHNninQZmo="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:54704f03-f540-458d-aa4a-004804ac77bb"}},"directories":{},"maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/i18n_3.0.5_1785304511439_0.09019918844735342"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-14T04:23:48.364Z","modified":"2026-07-29T05:55:11.770Z","2.0.0":"2026-07-14T04:23:48.641Z","3.0.1":"2026-07-16T07:23:24.384Z","3.0.2":"2026-07-16T07:53:22.834Z","3.0.3":"2026-07-24T04:38:28.381Z","3.0.4":"2026-07-29T02:44:44.281Z","3.0.5":"2026-07-29T05:55:11.599Z"},"bugs":{"url":"https://github.com/Hsing24/cheetah-i18n/issues"},"license":"Apache-2.0","homepage":"https://github.com/Hsing24/cheetah-i18n#readme","repository":{"type":"git","url":"git+https://github.com/Hsing24/cheetah-i18n.git"},"description":"Locale-aware Angular router, switcher, SEO helpers and fallback util shared across cheetah sites.","maintainers":[{"name":"hsing24","email":"twrandy24@gmail.com"}],"readme":"# @cheetah-labs/i18n\n\nAngular 的 locale 共用工具,跨全部 cheetah 客戶站使用:router prefix guard、語言切換器、SEO helper、fallback util。\n\n**只解決 i18n 共用邏輯** —— 不處理 CMS 內容、不負責翻譯字串管理。公開 npm package(npmjs,scope `@cheetah-labs`)。\n\n> 動手改這個 repo 前先讀 [`AGENTS.md`](./AGENTS.md);架構圖見 [`docs/flow.md`](./docs/flow.md)。\n\n## Install\n\n```bash\npnpm add @cheetah-labs/i18n\n```\n\n公開套件可直接安裝,不需要設定私有 registry。\n\n## 版本相容表\n\n| @cheetah-labs/i18n | Angular (peer) | rxjs (peer) | Node (engines) |\n|---|---|---|---|\n| 3.x(latest `3.0.3`) | `>=22.0.0 <23.0.0` | `^7.8.0` | `>=22.0.0` |\n| 2.x | `>=21.0.0 <22.0.0` | `^7.8.0` | `>=22.0.0` |\n\n> 每個 Angular major 對應本套件一個 major;Angular 23 支援將以下一個 major 提供。\n>\n> `engines.node` 對消費端刻意保持寬鬆(`>=22`),與本 repo 開發環境的 Node 26(`.nvmrc`)是兩回事 —— 對已發佈 library 收緊 engines 屬 breaking change,請勿對齊。\n>\n> **npmjs 上實際存在的版本只有 `2.0.0`、`3.0.1`、`3.0.2`、`3.0.3`。** `3.0.0` 因發佈設定問題跳號、npm 上不存在;`0.1.x` / `1.x` 有 git tag 但從未發佈到 npmjs。\n\n## API\n\n### `provideI18n(config)`\n\n```ts\nimport { provideI18n, withLocaleRouting } from '@cheetah-labs/i18n';\nimport { provideRouter } from '@angular/router';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideI18n({\n      locales: ['zh-TW', 'en'],\n      defaultLocale: 'zh-TW',\n      fallbackLocale: 'zh-TW',\n      strategy: 'prefix',\n    }),\n    provideRouter(withLocaleRouting(routes)),\n  ],\n};\n```\n\n`I18nConfig` 欄位：`locales`（必填，順序即 switcher 渲染順序）、`defaultLocale`（必填，須在 `locales` 內）、`fallbackLocale`（選填，預設 = `defaultLocale`）、`strategy`（選填，v1 僅支援 `'prefix'`）。設定不合法時 `provideI18n` 會直接 throw（locales 為空、重複、default/fallback 不在清單內）。\n\n若只想拿到填好預設值的設定物件而不註冊 providers，可單獨呼叫 `resolveI18nConfig(config)`，回傳 `ResolvedI18nConfig`（`locales` 已凍結）。已註冊後也可用 `inject(I18N_CONFIG)` 取得同一份設定。\n\n### `withLocaleRouting(routes)`\n\n把路由樹包裝為「每個 top-level route 都會跑 `localePrefixGuard`」。\n- `/about` → 重導 `/<defaultLocale>/about`\n- `/fr/about`（fr 不在 locales）→ 重導 `/<defaultLocale>/about`\n- `/zh-TW/about`（合法）→ 通過\n\n守衛會掛在 `path: ''` 與 `:locale` 兩層，單次導航會跑兩次；這是刻意的且安全 —— 守衛只依目標 URL 決策且具幂等性，URL 帶合法 locale 後第二次即為 no-op。\n\n搭配 `withLocaleRoutingConfig()` 可套用建議的 Router 設定（`paramsInheritanceStrategy: 'always'`，讓子路由讀得到 `:locale` 參數）：\n\n```ts\nprovideRouter(withLocaleRouting(routes), withLocaleRoutingConfig());\n```\n\n`localePrefixGuard` 亦單獨匯出，需要自行組裝路由樹時可直接使用。\n\n### `<i18n-switcher />`\n\n```html\n<i18n-switcher />\n```\n\nStandalone component，列出所有 locale，點擊切換 URL；自帶 `aria-label=\"Language switcher\"` / `aria-current`。\n\n### 取得目前語系\n\n```ts\nimport { CURRENT_LOCALE, LocaleResolver } from '@cheetah-labs/i18n';\n\n// 注入時快照 — 不會隨後續導航更新\nconst locale = inject(CURRENT_LOCALE);\n\n// reactive — 瀏覽器端切換語系後會跟著變\nconst locale$ = inject(LocaleResolver).current$();\n```\n\n> `CURRENT_LOCALE` 是**注入當下**解析的一次性快照。SSR 每個 request 一個 injector，直接用即可；瀏覽器端長駐應用中，之後切換語系不會改變已注入的值，reactive 場景請改用 `LocaleResolver.current$()`。\n\n### `pickLocalized(field, locale, fallback)`\n\n```ts\nimport { pickLocalized } from '@cheetah-labs/i18n';\n\npickLocalized({ 'zh-TW': '你好', en: 'Hello' }, 'en', 'zh-TW'); // 'Hello'\npickLocalized({ 'zh-TW': '你好' },                'en', 'zh-TW'); // '你好'\npickLocalized('plain string',                     'en', 'zh-TW'); // 'plain string'\npickLocalized(undefined,                          'en', 'zh-TW'); // undefined\n```\n\n> **限制**：`pickLocalized` 以「值是否為 plain object」判斷 i18n 欄位。若 `T` 本身是物件（非 locale map），會被誤判為 i18n map 而回傳 `undefined`；物件型內容請自行依 locale key 存取。\n\n### SEO\n\n```ts\nimport { HreflangService, CanonicalService } from '@cheetah-labs/i18n';\n\nconstructor() {\n  inject(HreflangService).set('https://example.com/{locale}/about');\n  inject(CanonicalService).set('https://example.com/zh-TW/about');\n}\n```\n\n`HreflangService.set(pathTemplate)` 為每個 locale 產生一個 `<link rel=\"alternate\" hreflang>`，並自動補一組指向 `defaultLocale` 的 `x-default`；每次 `set()` 前會清掉自己先前產生的 tag（以 `data-i18n-hreflang` 標記辨識，不會誤刪他人的 link）。\n\n`pathTemplate` 解析規則：含 `{locale}` 佔位符時直接替換；否則嘗試比對 URL 中既有的 locale 區段並替換；兩者皆不中時，dev 模式會 `console.warn` 並原樣輸出。\n\n兩個 service 都另有 `clear()` 可主動移除自己產生的 tag：\n\n```ts\ninject(HreflangService).clear();   // 移除所有 data-i18n-hreflang 標記的 link\ninject(CanonicalService).clear();  // 移除 <link rel=\"canonical\">\n```\n\n`CanonicalService.set(href)` 具幂等性：已存在 canonical link 時只更新 `href`，不會重複插入。\n\n### SSR locale resolver\n\n```ts\n// server.ts (Express)\nimport { withSsrLocaleResolver, provideI18n } from '@cheetah-labs/i18n';\n\napp.use('*', (req, res) => {\n  bootstrapApplication(AppComponent, {\n    providers: [\n      provideI18n({ locales: ['zh-TW', 'en'], defaultLocale: 'zh-TW' }),\n      withSsrLocaleResolver(req),\n    ],\n  });\n});\n```\n\n`CURRENT_LOCALE` 解析優先序：\n\n- **SSR**（`withSsrLocaleResolver`）：URL prefix → `Accept-Language` → defaultLocale\n- **瀏覽器**（`provideI18n` 內建）：URL prefix → defaultLocale\n\n`withSsrLocaleResolver(req)` 接受的 `req` 只需符合 `SsrLocaleRequest`（`{ url?, headers? }`），與 Express `req` 相容。若想在 provider 之外自行解析，可直接呼叫 `resolveSsrLocale(req, resolvedConfig)`。\n\n### Accept-Language 解析工具\n\nSSR resolver 內部使用、亦對外匯出，可單獨用於自訂解析邏輯：\n\n```ts\nimport { parseAcceptLanguage, pickPreferredLocale } from '@cheetah-labs/i18n';\nimport type { AcceptLanguageEntry } from '@cheetah-labs/i18n';\n\nparseAcceptLanguage('en;q=0.8, zh-TW');            // 依 q 值排序的 entries\npickPreferredLocale(header, ['zh-TW', 'en'], 'zh-TW');\n```\n\n`pickPreferredLocale` 的比對順序：完全相符 → primary subtag 相符（如 `zh` → `zh-TW`）→ 回傳傳入的 default。\n\n## Develop\n\n前置需求：Node **26**（`.nvmrc`）、pnpm **11.13.1**（`packageManager`）。\n\n```bash\nsource ~/.nvm/nvm.sh && nvm use\npnpm install\npnpm test          # Jest（CI 用 pnpm test:ci）\npnpm lint\npnpm type-check\npnpm build         # ng-packagr → ./dist\npnpm clean         # rimraf dist\n```\n\n> 發佈用的 `engines.node` 刻意維持寬鬆的 `>=22.0.0`，與開發環境的 Node 26 是兩回事 —— 已發佈 library 收緊 engines 屬 breaking change，請勿對齊。\n\n## Publish\n\n版本管理由 **release-please** 自動化：\n\n1. conventional commits merge 進 `main`\n2. release-please 自動開 / 更新 Release PR（version bump + `CHANGELOG.md`）\n3. Release PR merge 後自動打 `vX.Y.Z` tag 並建立 GitHub Release\n4. tag 觸發 `.github/workflows/publish.yml` → 驗證 tag 與 `package.json` 版本一致 → publish 到 npmjs\n\n不手動 bump version、不手動編輯 `CHANGELOG.md`、不手動打 tag。發佈驗證使用 npm Trusted Publishing（OIDC，需在 npmjs 套件設定對應此 repo/workflow 的 Trusted Publisher）；workflow 未接 token fallback，OIDC 是唯一發佈憑證。本 repo 為 private、npm 套件為 public——npm 對 private repo 不會產生 provenance attestations，此為專案定案的發佈模式。\n\nrelease-please 以 `include-component-in-tag: false` 產生 `vX.Y.Z` 形式的 tag；`publish.yml` 只接 `v*`，且會先驗證 tag 與 `package.json` 版本一致才發佈。\n\n**發佈不會自動送達消費端。** 新版上線後,`cheetah-template-website` 與 `cheetah-platform` **各有三處**要手動改:package.json 版號、`pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude`、`unlink:i18n` script 內硬寫的還原版號。漏掉第二項時 pnpm 的 release-age gate 會靜默擋住新版。完整 SOP 見根目錄 `HANDBOOK.md` §6。\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}