@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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- 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
|
+
});
|
package/src/i18n/context.tsx
CHANGED
|
@@ -63,7 +63,7 @@ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
|
|
|
63
63
|
*
|
|
64
64
|
* return (
|
|
65
65
|
* <div>
|
|
66
|
-
* <h1>{_(t({ message: "
|
|
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: "
|
|
75
|
+
* i18n._(t({ message: "Settings", comment: "@context: Page title" }))
|
|
76
76
|
* ```
|
|
77
77
|
*/
|
|
78
78
|
export function useLingui() {
|
package/src/i18n/detect.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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
|