@jant/core 0.3.46 → 0.3.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/db/execute-file.js +12 -4
- package/bin/commands/db/rehearse.js +2 -2
- package/bin/commands/export.js +12 -4
- package/bin/commands/import-site.js +60 -267
- package/bin/commands/migrate.js +36 -69
- package/bin/commands/reset-password.js +10 -4
- package/bin/commands/site/export.js +59 -248
- package/bin/commands/site/snapshot/export.js +58 -45
- package/bin/commands/site/snapshot/import.js +104 -52
- package/bin/lib/node-env.js +100 -0
- package/bin/lib/runtime-target.js +64 -0
- package/bin/lib/site-snapshot.js +185 -54
- package/bin/lib/sql-export.js +19 -2
- package/dist/app-DU7dpJID.js +6 -0
- package/dist/{app-DB-P66E5.js → app-DdnIoX7y.js} +333 -191
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/client-BoUn7xBo.css +2 -0
- package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
- package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
- package/dist/github-sync-C593r22F.js +4 -0
- package/dist/github-sync-bL1hnx3Q.js +428 -0
- package/dist/index.js +3 -2
- package/dist/node.js +5 -4
- package/package.json +3 -2
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
- package/src/client/components/jant-settings-general.ts +164 -22
- package/src/client/components/settings-types.ts +4 -6
- package/src/client-auth.ts +1 -1
- package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
- package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
- package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
- package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
- package/src/db/migrations/meta/0021_snapshot.json +2121 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
- package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +21 -26
- package/src/db/rehearsal-fixtures/demo-current.json +1 -1
- package/src/db/schema.ts +16 -20
- package/src/i18n/__tests__/middleware.test.ts +43 -1
- package/src/i18n/coverage.generated.ts +17 -0
- package/src/i18n/i18n.ts +18 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/settings/en.po +16 -11
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +17 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +16 -11
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +84 -2
- package/src/i18n/middleware.ts +25 -16
- package/src/i18n/supported-locales.ts +153 -0
- package/src/lib/__tests__/csp-builder.test.ts +19 -2
- package/src/lib/__tests__/feed.test.ts +242 -1
- package/src/lib/__tests__/post-meta.test.ts +0 -1
- package/src/lib/__tests__/view.test.ts +0 -1
- package/src/lib/api-posts.ts +9 -7
- package/src/lib/csp-builder.ts +28 -10
- package/src/lib/feed.ts +153 -3
- package/src/middleware/__tests__/secure-headers.test.ts +89 -0
- package/src/middleware/auth.ts +1 -1
- package/src/middleware/secure-headers.ts +47 -1
- package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
- package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
- package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
- package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
- package/src/node/__tests__/cli-sql-export.test.ts +49 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +8 -2
- package/src/routes/api/__tests__/settings.test.ts +3 -2
- package/src/routes/api/github-sync.tsx +1 -1
- package/src/routes/api/settings.ts +4 -1
- package/src/routes/auth/signin.tsx +6 -0
- package/src/routes/pages/archive.tsx +4 -2
- package/src/services/__tests__/post.test.ts +19 -19
- package/src/services/__tests__/search.test.ts +0 -1
- package/src/services/__tests__/settings.test.ts +22 -3
- package/src/services/bootstrap.ts +7 -3
- package/src/services/collection.ts +3 -3
- package/src/services/export.ts +0 -3
- package/src/services/navigation.ts +0 -2
- package/src/services/path.ts +1 -38
- package/src/services/post.ts +32 -66
- package/src/services/search.ts +0 -6
- package/src/services/settings.ts +47 -6
- package/src/services/site-admin.ts +6 -1
- package/src/styles/ui.css +14 -25
- package/src/types/entities.ts +0 -1
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/dash/settings/GeneralContent.tsx +17 -19
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
- package/src/ui/feed/NoteCard.tsx +1 -11
- package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
- package/src/ui/pages/PostPage.tsx +2 -0
- package/bin/commands/collections.js +0 -268
- package/bin/commands/media.js +0 -302
- package/bin/commands/posts.js +0 -262
- package/bin/commands/search.js +0 -53
- package/bin/commands/settings.js +0 -93
- package/bin/lib/http-api.js +0 -223
- package/bin/lib/media-upload.js +0 -206
- package/dist/app-CM7sb3xO.js +0 -5
- package/dist/client/_assets/client-DDs6NzB3.css +0 -2
- package/src/__tests__/bin/content-cli.test.ts +0 -179
- package/src/__tests__/bin/media-cli.test.ts +0 -192
- /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
- /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
|
@@ -368,11 +368,6 @@ msgstr "連線後會將您的網站同步到 {repo} 的預設分支,並疊加
|
|
|
368
368
|
msgid "Connecting…"
|
|
369
369
|
msgstr "連線中…"
|
|
370
370
|
|
|
371
|
-
#. @context: Help text under the site language select
|
|
372
|
-
#: src/ui/dash/settings/GeneralContent.tsx
|
|
373
|
-
msgid "Controls the language of the dashboard and settings. Public pages stay in English."
|
|
374
|
-
msgstr "控制儀表板與設定的語言。公開頁面保持英文。"
|
|
375
|
-
|
|
376
371
|
#. @context: Feedback after copying API token
|
|
377
372
|
#: src/ui/dash/settings/ApiTokensContent.tsx
|
|
378
373
|
msgid "Copied"
|
|
@@ -796,11 +791,6 @@ msgstr "安裝 GitHub App"
|
|
|
796
791
|
msgid "Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub."
|
|
797
792
|
msgstr "安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。"
|
|
798
793
|
|
|
799
|
-
#. @context: Settings group label for third-party integrations
|
|
800
|
-
#: src/ui/dash/settings/SettingsRootContent.tsx
|
|
801
|
-
msgid "Integrations"
|
|
802
|
-
msgstr "整合"
|
|
803
|
-
|
|
804
794
|
#. @context: Nav item label field
|
|
805
795
|
#: src/ui/dash/appearance/NavigationContent.tsx
|
|
806
796
|
msgid "Label"
|
|
@@ -816,7 +806,7 @@ msgstr "標籤與 URL 為必填"
|
|
|
816
806
|
msgid "Label is required"
|
|
817
807
|
msgstr "標籤為必填"
|
|
818
808
|
|
|
819
|
-
#. @context: Settings form field for site/
|
|
809
|
+
#. @context: Settings form field for site/admin language
|
|
820
810
|
#: src/ui/dash/settings/GeneralContent.tsx
|
|
821
811
|
msgid "Language"
|
|
822
812
|
msgstr "語言"
|
|
@@ -1029,6 +1019,11 @@ msgstr "尚未有自訂 URL。建立一個自訂 URL 以為文章新增重新導
|
|
|
1029
1019
|
msgid "No file selected. Choose a file to upload."
|
|
1030
1020
|
msgstr "未選取檔案。請選擇要上傳的檔案。"
|
|
1031
1021
|
|
|
1022
|
+
#. @context: Empty state shown when the language search filters out every entry
|
|
1023
|
+
#: src/ui/dash/settings/GeneralContent.tsx
|
|
1024
|
+
msgid "No matches."
|
|
1025
|
+
msgstr "無相符結果。"
|
|
1026
|
+
|
|
1032
1027
|
#. @context: Empty state for navigation items
|
|
1033
1028
|
#: src/ui/dash/appearance/NavigationContent.tsx
|
|
1034
1029
|
msgid "No navigation items yet. Add links or enable system items below."
|
|
@@ -1278,6 +1273,11 @@ msgstr "搜尋儲存庫"
|
|
|
1278
1273
|
msgid "Search settings updated."
|
|
1279
1274
|
msgstr "搜尋設定已更新。"
|
|
1280
1275
|
|
|
1276
|
+
#. @context: Placeholder inside the language combobox search field
|
|
1277
|
+
#: src/ui/dash/settings/GeneralContent.tsx
|
|
1278
|
+
msgid "Search…"
|
|
1279
|
+
msgstr "搜尋…"
|
|
1280
|
+
|
|
1281
1281
|
#. @context: Settings group label for account security settings
|
|
1282
1282
|
#: src/ui/dash/settings/AccountMenuContent.tsx
|
|
1283
1283
|
msgid "Security"
|
|
@@ -1325,6 +1325,11 @@ msgstr "工作階段"
|
|
|
1325
1325
|
msgid "Sessions and password"
|
|
1326
1326
|
msgstr "工作階段與密碼"
|
|
1327
1327
|
|
|
1328
|
+
#. @context: Help text under the site language input
|
|
1329
|
+
#: src/ui/dash/settings/GeneralContent.tsx
|
|
1330
|
+
msgid "Sets the content language announced to readers (HTML lang, RSS) and the dashboard language. Any BCP 47 tag is accepted; tags without a dashboard translation fall back to English."
|
|
1331
|
+
msgstr "聲明給讀者的內容語言(HTML lang、RSS),同時驅動後台介面的語言。可填任意 BCP 47 標籤;沒有對應翻譯時後台會回退為英文。"
|
|
1332
|
+
|
|
1328
1333
|
#. @context: Breadcrumb label
|
|
1329
1334
|
#. @context: Page title for the settings home page
|
|
1330
1335
|
#: src/routes/dash/settings.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+9JI/F\":[\"連線後會將您的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+zy2Nq\":[\"類型\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/JnyjR\":[\"切換內建導覽項目。它們的順序決定頁首顯示哪些項目,以及首頁會先開啟哪一個 feed。\"],\"0OGSSc\":[\"頭像顯示已更新.\"],\"0UzCUX\":[\"更新您用來登入的密碼\"],\"10UtuM\":[\"CJK 字體\"],\"14BEca\":[\"瞭解原因\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1njn7W\":[\"淺色\"],\"2B7t+s\":[\"工作階段與密碼\"],\"2DoBvq\":[\"訂閱來源\"],\"2FYpfJ\":[\"更多\"],\"2MXb5X\":[\"關於靜謐設計的實地筆記\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2cFU6q\":[\"網站頁尾\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4cEClj\":[\"工作階段\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"69OXZB\":[\"刪除託管網站\"],\"6ArdBh\":[\"將精選文章用於 /feed.\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6V3Ea3\":[\"已複製\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎收錄我的網站\"],\"7MZxzw\":[\"密碼已變更.\"],\"7vhWI8\":[\"新密碼\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 開啟託管站點控制項以取消計費或永久刪除此站點。\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8N/Mcp\":[\"封存篩選參數 (例如 format=note&view=list)\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看您在哪裡已登入,並撤銷舊的工作階段\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於您的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"
|
|
1
|
+
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+9JI/F\":[\"連線後會將您的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+zy2Nq\":[\"類型\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/JnyjR\":[\"切換內建導覽項目。它們的順序決定頁首顯示哪些項目,以及首頁會先開啟哪一個 feed。\"],\"/ODeyS\":[\"聲明給讀者的內容語言(HTML lang、RSS),同時驅動後台介面的語言。可填任意 BCP 47 標籤;沒有對應翻譯時後台會回退為英文。\"],\"0OGSSc\":[\"頭像顯示已更新.\"],\"0UzCUX\":[\"更新您用來登入的密碼\"],\"10UtuM\":[\"CJK 字體\"],\"14BEca\":[\"瞭解原因\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1njn7W\":[\"淺色\"],\"2B7t+s\":[\"工作階段與密碼\"],\"2DoBvq\":[\"訂閱來源\"],\"2FYpfJ\":[\"更多\"],\"2MXb5X\":[\"關於靜謐設計的實地筆記\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2cFU6q\":[\"網站頁尾\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4cEClj\":[\"工作階段\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"69OXZB\":[\"刪除託管網站\"],\"6ArdBh\":[\"將精選文章用於 /feed.\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6V3Ea3\":[\"已複製\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎收錄我的網站\"],\"7MZxzw\":[\"密碼已變更.\"],\"7vhWI8\":[\"新密碼\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 開啟託管站點控制項以取消計費或永久刪除此站點。\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8N/Mcp\":[\"封存篩選參數 (例如 format=note&view=list)\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看您在哪裡已登入,並撤銷舊的工作階段\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於您的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"EO3I6h\":[\"上傳未成功. 請稍後再試.\"],\"Enslfm\":[\"目標網址\"],\"F7FKwe\":[\"你在此處貼上的任何內容都能完全存取訪客的瀏覽器。僅使用來自你信任來源的程式碼。\"],\"FkMol5\":[\"精選\"],\"G/1oP+\":[\"移除 webhook 並停止同步。您的儲存庫內容不會被刪除。\"],\"G0qJsQ\":[\"找不到安全權杖。請重新整理頁面後再試一次。\"],\"G39wnK\":[\"將內容備份並與 GitHub 儲存庫同步\"],\"GMMWcy\":[\"名稱, 中繼資料, 語言, 和 搜尋預設值\"],\"GXsAby\":[\"撤銷\"],\"GxkJXS\":[\"上傳中...\"],\"GzKzUa\":[\"試用限制\"],\"HKH+W+\":[\"資料\"],\"Hp1l6f\":[\"目前\"],\"HxlY7t\":[\"變更此設定會更新訂閱者從 /feed 取得的內容。\"],\"HxuOlm\":[\"網站頁首\"],\"I6gXOa\":[\"路徑\"],\"ID38tA\":[\"示範模式下帳號刪除已停用。共用示範會另行重置。\"],\"IF9tPu\":[\"何時使用網站匯出、資料庫備份與復原演練。\"],\"IW5PBo\":[\"複製權杖\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"儲存庫\"],\"J6bLeg\":[\"新增自訂連結到任何 URL\"],\"JL7LF5\":[\"可用的 CSS 變數、data 屬性與範例.\"],\"JTviaO\":[\"管理登入安全、匯出,以及不可逆的操作。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製您的權杖 — 它不會再顯示。\"],\"JrFTcr\":[\"連線中…\"],\"JuN5GC\":[\"未選取檔案。請選擇要上傳的檔案。\"],\"KDw4GX\":[\"重試\"],\"KSgo21\":[\"選擇一個儲存庫\"],\"KVVYBh\":[\"新增選集到導覽\"],\"KiJn9B\":[\"筆記\"],\"L3DEwT\":[\"移除這個頭像?您的 favicon 與頁首圖示會回復為預設。\"],\"L4t4/q\":[\"3月14日\"],\"LdyooL\":[\"連結\"],\"M/D8PK\":[\"+ 安裝到其他帳戶\"],\"M/haSd\":[\"永遠顯示主題的淺色版本。\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"Me5t5H\":[\"連接 GitHub 倉庫,自動將你的文章備份為 Markdown 檔案。GitHub 上的編輯會同步回你的網站。\"],\"MtENL9\":[\"調整您的網站外觀、可讀性與執行效能。\"],\"N/8NPV\":[\"在刪除前,請先下載網站匯出檔案。刪除後無法恢復此帳號。\"],\"N7UNHY\":[\"精選 RSS 來源\"],\"NHnUHF\":[\"頁首上的網站圖示與個人標記\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Nldjdr\":[\"尚未有自訂 URL。建立一個自訂 URL 以為文章新增重新導向或自訂路徑。\"],\"O7rgs6\":[\"頁首 RSS 指向你的 \",[\"feed\"],\" 訂閱 (/feed)。在「一般」中變更 /feed 回傳的內容。\"],\"OSJXFg\":[\"套用於整個網站, 包括管理頁面. 選擇一個調色盤, 然後決定它是跟隨系統還是維持固定.\"],\"Ox3+3h\":[\"無相符結果。\"],\"PEUV5I\":[\"程式碼注入已更新。\"],\"PZ7HJ8\":[\"部落格大頭貼\"],\"Pwqkdw\":[\"載入中…\"],\"PxJ9W6\":[\"產生權杖\"],\"Q/6Y+2\":[\"需要在目標儲存庫上擁有 Contents(讀/寫)和 Webhooks(讀/寫)權限。\"],\"Q30z/l\":[\"要從導覽移除這個選集嗎?選集本身不會被刪除。\"],\"Q99OtV\":[\"將選集釘選到導覽列。最近 48 小時內更新的選集旁會顯示一個 * 號。\"],\"QZmz0H\":[\"內建連結\"],\"Qnrzvb\":[\"已啟用的權杖\"],\"R6Z4LE\":[\"下載失敗。請再試一次。\"],\"R9Khdg\":[\"自動\"],\"RxsRD6\":[\"時區\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"權杖名稱 (API 權杖名稱欄位。)\"],\"SVQQPe\":[\"無法連線。請檢查錯誤並再試一次。\"],\"TpF3v+\":[\"在 </head> 之前注入。用於分析、自訂 meta 標籤,以及必須提前載入的樣式。\"],\"Tz0i8g\":[\"設定\"],\"UFK415\":[\"用於分析與小工具的網站全域 HTML\"],\"Uj/btJ\":[\"在我的網站頁首顯示大頭貼\"],\"UsODUn\":[\"選擇一個帳戶\"],\"UxKoFf\":[\"導覽\"],\"V+bhUy\":[\"安裝 GitHub App\"],\"V4WsyL\":[\"新增連結\"],\"V5pZwT\":[\"搜尋設定已更新。\"],\"VXUPla\":[\"使用 GitHub App 連線\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"VoZYGU\":[\"這會永久刪除您所有的資料 — 文章、媒體、選集、設定,以及您的帳戶。您的部落格將被重設為初始設定狀態。此操作無法復原。\"],\"Weq9zb\":[\"一般\"],\"Wi9i06\":[\"依照每位訪客的系統偏好。\"],\"Wx1M8N\":[\"安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽預覽\"],\"X9Hujr\":[\"手動推送\"],\"XtBJV8\":[\"正在檢查儲存庫…\"],\"Xtc16w\":[\"重新整理儲存庫清單\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"YF6zHf\":[\"網站設定已更新.\"],\"YdG2RF\":[\"匯出網站\"],\"YwhjRx\":[\"管理帳戶\"],\"ZDY7Fy\":[\"同步中…\"],\"ZQKLI1\":[\"危險區域\"],\"ZS/CBL\":[\"刪除此導覽連結?訪客將不再在您的網站頁首看到它。\"],\"ZhhOwV\":[\"引用 (Jant 的貼文格式之一。)\"],\"ZiooJI\":[\"API 權杖\"],\"Zm7Qb0\":[\"備份與還原指南\"],\"ZmUkwN\":[\"新增自訂連結到導覽\"],\"a14mj8\":[\"未知裝置\"],\"a3LDKx\":[\"安全性\"],\"aAIQg2\":[\"外觀\"],\"aFkzVF\":[\"目標文章或選集的 slug\"],\"alKG0+\":[\"字型主題\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"主題指南\"],\"b+/jO6\":[\"301 (永久)\"],\"bHOiy1\":[\"示範模式已停用變更密碼功能。請使用共用示範帳號登入。\"],\"bHYIks\":[\"登出\"],\"bmrL08\":[\"示範模式會隱藏會話、密碼更改與帳號刪除。匯出功能仍可使用。\"],\"c3MN2z\":[\"所有可用的端點與請求格式。\"],\"cSDy01\":[\"自訂 CSS 已更新.\"],\"clzoNp\":[\"始終顯示深色主題。\"],\"cnGeoo\":[\"刪除\"],\"d3FRkY\":[\"無法複製. 請再試一次.\"],\"d5oGUo\":[\"在 GitHub 建立新儲存庫\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"確認刪除帳號\"],\"dYKrp3\":[\"從最新中隱藏 (不出現在 Latest 中,但仍可透過固定連結和集合存取的狀態。)\"],\"dk7TCH\":[\"永久刪除所有資料並重設部落格\"],\"drodVV\":[\"目前還沒有選集。請先建立一個,然後將它加入您的導覽。\"],\"dsWkIw\":[\"要與 GitHub 斷開連線嗎? webhook 將會被移除。您的儲存庫內容不會被刪除。\"],\"e/tSI5\":[\"導覽順序已更新。\"],\"ePK91l\":[\"編輯\"],\"ebQKK7\":[\"網站\"],\"egK+Yy\":[\"供腳本與自動化使用的 Bearer 權杖\"],\"ehj/zN\":[\"重新導向類型\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"已複製訂閱網址。\"],\"f8fH8W\":[\"設計\"],\"fWYqkz\":[\"程式碼注入\"],\"gOWiTY\":[\"載入為中文、日文或韓文內容最佳化的襯線字型。\"],\"gZ5owP\":[\"搜尋儲存庫\"],\"gbqbh6\":[\"可以放心離開此頁面 — 同步會在背景繼續進行。\"],\"gkFvVN\":[\"在 </body> 之前注入。用於聊天小工具和不應阻塞頁面載入的腳本。\"],\"gtQsRO\":[\"建立自訂網址\"],\"hBO/y4\":[\"安全權杖已過期。請重新整理頁面並再試一次。\"],\"hGmyDl\":[\"權杖讓您從腳本, 捷徑和其他工具存取 API 無需登入\"],\"hIHkRy\":[\"已透過 GitHub 應用程式連線\"],\"hdSi1b\":[\"輸入 \",[\"repo\"],\" 以確認\"],\"he3ygx\":[\"複製\"],\"i0qMbr\":[\"首頁\"],\"iEUzMn\":[\"系統\"],\"iSLIjg\":[\"連接\"],\"iVOMRi\":[\"首頁設定已更新。\"],\"icB4Cv\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"ihn4zD\":[\"搜尋…\"],\"iiDXZc\":[\"顯示於所有文章與頁面的底部。\"],\"j4VrG6\":[\"下載匯出 ZIP\"],\"j5nQL2\":[\"例如 iOS 捷徑\"],\"jUV7CU\":[\"上傳大頭貼\"],\"jVUmOK\":[\"支援 Markdown\"],\"jgBjXJ\":[\"要撤銷這個權杖嗎?任何使用它的腳本都會停止運作。\"],\"jpctdh\":[\"檢視\"],\"k1ifdL\":[\"處理中...\"],\"kMXclu\":[\"下載網站匯出檔案\"],\"kNiQp6\":[\"已釘選\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"細緻的樣式覆寫\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kxDZ2i\":[\"此程式碼會在您網站的每個頁面上執行。\"],\"l2Op2p\":[\"查詢參數\"],\"lLW3vJ\":[\"目標 slug\"],\"lYHJih\":[\"撤銷此工作階段?該裝置將需要重新登入。\"],\"mLOk1i\":[\"立即將所有文章推送到 GitHub,而不是等待下一次自動同步。\"],\"mSNmrX\":[\"列出貼文:\"],\"nK07ni\":[\"為您的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"o/vNDE\":[\"讓您覆寫任何主題變數。\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我們會預先填入名稱 \",[\"name\"],\"。返回時清單會重新整理。\"],\"oKOOsY\":[\"色彩主題\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有選集已經在您的導覽中。\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pgTIrt\":[\"選擇要與此網站同步的 GitHub 帳號和儲存庫.\"],\"psoxDF\":[\"該字型主題無法使用. 請選擇其他主題.\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"選集\"],\"r5EW6f\":[\"此儲存庫已在備份另一個 Jant 網站 (\",[\"host\"],\"). 請選擇其他儲存庫.\"],\"rEspiY\":[\"導覽位置已更新。\"],\"rFmBG3\":[\"色彩主題\"],\"rlonmB\":[\"無法刪除。請稍後再試。\"],\"satWc6\":[\"主要 RSS 訂閱\"],\"sgr2wQ\":[\"選集\"],\"sqxcaY\":[\"建立於 \",[\"date\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tfDRzk\":[\"儲存\"],\"tvgAq5\":[\"尚未授權任何帳戶\"],\"u1VTd3\":[\"調色盤、表面色調與整體氛圍\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"想要更細緻的控制?\"],\"udPwLB\":[\"頁首\"],\"ui6aMF\":[\"這些裝置目前已登入您的帳號。撤銷任何您不認識的工作階段。\"],\"vBEKwo\":[\"在此管理本網站的活動工作階段。密碼與託管存取由 \",[\"providerLabel\"],\" 管理。\"],\"vRldcl\":[\"字體選擇與閱讀質感\"],\"vSYKYI\":[\"主要訂閱來源\"],\"vTuib7\":[\"這會控制 /feed 回傳的內容。\"],\"vXIe7J\":[\"語言\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page]、[data-post]、[data-format] 這類資料屬性來選取特定元素。\"],\"vzX5FB\":[\"刪除帳號\"],\"w8Rv8T\":[\"標籤為必填\"],\"wL3cK8\":[\"最新\"],\"wPmHHc\":[\"低調的介面讓文字成為主角。\"],\"wW6NCp\":[\"上次錯誤\"],\"wc+17X\":[\"/* 在此放入您的自訂 CSS */\"],\"wuLtXn\":[\"目前沒有任何活動中的工作階段。已登入的裝置會顯示在此處。\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查您的伺服器設定。\"],\"xHt036\":[\"個人存取權杖\"],\"xbN8dp\":[\"柔和的色彩仍應保有清晰的閱讀節奏。\"],\"y28hnO\":[\"文章\"],\"y8Md/V\":[\"語言與時間已更新。\"],\"yNCqOt\":[\"最新 RSS 來源\"],\"yQ3kNF\":[\"請輸入以下短語以確認:\"],\"ydq1k2\":[\"請先選擇一個帳戶\"],\"yjjCV8\":[\"固定的 RSS 檔案網址\"],\"yjkELF\":[\"確認新密碼\"],\"yzF66j\":[\"連結 (Jant 的貼文格式之一。)\"],\"z6wakA\":[\"顯示在您的主頁上的簡短介紹。\"],\"zEizrk\":[\"最後使用於 \",[\"date\"]],\"zSURJW\":[\"沒有符合的儲存庫.\"],\"zXH2jX\":[\"語言與時區\"],\"zlcDd2\":[\"刪除此自訂 URL?使用該 URL 的訪客將不會再被重新導向。\"],\"zwBp5t\":[\"私有\"],\"zxRN6H\":[\"頁首連結、首頁動態與更多選單\"]}")as Messages;
|
package/src/i18n/locales.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Locale configuration
|
|
3
|
+
*
|
|
4
|
+
* Two related-but-distinct concepts:
|
|
5
|
+
*
|
|
6
|
+
* - `Locale` (catalog locale): the small enum of locales for which Jant ships
|
|
7
|
+
* a translation catalog. Used to pick which dashboard translation to render.
|
|
8
|
+
* - Content language: any syntactically valid BCP 47 language tag, used for
|
|
9
|
+
* `<html lang>`, RSS feed `<language>`, and other metadata. Independent of
|
|
10
|
+
* whether Jant has a dashboard translation for it — a Finnish blogger should
|
|
11
|
+
* be able to set `fi` for correct content metadata even though the dashboard
|
|
12
|
+
* itself falls back to English.
|
|
13
|
+
*
|
|
14
|
+
* The dashboard UI surfaces catalog locales as suggestions, but the underlying
|
|
15
|
+
* setting accepts any BCP 47 tag.
|
|
3
16
|
*/
|
|
4
17
|
|
|
5
18
|
export const locales = ["en", "zh-Hans", "zh-Hant"] as const;
|
|
@@ -7,8 +20,77 @@ export type Locale = (typeof locales)[number];
|
|
|
7
20
|
export const baseLocale: Locale = "en";
|
|
8
21
|
|
|
9
22
|
/**
|
|
10
|
-
* Check if
|
|
23
|
+
* Check if `value` is a Locale Jant has a dashboard translation catalog for.
|
|
11
24
|
*/
|
|
12
25
|
export function isLocale(value: unknown): value is Locale {
|
|
13
26
|
return typeof value === "string" && locales.includes(value as Locale);
|
|
14
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if `value` is a syntactically valid BCP 47 language tag.
|
|
31
|
+
*
|
|
32
|
+
* Accepts any tag the platform's `Intl.Locale` parses, including ones Jant
|
|
33
|
+
* has no dashboard translation for (e.g. `fi`, `ja`, `de`, `fr-CA`).
|
|
34
|
+
*/
|
|
35
|
+
export function isValidContentLanguage(value: unknown): value is string {
|
|
36
|
+
if (typeof value !== "string") return false;
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed) return false;
|
|
39
|
+
try {
|
|
40
|
+
new Intl.Locale(trimmed);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a BCP 47 tag to canonical form (`zh-cn` → `zh-CN`,
|
|
49
|
+
* `ZH-HANS` → `zh-Hans`). Returns `value` unchanged if it cannot be parsed.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeContentLanguage(value: string): string {
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
try {
|
|
54
|
+
return new Intl.Locale(trimmed).baseName;
|
|
55
|
+
} catch {
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a content language tag to the catalog locale that should drive the
|
|
62
|
+
* dashboard UI for that user.
|
|
63
|
+
*
|
|
64
|
+
* Fallback chain: exact match → language family match (`zh-CN` → `zh-Hans`,
|
|
65
|
+
* `zh-TW` → `zh-Hant`) → `baseLocale`.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveCatalogLocale(tag: string): Locale {
|
|
68
|
+
const trimmed = tag.trim();
|
|
69
|
+
if (!trimmed) return baseLocale;
|
|
70
|
+
|
|
71
|
+
let parsed: Intl.Locale;
|
|
72
|
+
try {
|
|
73
|
+
parsed = new Intl.Locale(trimmed);
|
|
74
|
+
} catch {
|
|
75
|
+
return baseLocale;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Exact match against a shipped catalog
|
|
79
|
+
if (isLocale(parsed.baseName)) return parsed.baseName;
|
|
80
|
+
|
|
81
|
+
// Language-family fallback
|
|
82
|
+
if (parsed.language === "zh") {
|
|
83
|
+
const region = parsed.region;
|
|
84
|
+
if (
|
|
85
|
+
parsed.script === "Hant" ||
|
|
86
|
+
region === "TW" ||
|
|
87
|
+
region === "HK" ||
|
|
88
|
+
region === "MO"
|
|
89
|
+
) {
|
|
90
|
+
return "zh-Hant";
|
|
91
|
+
}
|
|
92
|
+
return "zh-Hans";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return baseLocale;
|
|
96
|
+
}
|
package/src/i18n/middleware.ts
CHANGED
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
import type { MiddlewareHandler } from "hono";
|
|
6
6
|
import type { I18n } from "@lingui/core";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createI18n,
|
|
9
|
+
baseLocale,
|
|
10
|
+
isValidContentLanguage,
|
|
11
|
+
normalizeContentLanguage,
|
|
12
|
+
resolveCatalogLocale,
|
|
13
|
+
} from "./i18n.js";
|
|
8
14
|
|
|
9
15
|
declare module "hono" {
|
|
10
16
|
interface ContextVariableMap {
|
|
11
|
-
lang
|
|
17
|
+
/** BCP 47 content language tag for `<html lang>` and RSS metadata. */
|
|
18
|
+
lang: string;
|
|
12
19
|
i18n: I18n;
|
|
13
20
|
}
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
24
|
* Path prefixes that render the admin/settings surface. Requests to these
|
|
18
|
-
* paths activate the user's configured
|
|
19
|
-
* forced to `baseLocale` (English).
|
|
25
|
+
* paths activate the catalog locale resolved from the user's configured
|
|
26
|
+
* `SITE_LANGUAGE`; everything else is forced to `baseLocale` (English).
|
|
20
27
|
*
|
|
21
28
|
* Why: Lingui computes message IDs from `message` text alone (the `comment`
|
|
22
29
|
* field is a translator note and does not disambiguate). Shared strings like
|
|
@@ -37,22 +44,24 @@ function isAdminPath(path: string): boolean {
|
|
|
37
44
|
* Hono middleware for internationalization.
|
|
38
45
|
* Creates a per-request i18n instance to avoid race conditions in concurrent environments.
|
|
39
46
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* operator
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* language
|
|
47
|
+
* Two related-but-distinct values are computed per request:
|
|
48
|
+
*
|
|
49
|
+
* - `lang` (used for `<html lang>` and RSS): the verbatim BCP 47 content
|
|
50
|
+
* language tag the operator configured. Accepts any tag — `fi`, `de`,
|
|
51
|
+
* `fr-CA`, etc. — independent of whether Jant has a dashboard catalog.
|
|
52
|
+
* - The active i18n locale: the catalog Jant should render the dashboard in.
|
|
53
|
+
* Resolved from the content language via a fallback chain (exact catalog
|
|
54
|
+
* match → language family → `baseLocale`). On non-admin routes it is
|
|
55
|
+
* always `baseLocale` so public chrome stays English regardless.
|
|
48
56
|
*/
|
|
49
57
|
export function i18nMiddleware(): MiddlewareHandler {
|
|
50
58
|
return async (c, next) => {
|
|
51
|
-
const
|
|
52
|
-
const contentLang
|
|
53
|
-
?
|
|
59
|
+
const rawSetting = c.get("allSettings")?.SITE_LANGUAGE;
|
|
60
|
+
const contentLang = isValidContentLanguage(rawSetting)
|
|
61
|
+
? normalizeContentLanguage(rawSetting)
|
|
54
62
|
: baseLocale;
|
|
55
|
-
const
|
|
63
|
+
const catalogLocale = resolveCatalogLocale(contentLang);
|
|
64
|
+
const uiLang = isAdminPath(c.req.path) ? catalogLocale : baseLocale;
|
|
56
65
|
const i18n = createI18n(uiLang);
|
|
57
66
|
|
|
58
67
|
c.set("lang", contentLang);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated list of BCP 47 locale tags surfaced in the language picker.
|
|
3
|
+
*
|
|
4
|
+
* Picker UX is constrained: users select from this list rather than typing
|
|
5
|
+
* arbitrary tags, so the list must cover the realistic long tail of blog
|
|
6
|
+
* audiences. Display names are derived at call time via `Intl.DisplayNames`,
|
|
7
|
+
* coverage is resolved through `resolveCatalogLocale` against the shipped
|
|
8
|
+
* dashboard catalogs.
|
|
9
|
+
*
|
|
10
|
+
* Order is roughly intent: catalog locales first, then world languages, then
|
|
11
|
+
* common regional variants.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { resolveCatalogLocale, baseLocale } from "./locales.js";
|
|
15
|
+
import { SETTINGS_TRANSLATION_COVERAGE } from "./coverage.generated.js";
|
|
16
|
+
|
|
17
|
+
export const SUPPORTED_LOCALE_TAGS = [
|
|
18
|
+
// Catalog locales — pinned to the top.
|
|
19
|
+
"en",
|
|
20
|
+
"zh-Hans",
|
|
21
|
+
"zh-Hant",
|
|
22
|
+
|
|
23
|
+
// Other major world languages.
|
|
24
|
+
"ja",
|
|
25
|
+
"ko",
|
|
26
|
+
"es",
|
|
27
|
+
"fr",
|
|
28
|
+
"de",
|
|
29
|
+
"it",
|
|
30
|
+
"pt",
|
|
31
|
+
"ru",
|
|
32
|
+
"ar",
|
|
33
|
+
"hi",
|
|
34
|
+
"bn",
|
|
35
|
+
"ur",
|
|
36
|
+
"tr",
|
|
37
|
+
"vi",
|
|
38
|
+
"th",
|
|
39
|
+
"id",
|
|
40
|
+
"fa",
|
|
41
|
+
"he",
|
|
42
|
+
|
|
43
|
+
// European long-tail.
|
|
44
|
+
"nl",
|
|
45
|
+
"pl",
|
|
46
|
+
"sv",
|
|
47
|
+
"da",
|
|
48
|
+
"no",
|
|
49
|
+
"fi",
|
|
50
|
+
"cs",
|
|
51
|
+
"hu",
|
|
52
|
+
"el",
|
|
53
|
+
"ro",
|
|
54
|
+
"uk",
|
|
55
|
+
|
|
56
|
+
// Common regional variants worth preselecting.
|
|
57
|
+
"en-GB",
|
|
58
|
+
"en-US",
|
|
59
|
+
"fr-CA",
|
|
60
|
+
"pt-BR",
|
|
61
|
+
"es-MX",
|
|
62
|
+
"zh-CN",
|
|
63
|
+
"zh-TW",
|
|
64
|
+
"zh-HK",
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
export interface LocaleEntry {
|
|
68
|
+
/** Canonical BCP 47 tag stored in settings. */
|
|
69
|
+
tag: string;
|
|
70
|
+
/** Native display name (e.g. "简体中文", "Suomi"). */
|
|
71
|
+
native: string;
|
|
72
|
+
/** English display name for searching (e.g. "Simplified Chinese"). */
|
|
73
|
+
english: string;
|
|
74
|
+
/**
|
|
75
|
+
* Translation completeness as perceived by a user picking this tag, in
|
|
76
|
+
* [0, 1]. A non-English tag that resolves to the English fallback yields 0
|
|
77
|
+
* because the dashboard would not appear in the user's chosen language.
|
|
78
|
+
*/
|
|
79
|
+
coverage: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let entriesCache: LocaleEntry[] | null = null;
|
|
83
|
+
|
|
84
|
+
function buildEntry(tag: string): LocaleEntry {
|
|
85
|
+
let native = tag;
|
|
86
|
+
let english = tag;
|
|
87
|
+
try {
|
|
88
|
+
const nativeDn = new Intl.DisplayNames([tag], { type: "language" });
|
|
89
|
+
const fromNative = nativeDn.of(tag);
|
|
90
|
+
if (typeof fromNative === "string" && fromNative.length > 0) {
|
|
91
|
+
native = fromNative;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Intl.DisplayNames couldn't parse the tag; keep tag as fallback.
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const englishDn = new Intl.DisplayNames(["en"], { type: "language" });
|
|
98
|
+
const fromEnglish = englishDn.of(tag);
|
|
99
|
+
if (typeof fromEnglish === "string" && fromEnglish.length > 0) {
|
|
100
|
+
english = fromEnglish;
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Same fallback.
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
tag,
|
|
108
|
+
native,
|
|
109
|
+
english,
|
|
110
|
+
coverage: getCoverageFor(tag),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Coverage as seen by a user picking `tag`. Resolves through the catalog
|
|
116
|
+
* fallback chain; tags that fall through to the English base when the user's
|
|
117
|
+
* actual language is not English get 0.
|
|
118
|
+
*/
|
|
119
|
+
export function getCoverageFor(tag: string): number {
|
|
120
|
+
const catalogLocale = resolveCatalogLocale(tag);
|
|
121
|
+
if (catalogLocale === baseLocale) {
|
|
122
|
+
let userLanguage: string;
|
|
123
|
+
try {
|
|
124
|
+
userLanguage = new Intl.Locale(tag).language;
|
|
125
|
+
} catch {
|
|
126
|
+
// Treat unparseable tags as zero — picker shouldn't surface them anyway.
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
if (userLanguage !== baseLocale) return 0;
|
|
130
|
+
}
|
|
131
|
+
return SETTINGS_TRANSLATION_COVERAGE[catalogLocale];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* All curated entries. Built lazily once per process.
|
|
136
|
+
*/
|
|
137
|
+
export function getSupportedLocaleEntries(): LocaleEntry[] {
|
|
138
|
+
if (entriesCache) return entriesCache;
|
|
139
|
+
entriesCache = SUPPORTED_LOCALE_TAGS.map(buildEntry);
|
|
140
|
+
return entriesCache;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Find a curated entry by tag, or build one on the fly. Used when the stored
|
|
145
|
+
* setting value is something the picker doesn't list (e.g. a Welsh blogger
|
|
146
|
+
* pre-seeded `cy` via env var).
|
|
147
|
+
*/
|
|
148
|
+
export function getOrBuildEntry(tag: string): LocaleEntry {
|
|
149
|
+
const trimmed = tag.trim();
|
|
150
|
+
const found = getSupportedLocaleEntries().find((e) => e.tag === trimmed);
|
|
151
|
+
if (found) return found;
|
|
152
|
+
return buildEntry(trimmed);
|
|
153
|
+
}
|
|
@@ -12,15 +12,17 @@ function defaults() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
describe("buildCspDirectives", () => {
|
|
15
|
-
it("opens https for frames and
|
|
15
|
+
it("opens https for frames, scripts, styles, fonts, and connect on public pages", () => {
|
|
16
16
|
const directives = buildCspDirectives(defaults());
|
|
17
17
|
expect(directives.frameSrc).toEqual(["'self'", "https:"]);
|
|
18
18
|
expect(directives.scriptSrc).toContain("https:");
|
|
19
|
+
expect(directives.styleSrc).toContain("https:");
|
|
20
|
+
expect(directives.fontSrc).toContain("https:");
|
|
19
21
|
expect(directives.connectSrc).toContain("https:");
|
|
20
22
|
expect(directives.frameAncestors).toBeUndefined();
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
it("locks down iframes and
|
|
25
|
+
it("locks down iframes, scripts, styles, and fonts on admin paths", () => {
|
|
24
26
|
const directives = buildCspDirectives({
|
|
25
27
|
...defaults(),
|
|
26
28
|
path: "/settings",
|
|
@@ -28,6 +30,8 @@ describe("buildCspDirectives", () => {
|
|
|
28
30
|
});
|
|
29
31
|
expect(directives.frameSrc).toBeUndefined();
|
|
30
32
|
expect(directives.scriptSrc).not.toContain("https:");
|
|
33
|
+
expect(directives.styleSrc).not.toContain("https:");
|
|
34
|
+
expect(directives.fontSrc).not.toContain("https:");
|
|
31
35
|
expect(directives.connectSrc).not.toContain("https:");
|
|
32
36
|
expect(directives.frameAncestors).toEqual(["'none'"]);
|
|
33
37
|
});
|
|
@@ -65,4 +69,17 @@ describe("buildCspDirectives", () => {
|
|
|
65
69
|
});
|
|
66
70
|
expect(directives.connectSrc).toContain("ws:");
|
|
67
71
|
});
|
|
72
|
+
|
|
73
|
+
it("omits 'unsafe-inline' from script-src by default", () => {
|
|
74
|
+
const directives = buildCspDirectives(defaults());
|
|
75
|
+
expect(directives.scriptSrc).not.toContain("'unsafe-inline'");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("adds 'unsafe-inline' to script-src when allowInlineScript is set", () => {
|
|
79
|
+
const directives = buildCspDirectives({
|
|
80
|
+
...defaults(),
|
|
81
|
+
allowInlineScript: true,
|
|
82
|
+
});
|
|
83
|
+
expect(directives.scriptSrc).toContain("'unsafe-inline'");
|
|
84
|
+
});
|
|
68
85
|
});
|