@jant/core 0.2.2 → 0.2.4

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 (92) hide show
  1. package/dist/client.d.ts +4 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +6 -2
  4. package/dist/lib/assets.d.ts +4 -3
  5. package/dist/lib/assets.d.ts.map +1 -1
  6. package/dist/lib/assets.js +1 -3
  7. package/dist/theme/layouts/BaseLayout.js +0 -5
  8. package/package.json +4 -5
  9. package/src/app.tsx +377 -0
  10. package/src/auth.ts +38 -0
  11. package/src/client.ts +7 -2
  12. package/src/db/index.ts +14 -0
  13. package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
  14. package/src/db/migrations/0001_add_search_fts.sql +40 -0
  15. package/src/db/migrations/0002_collection_path.sql +2 -0
  16. package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
  17. package/src/db/migrations/0004_media_uuid.sql +35 -0
  18. package/src/db/migrations/meta/0000_snapshot.json +784 -0
  19. package/src/db/migrations/meta/_journal.json +41 -0
  20. package/src/db/schema.ts +159 -0
  21. package/src/i18n/EXAMPLES.md +235 -0
  22. package/src/i18n/README.md +296 -0
  23. package/src/i18n/Trans.tsx +31 -0
  24. package/src/i18n/context.tsx +101 -0
  25. package/src/i18n/detect.ts +100 -0
  26. package/src/i18n/i18n.ts +62 -0
  27. package/src/i18n/index.ts +65 -0
  28. package/src/i18n/locales/en.po +875 -0
  29. package/src/i18n/locales/en.ts +1 -0
  30. package/src/i18n/locales/zh-Hans.po +875 -0
  31. package/src/i18n/locales/zh-Hans.ts +1 -0
  32. package/src/i18n/locales/zh-Hant.po +875 -0
  33. package/src/i18n/locales/zh-Hant.ts +1 -0
  34. package/src/i18n/locales.ts +14 -0
  35. package/src/i18n/middleware.ts +59 -0
  36. package/src/index.ts +42 -0
  37. package/src/lib/assets.ts +49 -0
  38. package/src/lib/constants.ts +67 -0
  39. package/src/lib/image.ts +107 -0
  40. package/src/lib/index.ts +9 -0
  41. package/src/lib/markdown.ts +93 -0
  42. package/src/lib/schemas.ts +92 -0
  43. package/src/lib/sqid.ts +79 -0
  44. package/src/lib/sse.ts +152 -0
  45. package/src/lib/time.ts +117 -0
  46. package/src/lib/url.ts +107 -0
  47. package/src/middleware/auth.ts +59 -0
  48. package/src/preset.css +2 -11
  49. package/src/routes/api/posts.ts +127 -0
  50. package/src/routes/api/search.ts +53 -0
  51. package/src/routes/api/upload.ts +240 -0
  52. package/src/routes/dash/collections.tsx +341 -0
  53. package/src/routes/dash/index.tsx +89 -0
  54. package/src/routes/dash/media.tsx +551 -0
  55. package/src/routes/dash/pages.tsx +245 -0
  56. package/src/routes/dash/posts.tsx +202 -0
  57. package/src/routes/dash/redirects.tsx +155 -0
  58. package/src/routes/dash/settings.tsx +93 -0
  59. package/src/routes/feed/rss.ts +119 -0
  60. package/src/routes/feed/sitemap.ts +75 -0
  61. package/src/routes/pages/archive.tsx +223 -0
  62. package/src/routes/pages/collection.tsx +79 -0
  63. package/src/routes/pages/home.tsx +93 -0
  64. package/src/routes/pages/page.tsx +64 -0
  65. package/src/routes/pages/post.tsx +81 -0
  66. package/src/routes/pages/search.tsx +162 -0
  67. package/src/services/collection.ts +180 -0
  68. package/src/services/index.ts +40 -0
  69. package/src/services/media.ts +97 -0
  70. package/src/services/post.ts +279 -0
  71. package/src/services/redirect.ts +74 -0
  72. package/src/services/search.ts +117 -0
  73. package/src/services/settings.ts +76 -0
  74. package/src/theme/components/ActionButtons.tsx +98 -0
  75. package/src/theme/components/CrudPageHeader.tsx +48 -0
  76. package/src/theme/components/DangerZone.tsx +77 -0
  77. package/src/theme/components/EmptyState.tsx +56 -0
  78. package/src/theme/components/ListItemRow.tsx +24 -0
  79. package/src/theme/components/PageForm.tsx +114 -0
  80. package/src/theme/components/Pagination.tsx +196 -0
  81. package/src/theme/components/PostForm.tsx +122 -0
  82. package/src/theme/components/PostList.tsx +68 -0
  83. package/src/theme/components/ThreadView.tsx +118 -0
  84. package/src/theme/components/TypeBadge.tsx +28 -0
  85. package/src/theme/components/VisibilityBadge.tsx +33 -0
  86. package/src/theme/components/index.ts +12 -0
  87. package/src/theme/index.ts +24 -0
  88. package/src/theme/layouts/BaseLayout.tsx +50 -0
  89. package/src/theme/layouts/DashLayout.tsx +108 -0
  90. package/src/theme/layouts/index.ts +2 -0
  91. package/src/types.ts +222 -0
  92. package/static/assets/datastar.min.js +0 -7
