@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.
- package/dist/client.d.ts +4 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -2
- package/dist/lib/assets.d.ts +4 -3
- package/dist/lib/assets.d.ts.map +1 -1
- package/dist/lib/assets.js +1 -3
- package/dist/theme/layouts/BaseLayout.js +0 -5
- package/package.json +4 -5
- package/src/app.tsx +377 -0
- package/src/auth.ts +38 -0
- package/src/client.ts +7 -2
- package/src/db/index.ts +14 -0
- package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
- package/src/db/migrations/0001_add_search_fts.sql +40 -0
- package/src/db/migrations/0002_collection_path.sql +2 -0
- package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
- package/src/db/migrations/0004_media_uuid.sql +35 -0
- package/src/db/migrations/meta/0000_snapshot.json +784 -0
- package/src/db/migrations/meta/_journal.json +41 -0
- package/src/db/schema.ts +159 -0
- package/src/i18n/EXAMPLES.md +235 -0
- package/src/i18n/README.md +296 -0
- package/src/i18n/Trans.tsx +31 -0
- package/src/i18n/context.tsx +101 -0
- package/src/i18n/detect.ts +100 -0
- package/src/i18n/i18n.ts +62 -0
- package/src/i18n/index.ts +65 -0
- package/src/i18n/locales/en.po +875 -0
- package/src/i18n/locales/en.ts +1 -0
- package/src/i18n/locales/zh-Hans.po +875 -0
- package/src/i18n/locales/zh-Hans.ts +1 -0
- package/src/i18n/locales/zh-Hant.po +875 -0
- package/src/i18n/locales/zh-Hant.ts +1 -0
- package/src/i18n/locales.ts +14 -0
- package/src/i18n/middleware.ts +59 -0
- package/src/index.ts +42 -0
- package/src/lib/assets.ts +49 -0
- package/src/lib/constants.ts +67 -0
- package/src/lib/image.ts +107 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/markdown.ts +93 -0
- package/src/lib/schemas.ts +92 -0
- package/src/lib/sqid.ts +79 -0
- package/src/lib/sse.ts +152 -0
- package/src/lib/time.ts +117 -0
- package/src/lib/url.ts +107 -0
- package/src/middleware/auth.ts +59 -0
- package/src/preset.css +2 -11
- package/src/routes/api/posts.ts +127 -0
- package/src/routes/api/search.ts +53 -0
- package/src/routes/api/upload.ts +240 -0
- package/src/routes/dash/collections.tsx +341 -0
- package/src/routes/dash/index.tsx +89 -0
- package/src/routes/dash/media.tsx +551 -0
- package/src/routes/dash/pages.tsx +245 -0
- package/src/routes/dash/posts.tsx +202 -0
- package/src/routes/dash/redirects.tsx +155 -0
- package/src/routes/dash/settings.tsx +93 -0
- package/src/routes/feed/rss.ts +119 -0
- package/src/routes/feed/sitemap.ts +75 -0
- package/src/routes/pages/archive.tsx +223 -0
- package/src/routes/pages/collection.tsx +79 -0
- package/src/routes/pages/home.tsx +93 -0
- package/src/routes/pages/page.tsx +64 -0
- package/src/routes/pages/post.tsx +81 -0
- package/src/routes/pages/search.tsx +162 -0
- package/src/services/collection.ts +180 -0
- package/src/services/index.ts +40 -0
- package/src/services/media.ts +97 -0
- package/src/services/post.ts +279 -0
- package/src/services/redirect.ts +74 -0
- package/src/services/search.ts +117 -0
- package/src/services/settings.ts +76 -0
- package/src/theme/components/ActionButtons.tsx +98 -0
- package/src/theme/components/CrudPageHeader.tsx +48 -0
- package/src/theme/components/DangerZone.tsx +77 -0
- package/src/theme/components/EmptyState.tsx +56 -0
- package/src/theme/components/ListItemRow.tsx +24 -0
- package/src/theme/components/PageForm.tsx +114 -0
- package/src/theme/components/Pagination.tsx +196 -0
- package/src/theme/components/PostForm.tsx +122 -0
- package/src/theme/components/PostList.tsx +68 -0
- package/src/theme/components/ThreadView.tsx +118 -0
- package/src/theme/components/TypeBadge.tsx +28 -0
- package/src/theme/components/VisibilityBadge.tsx +33 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/index.ts +24 -0
- package/src/theme/layouts/BaseLayout.tsx +50 -0
- package/src/theme/layouts/DashLayout.tsx +108 -0
- package/src/theme/layouts/index.ts +2 -0
- package/src/types.ts +222 -0
- 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
|
+
}
|
package/src/i18n/i18n.ts
ADDED
|
@@ -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";
|