@jant/core 0.3.35 → 0.3.37

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 (307) 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/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -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