@jant/core 0.3.46 → 0.3.47

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.
Files changed (109) hide show
  1. package/bin/commands/db/execute-file.js +12 -4
  2. package/bin/commands/db/rehearse.js +2 -2
  3. package/bin/commands/export.js +12 -4
  4. package/bin/commands/import-site.js +60 -267
  5. package/bin/commands/migrate.js +36 -69
  6. package/bin/commands/reset-password.js +10 -4
  7. package/bin/commands/site/export.js +59 -248
  8. package/bin/commands/site/snapshot/export.js +58 -45
  9. package/bin/commands/site/snapshot/import.js +104 -52
  10. package/bin/lib/node-env.js +100 -0
  11. package/bin/lib/runtime-target.js +64 -0
  12. package/bin/lib/site-snapshot.js +185 -54
  13. package/bin/lib/sql-export.js +19 -2
  14. package/dist/{app-DB-P66E5.js → app-3REcR-3U.js} +331 -189
  15. package/dist/app-B67XOEyo.js +6 -0
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
  18. package/dist/client/_assets/client-s71Js1Cu.css +2 -0
  19. package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
  20. package/dist/github-sync-C593r22F.js +4 -0
  21. package/dist/github-sync-bL1hnx3Q.js +428 -0
  22. package/dist/index.js +3 -2
  23. package/dist/node.js +5 -4
  24. package/package.json +3 -2
  25. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  26. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  28. package/src/client/components/jant-settings-general.ts +164 -22
  29. package/src/client/components/settings-types.ts +4 -6
  30. package/src/client-auth.ts +1 -1
  31. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  32. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  33. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  34. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  35. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  36. package/src/db/migrations/meta/_journal.json +7 -0
  37. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  38. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  39. package/src/db/migrations/pg/meta/_journal.json +7 -0
  40. package/src/db/pg/schema.ts +21 -26
  41. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  42. package/src/db/schema.ts +16 -20
  43. package/src/i18n/__tests__/middleware.test.ts +43 -1
  44. package/src/i18n/coverage.generated.ts +17 -0
  45. package/src/i18n/i18n.ts +18 -2
  46. package/src/i18n/index.ts +3 -0
  47. package/src/i18n/locales/settings/en.po +16 -11
  48. package/src/i18n/locales/settings/en.ts +1 -1
  49. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  50. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  52. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  53. package/src/i18n/locales.ts +84 -2
  54. package/src/i18n/middleware.ts +25 -16
  55. package/src/i18n/supported-locales.ts +153 -0
  56. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  57. package/src/lib/__tests__/feed.test.ts +242 -1
  58. package/src/lib/__tests__/post-meta.test.ts +0 -1
  59. package/src/lib/__tests__/view.test.ts +0 -1
  60. package/src/lib/csp-builder.ts +28 -10
  61. package/src/lib/feed.ts +153 -3
  62. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  63. package/src/middleware/auth.ts +1 -1
  64. package/src/middleware/secure-headers.ts +47 -1
  65. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  66. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  67. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  68. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  69. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  70. package/src/node/index.ts +1 -0
  71. package/src/preset.css +8 -2
  72. package/src/routes/api/__tests__/settings.test.ts +3 -2
  73. package/src/routes/api/github-sync.tsx +1 -1
  74. package/src/routes/api/settings.ts +4 -1
  75. package/src/routes/auth/signin.tsx +6 -0
  76. package/src/routes/pages/archive.tsx +4 -2
  77. package/src/services/__tests__/post.test.ts +19 -19
  78. package/src/services/__tests__/search.test.ts +0 -1
  79. package/src/services/__tests__/settings.test.ts +22 -3
  80. package/src/services/bootstrap.ts +7 -3
  81. package/src/services/collection.ts +3 -3
  82. package/src/services/export.ts +0 -3
  83. package/src/services/navigation.ts +0 -2
  84. package/src/services/path.ts +1 -38
  85. package/src/services/post.ts +32 -66
  86. package/src/services/search.ts +0 -6
  87. package/src/services/settings.ts +47 -6
  88. package/src/services/site-admin.ts +6 -1
  89. package/src/styles/ui.css +12 -23
  90. package/src/types/entities.ts +0 -1
  91. package/src/ui/color-themes.ts +1 -1
  92. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  93. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  94. package/src/ui/feed/NoteCard.tsx +1 -11
  95. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  96. package/src/ui/pages/PostPage.tsx +2 -0
  97. package/bin/commands/collections.js +0 -268
  98. package/bin/commands/media.js +0 -302
  99. package/bin/commands/posts.js +0 -262
  100. package/bin/commands/search.js +0 -53
  101. package/bin/commands/settings.js +0 -93
  102. package/bin/lib/http-api.js +0 -223
  103. package/bin/lib/media-upload.js +0 -206
  104. package/dist/app-CM7sb3xO.js +0 -5
  105. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  106. package/src/__tests__/bin/content-cli.test.ts +0 -179
  107. package/src/__tests__/bin/media-cli.test.ts +0 -192
  108. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  109. /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/dashboard language
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\"],\" 管理密碼與託管存取\"],\"ECIBO2\":[\"控制儀表板與設定的語言。公開頁面保持英文。\"],\"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\":[\"套用於整個網站, 包括管理頁面. 選擇一個調色盤, 然後決定它是跟隨系統還是維持固定.\"],\"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\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"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\":[\"為您的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"nbfdhU\":[\"整合\"],\"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;
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;
@@ -1,5 +1,18 @@
1
1
  /**
2
- * Centralized locale configuration
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 a value is a valid locale
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
+ }
@@ -4,19 +4,26 @@
4
4
 
5
5
  import type { MiddlewareHandler } from "hono";
6
6
  import type { I18n } from "@lingui/core";
7
- import { createI18n, baseLocale, isLocale, type Locale } from "./i18n.js";
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: Locale;
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 `SITE_LANGUAGE`; everything else is
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
- * `lang` and the active i18n locale are intentionally decoupled:
41
- * - `lang` (used for `<html lang>`) always reflects the configured
42
- * `SITE_LANGUAGE`, because it describes the *content* language the
43
- * operator's posts not the UI chrome.
44
- * - The active i18n locale is `SITE_LANGUAGE` on admin routes (so the
45
- * translated settings catalog applies) and forced to `baseLocale`
46
- * elsewhere, so public UI strings stay in English even when the content
47
- * language is non-English.
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 siteLanguage = c.get("allSettings")?.SITE_LANGUAGE;
52
- const contentLang: Locale = isLocale(siteLanguage)
53
- ? siteLanguage
59
+ const rawSetting = c.get("allSettings")?.SITE_LANGUAGE;
60
+ const contentLang = isValidContentLanguage(rawSetting)
61
+ ? normalizeContentLanguage(rawSetting)
54
62
  : baseLocale;
55
- const uiLang: Locale = isAdminPath(c.req.path) ? contentLang : baseLocale;
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 scripts on public pages", () => {
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 scripts on admin paths", () => {
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
  });