@jant/core 0.3.36 → 0.3.38

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 (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectLocaleFromHeader } from "../detect.js";
3
+
4
+ describe("detectLocaleFromHeader", () => {
5
+ // English variants
6
+ it("returns en for en", () => {
7
+ expect(detectLocaleFromHeader("en")).toBe("en");
8
+ });
9
+
10
+ it("returns en for en-US", () => {
11
+ expect(detectLocaleFromHeader("en-US")).toBe("en");
12
+ });
13
+
14
+ it("returns en for en-GB", () => {
15
+ expect(detectLocaleFromHeader("en-GB")).toBe("en");
16
+ });
17
+
18
+ // Simplified Chinese
19
+ it("maps zh-CN to zh-Hans", () => {
20
+ expect(detectLocaleFromHeader("zh-CN")).toBe("zh-Hans");
21
+ });
22
+
23
+ it("maps zh-SG to zh-Hans", () => {
24
+ expect(detectLocaleFromHeader("zh-SG")).toBe("zh-Hans");
25
+ });
26
+
27
+ it("maps zh-Hans to zh-Hans", () => {
28
+ expect(detectLocaleFromHeader("zh-Hans")).toBe("zh-Hans");
29
+ });
30
+
31
+ // Traditional Chinese
32
+ it("maps zh-TW to zh-Hant", () => {
33
+ expect(detectLocaleFromHeader("zh-TW")).toBe("zh-Hant");
34
+ });
35
+
36
+ it("maps zh-HK to zh-Hant", () => {
37
+ expect(detectLocaleFromHeader("zh-HK")).toBe("zh-Hant");
38
+ });
39
+
40
+ it("maps zh-MO to zh-Hant", () => {
41
+ expect(detectLocaleFromHeader("zh-MO")).toBe("zh-Hant");
42
+ });
43
+
44
+ it("maps zh-Hant to zh-Hant", () => {
45
+ expect(detectLocaleFromHeader("zh-Hant")).toBe("zh-Hant");
46
+ });
47
+
48
+ // Bare zh
49
+ it("maps bare zh to zh-Hans", () => {
50
+ expect(detectLocaleFromHeader("zh")).toBe("zh-Hans");
51
+ });
52
+
53
+ // q-value priority
54
+ it("picks highest q-value language", () => {
55
+ expect(detectLocaleFromHeader("fr;q=0.5,zh-CN;q=0.9,en;q=0.8")).toBe(
56
+ "zh-Hans",
57
+ );
58
+ });
59
+
60
+ it("picks first when q-values are equal (stable sort)", () => {
61
+ expect(detectLocaleFromHeader("zh-TW,en")).toBe("zh-Hant");
62
+ });
63
+
64
+ it("treats missing q as q=1.0", () => {
65
+ expect(detectLocaleFromHeader("zh-CN,en;q=0.5")).toBe("zh-Hans");
66
+ });
67
+
68
+ // Unsupported languages fallback
69
+ it("falls back to en for unsupported language", () => {
70
+ expect(detectLocaleFromHeader("fr")).toBe("en");
71
+ });
72
+
73
+ it("falls back to en when all languages are unsupported", () => {
74
+ expect(detectLocaleFromHeader("fr,de,ja")).toBe("en");
75
+ });
76
+
77
+ it("returns supported locale when mixed with unsupported", () => {
78
+ expect(detectLocaleFromHeader("fr,zh-TW;q=0.8")).toBe("zh-Hant");
79
+ });
80
+
81
+ // Edge cases
82
+ it("returns en for undefined", () => {
83
+ expect(detectLocaleFromHeader(undefined)).toBe("en");
84
+ });
85
+
86
+ it("returns en for empty string", () => {
87
+ expect(detectLocaleFromHeader("")).toBe("en");
88
+ });
89
+
90
+ it("returns en for whitespace-only string", () => {
91
+ expect(detectLocaleFromHeader(" ")).toBe("en");
92
+ });
93
+
94
+ it("skips entries with q=0", () => {
95
+ expect(detectLocaleFromHeader("zh-CN;q=0,en")).toBe("en");
96
+ });
97
+
98
+ it("handles malformed q-values gracefully (falls back to q=1.0)", () => {
99
+ expect(detectLocaleFromHeader("zh-CN;q=abc,en")).toBe("zh-Hans");
100
+ });
101
+
102
+ it("is case-insensitive", () => {
103
+ expect(detectLocaleFromHeader("ZH-CN")).toBe("zh-Hans");
104
+ });
105
+
106
+ it("handles wildcard (*)", () => {
107
+ expect(detectLocaleFromHeader("*")).toBe("en");
108
+ });
109
+
110
+ it("handles realistic browser header", () => {
111
+ expect(detectLocaleFromHeader("zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7")).toBe(
112
+ "zh-Hant",
113
+ );
114
+ });
115
+ });
@@ -63,7 +63,7 @@ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
63
63
  *
64
64
  * return (
65
65
  * <div>
66
- * <h1>{_(t({ message: "Dashboard", comment: "@context: Page title" }))}</h1>
66
+ * <h1>{_(t({ message: "Settings", comment: "@context: Page title" }))}</h1>
67
67
  * </div>
68
68
  * );
69
69
  * }
@@ -72,7 +72,7 @@ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
72
72
  * Or use the i18n instance directly:
73
73
  * ```tsx
74
74
  * const { i18n } = useLingui();
75
- * i18n._(t({ message: "Dashboard", comment: "@context: Page title" }))
75
+ * i18n._(t({ message: "Settings", comment: "@context: Page title" }))
76
76
  * ```
77
77
  */
78
78
  export function useLingui() {
@@ -2,7 +2,7 @@
2
2
  * Language Detection Utilities
3
3
  */
4
4
 
5
- import { locales, isLocale, type Locale } from "./locales.js";
5
+ import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
6
6
 
7
7
  /**
8
8
  * Get display name for a language code
@@ -32,3 +32,87 @@ export function getSupportedLanguages(): Array<{ code: Locale; name: string }> {
32
32
  export function isValidLanguage(lang: unknown): lang is Locale {
33
33
  return isLocale(lang);
34
34
  }
35
+
36
+ /**
37
+ * Map a BCP 47 language tag to a supported locale.
38
+ *
39
+ * @param tag - BCP 47 language tag (e.g. "zh-CN", "en-US")
40
+ * @returns Matching locale, or `undefined` if unsupported
41
+ */
42
+ function mapTagToLocale(tag: string): Locale | undefined {
43
+ const normalized = tag.trim().toLowerCase();
44
+ if (!normalized) return undefined;
45
+
46
+ // Extract primary language subtag
47
+ const primary = normalized.split("-")[0];
48
+
49
+ // Chinese variants need script/region mapping
50
+ if (primary === "zh") {
51
+ const rest = normalized.slice(3); // everything after "zh-"
52
+ // Simplified Chinese: zh-Hans, zh-CN, zh-SG
53
+ if (rest === "hans" || rest === "cn" || rest === "sg") return "zh-Hans";
54
+ // Traditional Chinese: zh-Hant, zh-TW, zh-HK, zh-MO
55
+ if (rest === "hant" || rest === "tw" || rest === "hk" || rest === "mo")
56
+ return "zh-Hant";
57
+ // Bare "zh" defaults to Simplified
58
+ if (!rest) return "zh-Hans";
59
+ return undefined;
60
+ }
61
+
62
+ // Direct match (e.g. "en")
63
+ if (isLocale(primary)) return primary;
64
+
65
+ return undefined;
66
+ }
67
+
68
+ /**
69
+ * Detect the best supported locale from an `Accept-Language` HTTP header.
70
+ *
71
+ * Parses comma-separated entries, respects q-values, and maps BCP 47 tags
72
+ * to supported locales using script/region rules for Chinese variants.
73
+ *
74
+ * @param header - Raw `Accept-Language` header value (e.g. "zh-TW,en;q=0.9")
75
+ * @returns Best matching locale, or the base locale ("en") if none match
76
+ *
77
+ * @example
78
+ * detectLocaleFromHeader("zh-CN,en;q=0.8") // "zh-Hans"
79
+ * detectLocaleFromHeader("fr,de;q=0.9") // "en" (fallback)
80
+ */
81
+ export function detectLocaleFromHeader(header: string | undefined): Locale {
82
+ if (!header || !header.trim()) return baseLocale;
83
+
84
+ // Parse entries: "lang;q=0.8" → { tag, q }
85
+ const entries: Array<{ tag: string; q: number }> = [];
86
+
87
+ for (const part of header.split(",")) {
88
+ const segments = part.trim().split(";");
89
+ const tag = segments[0]?.trim();
90
+ if (!tag) continue;
91
+
92
+ let q = 1.0;
93
+ for (let i = 1; i < segments.length; i++) {
94
+ const param = segments[i]?.trim();
95
+ if (param?.toLowerCase().startsWith("q=")) {
96
+ const parsed = Number.parseFloat(param.slice(2));
97
+ if (!Number.isNaN(parsed)) q = parsed;
98
+ break;
99
+ }
100
+ }
101
+
102
+ // q=0 means "not acceptable"
103
+ if (q <= 0) continue;
104
+
105
+ entries.push({ tag, q });
106
+ }
107
+
108
+ // Stable sort by q-value descending
109
+ entries.sort((a, b) => b.q - a.q);
110
+
111
+ // Return first matching locale
112
+ for (const { tag } of entries) {
113
+ const locale = mapTagToLocale(tag);
114
+ if (locale) return locale;
115
+ }
116
+
117
+ return baseLocale;
118
+ }
package/src/i18n/i18n.ts CHANGED
@@ -51,7 +51,7 @@ export function createI18n(locale: Locale): I18n {
51
51
  * import { getI18n } from "../i18n/index.js";
52
52
  *
53
53
  * const i18n = getI18n(c);
54
- * const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
54
+ * const title = i18n._(msg({ message: "Settings", comment: "@context: Page title" }));
55
55
  */
56
56
  export function getI18n(c: { get(key: "i18n"): I18n }): I18n {
57
57
  return c.get("i18n");
package/src/i18n/index.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * return (
26
26
  * <div>
27
- * <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
27
+ * <h1>{t({ message: "Settings", comment: "@context: Page title" })}</h1>
28
28
  * <Trans comment="@context: Help text">
29
29
  * Read the <a href="/docs">documentation</a>
30
30
  * </Trans>
@@ -56,6 +56,7 @@ export {
56
56
  isValidLanguage,
57
57
  getLanguageDisplayName,
58
58
  getSupportedLanguages,
59
+ detectLocaleFromHeader,
59
60
  } from "./detect.js";
60
61
 
61
62
  // Hono middleware