@@ -0,0 +1,296 @@
1
+ # Jant i18n - React-like API for Hono JSX
2
+
3
+ ## ✅ API Overview
4
+
5
+ We provide a React-like i18n API that works with Hono JSX SSR!
6
+
7
+ ### 1. **I18nProvider** - Like React Context Provider
8
+
9
+ ```tsx
10
+ import { I18nProvider } from "@/i18n";
11
+
12
+ // Wrap your app in route handler
13
+ dashRoute.get("/", async (c) => {
14
+ return c.html(
15
+ <I18nProvider c={c}>
16
+ <YourApp />
17
+ </I18nProvider>
18
+ );
19
+ });
20
+ ```
21
+
22
+ ### 2. **useLingui()** - Like React hook
23
+
24
+ ```tsx
25
+ import { useLingui } from "@/i18n";
26
+
27
+ function MyComponent() {
28
+ // React-like hook API
29
+ const { t } = useLingui();
30
+
31
+ return (
32
+ <div>
33
+ {/* Simple and clean */}
34
+ <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
35
+ </div>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ### 3. **Trans Component** - For Embedded JSX
41
+
42
+ ```tsx
43
+ import { Trans } from "@/i18n";
44
+
45
+ function MyComponent() {
46
+ return (
47
+ <Trans comment="@context: Help text">
48
+ Read the <a href="/docs">documentation</a>
49
+ </Trans>
50
+ );
51
+ }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 📝 Complete Example
57
+
58
+ ```tsx
59
+ /**
60
+ * Dashboard Route - React-like i18n API
61
+ */
62
+
63
+ import { Hono } from "hono";
64
+ import { I18nProvider, useLingui, Trans } from "@/i18n";
65
+
66
+ export const dashRoute = new Hono();
67
+
68
+ // Component: use useLingui() hook
69
+ function DashboardContent({ postCount }: { postCount: number }) {
70
+ const { t } = useLingui();
71
+
72
+ return (
73
+ <div>
74
+ {/* 1. Simple translation */}
75
+ <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
76
+
77
+ {/* 2. With variables */}
78
+ <p>
79
+ {t(
80
+ { message: "You have {count} posts", comment: "@context: Post count message" },
81
+ { count: postCount }
82
+ )}
83
+ </p>
84
+
85
+ {/* 3. With embedded components - use Trans */}
86
+ <p>
87
+ <Trans comment="@context: Help text">
88
+ Read the <a href="/docs" class="underline">documentation</a>
89
+ </Trans>
90
+ </p>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // Route handler: wrap in I18nProvider
96
+ dashRoute.get("/", async (c) => {
97
+ const posts = await c.var.services.posts.list();
98
+
99
+ return c.html(
100
+ <I18nProvider c={c}>
101
+ <DashboardContent postCount={posts.length} />
102
+ </I18nProvider>
103
+ );
104
+ });
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 🆚 Comparison: Before vs Now
110
+
111
+ ### Before (Complex - prop drilling)
112
+
113
+ ```tsx
114
+ import { getI18n } from "@/i18n";
115
+
116
+ dashRoute.get("/", async (c) => {
117
+ const i18n = getI18n(c);
118
+
119
+ return c.html(
120
+ <Layout title={i18n._({ message: "Dashboard", comment: "@context: ..." })}>
121
+ <MyComponent c={c} /> {/* Need to pass c prop */}
122
+ </Layout>
123
+ );
124
+ });
125
+
126
+ function MyComponent({ c }: { c: Context }) {
127
+ const i18n = getI18n(c);
128
+ return <h1>{i18n._({ message: "Hello", comment: "@context: ..." })}</h1>;
129
+ }
130
+ ```
131
+
132
+ ### Now (Clean - no prop drilling)
133
+
134
+ ```tsx
135
+ import { I18nProvider, useLingui } from "@/i18n";
136
+
137
+ dashRoute.get("/", async (c) => {
138
+ return c.html(
139
+ <I18nProvider c={c}>
140
+ <Layout>
141
+ <MyComponent /> {/* No need to pass c prop */}
142
+ </Layout>
143
+ </I18nProvider>
144
+ );
145
+ });
146
+
147
+ function MyComponent() {
148
+ const { t } = useLingui(); // Like React hook
149
+ return <h1>{t({ message: "Hello", comment: "@context: ..." })}</h1>;
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## ⚠️ Important Notes
156
+
157
+ ### 1. **Always include `comment` field**
158
+
159
+ ```tsx
160
+ // ✅ Correct - comment is crucial for AI translation
161
+ const { t } = useLingui();
162
+ t({ message: "Dashboard", comment: "@context: Page title" })
163
+
164
+ // ❌ Wrong - missing comment reduces translation quality
165
+ t({ message: "Dashboard" })
166
+ ```
167
+
168
+ ### 2. **I18nProvider must wrap your app**
169
+
170
+ ```tsx
171
+ // ✅ Correct - wrap in I18nProvider
172
+ c.html(
173
+ <I18nProvider c={c}>
174
+ <App />
175
+ </I18nProvider>
176
+ )
177
+
178
+ // ❌ Wrong - useLingui() will throw error
179
+ c.html(<App />) // useLingui() inside App won't find i18n context
180
+ ```
181
+
182
+ ### 3. **useLingui() only works inside components**
183
+
184
+ ```tsx
185
+ // ✅ Correct - inside JSX component
186
+ function MyComponent() {
187
+ const { t } = useLingui();
188
+ return <div>{t({ message: "Hello", comment: "@context: ..." })}</div>;
189
+ }
190
+
191
+ // ❌ Wrong - in route handler (outside I18nProvider)
192
+ dashRoute.get("/", async (c) => {
193
+ const { t } = useLingui(); // Error: not inside I18nProvider
194
+ ...
195
+ });
196
+ ```
197
+
198
+ ### 4. **Variables go in second parameter**
199
+
200
+ ```tsx
201
+ const { t } = useLingui();
202
+
203
+ // ✅ Correct - values as second parameter
204
+ t({ message: "Hello {name}", comment: "@context: Greeting" }, { name: "Alice" })
205
+
206
+ // ❌ Wrong - values inside first parameter (not supported)
207
+ t({ message: "Hello {name}", comment: "@context: Greeting", values: { name: "Alice" } })
208
+ ```
209
+
210
+ ---
211
+
212
+ ## 🎯 Best Practices
213
+
214
+ 1. **Route handler**: Wrap app in `<I18nProvider c={c}>`
215
+ 2. **Inside components**: Use `useLingui()` hook to get `t()` function
216
+ 3. **Translation calls**: `t({ message: "...", comment: "@context: ..." })`
217
+ 4. **With embedded JSX**: Use `<Trans>` component
218
+ 5. **Always include `comment`**: Helps AI understand context for better translations
219
+
220
+ ---
221
+
222
+ ## 📚 API Reference
223
+
224
+ ### `I18nProvider`
225
+
226
+ Provides i18n context to all child components. Must wrap your app in route handlers.
227
+
228
+ ```tsx
229
+ interface I18nProviderProps {
230
+ c: Context; // Hono context
231
+ children: JSX.Element;
232
+ }
233
+
234
+ // Usage
235
+ <I18nProvider c={c}>
236
+ <YourApp />
237
+ </I18nProvider>
238
+ ```
239
+
240
+ ### `useLingui()`
241
+
242
+ Hook to access i18n functionality inside components. Must be used within `I18nProvider`.
243
+
244
+ ```tsx
245
+ function useLingui(): {
246
+ i18n: I18n; // Lingui i18n instance
247
+ t: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
248
+ _: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
249
+ }
250
+
251
+ // Usage
252
+ function MyComponent() {
253
+ const { t } = useLingui();
254
+ return <h1>{t({ message: "Hello", comment: "@context: Greeting" })}</h1>;
255
+ }
256
+ ```
257
+
258
+ **Note**: `t()` and `_()` are equivalent - use whichever you prefer.
259
+
260
+ ### `Trans`
261
+
262
+ Component for translations with embedded JSX elements. Simplified implementation that renders children as-is.
263
+
264
+ ```tsx
265
+ interface TransProps {
266
+ comment?: string; // @context comment for translators
267
+ id?: string; // Optional message ID
268
+ children: JSX.Element; // JSX content with embedded elements
269
+ }
270
+
271
+ // Usage
272
+ <Trans comment="@context: Help text">
273
+ Read the <a href="/docs">documentation</a>
274
+ </Trans>
275
+ ```
276
+
277
+ **Note**: This is a simplified implementation. For complex translations with dynamic content, use `t()` with placeholders instead.
278
+
279
+ ---
280
+
281
+ ## 🔧 How It Works
282
+
283
+ 1. **I18nProvider** sets the global i18n instance during rendering
284
+ 2. **useLingui()** reads the current i18n instance from global state
285
+ 3. **Single-pass rendering**: Each request renders only once, making global state safe
286
+ 4. **Concurrency-safe**: Each request creates a new i18n instance, preventing race conditions
287
+
288
+ This implementation mimics React's Context API but is optimized for Hono JSX SSR scenarios.
289
+
290
+ ### Why Global State is Safe
291
+
292
+ Unlike React (client-side with multiple re-renders), Hono JSX renders once per request on the server:
293
+ - Request arrives → I18nProvider sets global i18n → Components render → Response sent
294
+ - Next request → New i18n instance → Components render → Response sent
295
+
296
+ Since rendering is synchronous and single-pass, there's no risk of concurrent requests interfering with each other.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Trans Component for Hono JSX
3
+ *
4
+ * Simple implementation that just renders children directly.
5
+ * For complex translations with embedded JSX, use the t() function with placeholders.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+
10
+ export interface TransProps extends PropsWithChildren {
11
+ comment?: string;
12
+ id?: string;
13
+ }
14
+
15
+ /**
16
+ * Trans component - renders children as-is
17
+ * Note: This is a simplified implementation. For translations with embedded JSX,
18
+ * it's recommended to use t() with placeholders instead.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Trans comment="@context: Help text">
23
+ * Visit the <a href="/docs">documentation</a>
24
+ * </Trans>
25
+ * ```
26
+ */
27
+ export const Trans: FC<TransProps> = ({ children }) => {
28
+ // In a full implementation, this would extract and translate the content
29
+ // For now, we just render children as-is (works for English/default locale)
30
+ return <>{children}</>;
31
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Hono JSX i18n Context System
3
+ *
4
+ * Mimics React's Context API for Hono JSX to provide i18n without prop drilling
5
+ */
6
+
7
+ import type { Context } from "hono";
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import type { I18n, MessageDescriptor } from "@lingui/core";
10
+ import { getI18n as getI18nFromContext } from "./i18n.js";
11
+
12
+ /**
13
+ * Message descriptor that accepts both pre-macro (without id) and post-macro (with id) formats
14
+ * This allows TypeScript to accept t({ message, comment }) before macro transformation
15
+ */
16
+ type TranslationDescriptor = {
17
+ id?: string;
18
+ message: string;
19
+ comment?: string;
20
+ values?: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
21
+ };
22
+
23
+ // Store i18n instance during render
24
+ let currentI18n: I18n | null = null;
25
+
26
+ /**
27
+ * I18nProvider - wraps your app to provide i18n context
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import { I18nProvider } from "@/i18n";
32
+ *
33
+ * return c.html(
34
+ * <I18nProvider c={c}>
35
+ * <YourApp />
36
+ * </I18nProvider>
37
+ * );
38
+ * ```
39
+ */
40
+ export interface I18nProviderProps extends PropsWithChildren {
41
+ c: Context;
42
+ }
43
+
44
+ export const I18nProvider: FC<I18nProviderProps> = ({ c, children }) => {
45
+ // Set current i18n for this render
46
+ // Note: In Hono JSX, rendering is synchronous and single-threaded per request
47
+ // so we can safely set global context without cleanup
48
+ currentI18n = getI18nFromContext(c);
49
+ return <>{children}</>;
50
+ };
51
+
52
+ /**
53
+ * useLingui hook - get i18n instance and translation function
54
+ * Mimics @lingui/react's useLingui() API
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * import { t } from "@lingui/core/macro";
59
+ * import { useLingui } from "@/i18n";
60
+ *
61
+ * function MyComponent() {
62
+ * const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
63
+ *
64
+ * return (
65
+ * <div>
66
+ * <h1>{_(t({ message: "Dashboard", comment: "@context: Page title" }))}</h1>
67
+ * </div>
68
+ * );
69
+ * }
70
+ * ```
71
+ *
72
+ * Or use the i18n instance directly:
73
+ * ```tsx
74
+ * const { i18n } = useLingui();
75
+ * i18n._(t({ message: "Dashboard", comment: "@context: Page title" }))
76
+ * ```
77
+ */
78
+ export function useLingui() {
79
+ if (!currentI18n) {
80
+ throw new Error(
81
+ "useLingui() called outside of I18nProvider. " +
82
+ "Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>"
83
+ );
84
+ }
85
+
86
+ // Create translation function that accepts both pre-macro and post-macro formats
87
+ const translate = (descriptor: TranslationDescriptor) => {
88
+ // The macro will add the id, or it's already present
89
+ // At runtime, we pass it to i18n._ which handles the descriptor with values
90
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- currentI18n is checked above
91
+ return currentI18n!._(descriptor as MessageDescriptor);
92
+ };
93
+
94
+ return {
95
+ i18n: currentI18n,
96
+ // t function - can be used with t macro from @lingui/core/macro
97
+ t: translate,
98
+ // _ is an alias for t (shorter)
99
+ _: translate,
100
+ };
101
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Language Detection Utilities
3
+ */
4
+
5
+ import type { Context } from "hono";
6
+ import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
7
+
8
+ export const LANGUAGE_COOKIE_NAME = "lang";
9
+
10
+ /**
11
+ * Get display name for a language code
12
+ */
13
+ export function getLanguageDisplayName(locale: Locale): string {
14
+ const names: Record<Locale, string> = {
15
+ en: "English",
16
+ "zh-Hans": "简体中文",
17
+ "zh-Hant": "繁體中文",
18
+ };
19
+ return names[locale];
20
+ }
21
+
22
+ /**
23
+ * Get all supported languages with display names
24
+ */
25
+ export function getSupportedLanguages(): Array<{ code: Locale; name: string }> {
26
+ return locales.map((code) => ({
27
+ code,
28
+ name: getLanguageDisplayName(code),
29
+ }));
30
+ }
31
+
32
+ /**
33
+ * Check if a language code is valid
34
+ */
35
+ export function isValidLanguage(lang: unknown): lang is Locale {
36
+ return isLocale(lang);
37
+ }
38
+
39
+ /**
40
+ * Parse Accept-Language header and return best matching locale
41
+ */
42
+ export function parseAcceptLanguage(header: string | null): Locale {
43
+ if (!header) return baseLocale;
44
+
45
+ // Parse "en-US,en;q=0.9,zh-CN;q=0.8" format
46
+ const languages = header
47
+ .split(",")
48
+ .map((part) => {
49
+ const [lang, qPart] = part.trim().split(";");
50
+ const q = qPart ? parseFloat(qPart.replace("q=", "")) : 1;
51
+ return { lang: lang?.trim() ?? "", q };
52
+ })
53
+ .sort((a, b) => b.q - a.q);
54
+
55
+ for (const { lang } of languages) {
56
+ // Direct match
57
+ if (isLocale(lang)) {
58
+ return lang;
59
+ }
60
+
61
+ // Map common variants
62
+ const normalized = lang.toLowerCase();
63
+ if (normalized.startsWith("zh-cn") || normalized.startsWith("zh-hans")) {
64
+ return "zh-Hans";
65
+ }
66
+ if (
67
+ normalized.startsWith("zh-tw") ||
68
+ normalized.startsWith("zh-hk") ||
69
+ normalized.startsWith("zh-hant")
70
+ ) {
71
+ return "zh-Hant";
72
+ }
73
+ if (normalized.startsWith("zh")) {
74
+ return "zh-Hans"; // Default Chinese to Simplified
75
+ }
76
+ if (normalized.startsWith("en")) {
77
+ return "en";
78
+ }
79
+ }
80
+
81
+ return baseLocale;
82
+ }
83
+
84
+ /**
85
+ * Detect user's preferred language from Hono context
86
+ * Priority: Cookie > Accept-Language header > Default
87
+ */
88
+ export function detectLanguage(c: Context): Locale {
89
+ // 1. Check cookie (using getCookie helper)
90
+ const cookies = c.req.raw.headers.get("Cookie") ?? "";
91
+ const cookieMatch = cookies.match(new RegExp(`${LANGUAGE_COOKIE_NAME}=([^;]+)`));
92
+ const cookieLang = cookieMatch?.[1];
93
+ if (cookieLang && isLocale(cookieLang)) {
94
+ return cookieLang;
95
+ }
96
+
97
+ // 2. Check Accept-Language header
98
+ const acceptLang = c.req.header("Accept-Language") ?? null;
99
+ return parseAcceptLanguage(acceptLang);
100
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * i18n Runtime using @lingui/core with Lingui macros
3
+ *
4
+ * Usage:
5
+ * import { msg } from "@lingui/core/macro";
6
+ * import { bindI18n } from "@/i18n";
7
+ *
8
+ * const { i18n } = bindI18n(c.get("i18n"));
9
+ *
10
+ * // Simple message
11
+ * i18n._(msg({ message: "Hello", comment: "@context: Greeting" }))
12
+ *
13
+ * // With interpolation
14
+ * i18n._(msg({ message: "Welcome, {name}!", comment: "@context: Welcome" }), { name })
15
+ *
16
+ * The msg macro generates hash-based IDs at compile time, which match the compiled catalogs.
17
+ */
18
+
19
+ import { I18n } from "@lingui/core";
20
+ import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
21
+ import { messages as messagesEn } from "./locales/en.js";
22
+ import { messages as messagesZhHans } from "./locales/zh-Hans.js";
23
+ import { messages as messagesZhHant } from "./locales/zh-Hant.js";
24
+
25
+ export { locales, baseLocale, isLocale, type Locale };
26
+
27
+ // Export I18n type for convenience
28
+ export type { I18n };
29
+
30
+ /**
31
+ * Create a new i18n instance for a specific locale.
32
+ * IMPORTANT: In Cloudflare Workers (concurrent environment), we must create
33
+ * a new instance per request to avoid race conditions. Never use a global instance!
34
+ */
35
+ export function createI18n(locale: Locale): I18n {
36
+ const i18n = new I18n({});
37
+
38
+ // Load all catalogs with English as fallback
39
+ i18n.load("en", messagesEn);
40
+ i18n.load("zh-Hans", { ...messagesEn, ...messagesZhHans });
41
+ i18n.load("zh-Hant", { ...messagesEn, ...messagesZhHant });
42
+
43
+ // Activate locale after loading messages to avoid warnings
44
+ i18n.activate(locale);
45
+
46
+ return i18n;
47
+ }
48
+
49
+ /**
50
+ * Helper to get the per-request i18n instance from Hono context.
51
+ * Use this in route handlers.
52
+ *
53
+ * @example
54
+ * import { msg } from "@lingui/core/macro";
55
+ * import { getI18n } from "@/i18n";
56
+ *
57
+ * const i18n = getI18n(c);
58
+ * const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
59
+ */
60
+ export function getI18n(c: { get(key: "i18n"): I18n }): I18n {
61
+ return c.get("i18n");
62
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * i18n Module
3
+ *
4
+ * IMPORTANT: This module is designed for concurrent environments (Cloudflare Workers).
5
+ * We create a new i18n instance per request to avoid race conditions.
6
+ *
7
+ * This is a custom implementation compatible with Hono JSX (SSR), not React.
8
+ * It provides a React-like API using Lingui macros with a custom context system.
9
+ *
10
+ * Usage:
11
+ * ```tsx
12
+ * import { useLingui, Trans, I18nProvider } from "@/i18n";
13
+ *
14
+ * // Wrap your app in I18nProvider (automatically done by BaseLayout when c is provided)
15
+ * c.html(
16
+ * <I18nProvider c={c}>
17
+ * <MyApp />
18
+ * </I18nProvider>
19
+ * );
20
+ *
21
+ * // Inside components, use useLingui() hook
22
+ * function MyApp() {
23
+ * const { t } = useLingui();
24
+ *
25
+ * return (
26
+ * <div>
27
+ * <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
28
+ * <Trans comment="@context: Help text">
29
+ * Read the <a href="/docs">documentation</a>
30
+ * </Trans>
31
+ * </div>
32
+ * );
33
+ * }
34
+ * ```
35
+ */
36
+
37
+ // Core i18n runtime
38
+ export {
39
+ createI18n,
40
+ getI18n,
41
+ locales,
42
+ baseLocale,
43
+ isLocale,
44
+ type Locale,
45
+ type I18n,
46
+ } from "./i18n.js";
47
+
48
+ // I18nProvider and useLingui hook (custom implementation for Hono JSX, SSR-compatible)
49
+ export { I18nProvider, useLingui } from "./context.js";
50
+
51
+ // Trans component (simplified for Hono JSX)
52
+ export { Trans } from "./Trans.js";
53
+
54
+ // Language detection utilities
55
+ export {
56
+ detectLanguage,
57
+ isValidLanguage,
58
+ parseAcceptLanguage,
59
+ getLanguageDisplayName,
60
+ getSupportedLanguages,
61
+ LANGUAGE_COOKIE_NAME,
62
+ } from "./detect.js";
63
+
64
+ // Hono middleware
65
+ export { i18nMiddleware } from "./middleware.js";