@jant/core 0.2.11 → 0.2.13
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/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +13 -3
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -25
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.d.ts.map +1 -1
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +2 -2
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +7 -15
- package/src/app.tsx +222 -59
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +45 -23
- package/src/i18n/README.md +39 -25
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +14 -6
- package/src/lib/image.ts +2 -2
- package/src/lib/schemas.ts +7 -3
- package/src/lib/sse.ts +133 -51
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +9 -9
- package/src/routes/api/upload.ts +39 -10
- package/src/routes/dash/collections.tsx +249 -81
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +94 -24
- package/src/routes/dash/pages.tsx +132 -54
- package/src/routes/dash/posts.tsx +99 -57
- package/src/routes/dash/redirects.tsx +117 -36
- package/src/routes/dash/settings.tsx +268 -55
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +78 -24
- package/src/routes/pages/collection.tsx +32 -8
- package/src/routes/pages/home.tsx +38 -10
- package/src/routes/pages/page.tsx +15 -5
- package/src/routes/pages/post.tsx +17 -6
- package/src/routes/pages/search.tsx +50 -13
- package/src/services/collection.ts +29 -8
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +37 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +31 -15
- package/src/theme/components/CrudPageHeader.tsx +3 -4
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +1 -5
- package/src/theme/components/PageForm.tsx +80 -25
- package/src/theme/components/Pagination.tsx +34 -31
- package/src/theme/components/PostForm.tsx +91 -27
- package/src/theme/components/PostList.tsx +23 -6
- package/src/theme/components/ThreadView.tsx +25 -10
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +12 -2
- package/src/theme/layouts/BaseLayout.tsx +6 -5
- package/src/theme/layouts/DashLayout.tsx +71 -18
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -4
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -20
- package/dist/tailwind.d.ts +0 -12
- package/dist/tailwind.d.ts.map +0 -1
- package/dist/tailwind.js +0 -15
package/src/db/schema.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Database schema for Jant
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
sqliteTable,
|
|
9
|
+
text,
|
|
10
|
+
integer,
|
|
11
|
+
primaryKey,
|
|
12
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
13
|
|
|
9
14
|
// =============================================================================
|
|
10
15
|
// Posts
|
|
@@ -12,8 +17,12 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
|
|
12
17
|
|
|
13
18
|
export const posts = sqliteTable("posts", {
|
|
14
19
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
15
|
-
type: text("type", {
|
|
16
|
-
|
|
20
|
+
type: text("type", {
|
|
21
|
+
enum: ["note", "article", "link", "quote", "image", "page"],
|
|
22
|
+
}).notNull(),
|
|
23
|
+
visibility: text("visibility", {
|
|
24
|
+
enum: ["featured", "quiet", "unlisted", "draft"],
|
|
25
|
+
})
|
|
17
26
|
.notNull()
|
|
18
27
|
.default("quiet"),
|
|
19
28
|
title: text("title"),
|
|
@@ -77,7 +86,7 @@ export const postCollections = sqliteTable(
|
|
|
77
86
|
.references(() => collections.id),
|
|
78
87
|
addedAt: integer("added_at").notNull(),
|
|
79
88
|
},
|
|
80
|
-
(table) => [primaryKey({ columns: [table.postId, table.collectionId] })]
|
|
89
|
+
(table) => [primaryKey({ columns: [table.postId, table.collectionId] })],
|
|
81
90
|
);
|
|
82
91
|
|
|
83
92
|
// =============================================================================
|
|
@@ -111,7 +120,9 @@ export const user = sqliteTable("user", {
|
|
|
111
120
|
id: text("id").primaryKey(),
|
|
112
121
|
name: text("name").notNull(),
|
|
113
122
|
email: text("email").notNull().unique(),
|
|
114
|
-
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
123
|
+
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
124
|
+
.notNull()
|
|
125
|
+
.default(false),
|
|
115
126
|
image: text("image"),
|
|
116
127
|
role: text("role").default("admin"),
|
|
117
128
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
@@ -141,8 +152,12 @@ export const account = sqliteTable("account", {
|
|
|
141
152
|
accessToken: text("access_token"),
|
|
142
153
|
refreshToken: text("refresh_token"),
|
|
143
154
|
idToken: text("id_token"),
|
|
144
|
-
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
145
|
-
|
|
155
|
+
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
156
|
+
mode: "timestamp",
|
|
157
|
+
}),
|
|
158
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
159
|
+
mode: "timestamp",
|
|
160
|
+
}),
|
|
146
161
|
scope: text("scope"),
|
|
147
162
|
password: text("password"),
|
|
148
163
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
package/src/i18n/EXAMPLES.md
CHANGED
|
@@ -14,7 +14,7 @@ dashRoute.get("/", async (c) => {
|
|
|
14
14
|
return c.html(
|
|
15
15
|
<I18nProvider c={c}>
|
|
16
16
|
<MyApp />
|
|
17
|
-
</I18nProvider
|
|
17
|
+
</I18nProvider>,
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -22,7 +22,9 @@ dashRoute.get("/", async (c) => {
|
|
|
22
22
|
function MyApp() {
|
|
23
23
|
const { t } = useLingui();
|
|
24
24
|
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
<h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
|
|
27
|
+
);
|
|
26
28
|
}
|
|
27
29
|
```
|
|
28
30
|
|
|
@@ -32,11 +34,11 @@ The `comment` field provides context for AI translators, improving translation q
|
|
|
32
34
|
|
|
33
35
|
```tsx
|
|
34
36
|
// ✅ Good - clear context
|
|
35
|
-
t({ message: "Dashboard", comment: "@context: Page title" })
|
|
36
|
-
t({ message: "Dashboard", comment: "@context: Navigation link" })
|
|
37
|
+
t({ message: "Dashboard", comment: "@context: Page title" });
|
|
38
|
+
t({ message: "Dashboard", comment: "@context: Navigation link" });
|
|
37
39
|
|
|
38
40
|
// ❌ Bad - no context (translator might choose wrong word)
|
|
39
|
-
t({ message: "Dashboard" })
|
|
41
|
+
t({ message: "Dashboard" });
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
---
|
|
@@ -53,12 +55,18 @@ dashRoute.get("/", async (c) => {
|
|
|
53
55
|
return c.html(
|
|
54
56
|
<I18nProvider c={c}>
|
|
55
57
|
<Dashboard postCount={posts.length} username="Alice" />
|
|
56
|
-
</I18nProvider
|
|
58
|
+
</I18nProvider>,
|
|
57
59
|
);
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
// Component: use useLingui() hook
|
|
61
|
-
function Dashboard({
|
|
63
|
+
function Dashboard({
|
|
64
|
+
postCount,
|
|
65
|
+
username,
|
|
66
|
+
}: {
|
|
67
|
+
postCount: number;
|
|
68
|
+
username: string;
|
|
69
|
+
}) {
|
|
62
70
|
const { t } = useLingui();
|
|
63
71
|
|
|
64
72
|
return (
|
|
@@ -69,23 +77,33 @@ function Dashboard({ postCount, username }: { postCount: number; username: strin
|
|
|
69
77
|
{/* 2. With variables */}
|
|
70
78
|
<p>
|
|
71
79
|
{t(
|
|
72
|
-
{
|
|
73
|
-
|
|
80
|
+
{
|
|
81
|
+
message: "Welcome back, {name}!",
|
|
82
|
+
comment: "@context: Welcome message",
|
|
83
|
+
},
|
|
84
|
+
{ name: username },
|
|
74
85
|
)}
|
|
75
86
|
</p>
|
|
76
87
|
|
|
77
88
|
{/* 3. With dynamic values */}
|
|
78
89
|
<p>
|
|
79
90
|
{t(
|
|
80
|
-
{
|
|
81
|
-
|
|
91
|
+
{
|
|
92
|
+
message: "You have {count} posts",
|
|
93
|
+
comment: "@context: Post count",
|
|
94
|
+
},
|
|
95
|
+
{ count: postCount },
|
|
82
96
|
)}
|
|
83
97
|
</p>
|
|
84
98
|
|
|
85
99
|
{/* 4. With embedded components */}
|
|
86
100
|
<p>
|
|
87
101
|
<Trans comment="@context: Help text">
|
|
88
|
-
Read the
|
|
102
|
+
Read the{" "}
|
|
103
|
+
<a href="/docs" class="underline">
|
|
104
|
+
documentation
|
|
105
|
+
</a>{" "}
|
|
106
|
+
for help
|
|
89
107
|
</Trans>
|
|
90
108
|
</p>
|
|
91
109
|
</div>
|
|
@@ -126,13 +144,16 @@ const { t } = useLingui();
|
|
|
126
144
|
// For dynamic content, use t() with placeholders
|
|
127
145
|
<p>
|
|
128
146
|
{t(
|
|
129
|
-
{
|
|
147
|
+
{
|
|
148
|
+
message: "Visit {linkStart}our website{linkEnd}",
|
|
149
|
+
comment: "@context: Website link",
|
|
150
|
+
},
|
|
130
151
|
{
|
|
131
152
|
linkStart: '<a href="/" class="text-primary">',
|
|
132
|
-
linkEnd:
|
|
133
|
-
}
|
|
153
|
+
linkEnd: "</a>",
|
|
154
|
+
},
|
|
134
155
|
)}
|
|
135
|
-
</p
|
|
156
|
+
</p>;
|
|
136
157
|
```
|
|
137
158
|
|
|
138
159
|
---
|
|
@@ -149,8 +170,8 @@ dashRoute.get("/", async (c) => {
|
|
|
149
170
|
|
|
150
171
|
return c.html(
|
|
151
172
|
<Layout>
|
|
152
|
-
<MyComponent c={c} />
|
|
153
|
-
</Layout
|
|
173
|
+
<MyComponent c={c} /> {/* Must pass c prop */}
|
|
174
|
+
</Layout>,
|
|
154
175
|
);
|
|
155
176
|
});
|
|
156
177
|
|
|
@@ -159,7 +180,7 @@ function MyComponent({ c }: { c: Context }) {
|
|
|
159
180
|
const title = i18n._({ message: "Dashboard", comment: "@context: Title" });
|
|
160
181
|
const greeting = i18n._(
|
|
161
182
|
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
162
|
-
{ name: "Alice" }
|
|
183
|
+
{ name: "Alice" },
|
|
163
184
|
);
|
|
164
185
|
|
|
165
186
|
return <h1>{title}</h1>;
|
|
@@ -175,9 +196,9 @@ dashRoute.get("/", async (c) => {
|
|
|
175
196
|
return c.html(
|
|
176
197
|
<I18nProvider c={c}>
|
|
177
198
|
<Layout>
|
|
178
|
-
<MyComponent />
|
|
199
|
+
<MyComponent /> {/* No props needed */}
|
|
179
200
|
</Layout>
|
|
180
|
-
</I18nProvider
|
|
201
|
+
</I18nProvider>,
|
|
181
202
|
);
|
|
182
203
|
});
|
|
183
204
|
|
|
@@ -186,7 +207,7 @@ function MyComponent() {
|
|
|
186
207
|
const title = t({ message: "Dashboard", comment: "@context: Title" });
|
|
187
208
|
const greeting = t(
|
|
188
209
|
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
189
|
-
{ name: "Alice" }
|
|
210
|
+
{ name: "Alice" },
|
|
190
211
|
);
|
|
191
212
|
|
|
192
213
|
return <h1>{title}</h1>;
|
|
@@ -214,8 +235,9 @@ function MyComponent() {
|
|
|
214
235
|
A: Lingui uses a build-time extraction process. The `t()` function expects a message descriptor object that gets transformed by Lingui's macro system during the build. If you pass a plain string, the extraction tool won't be able to find and extract the message for translation.
|
|
215
236
|
|
|
216
237
|
You must use the object syntax:
|
|
238
|
+
|
|
217
239
|
```tsx
|
|
218
|
-
t({ message: "Dashboard", comment: "@context: Page title" })
|
|
240
|
+
t({ message: "Dashboard", comment: "@context: Page title" });
|
|
219
241
|
```
|
|
220
242
|
|
|
221
243
|
### Q: Can I use Lingui's official Trans component?
|
package/src/i18n/README.md
CHANGED
|
@@ -14,7 +14,7 @@ dashRoute.get("/", async (c) => {
|
|
|
14
14
|
return c.html(
|
|
15
15
|
<I18nProvider c={c}>
|
|
16
16
|
<YourApp />
|
|
17
|
-
</I18nProvider
|
|
17
|
+
</I18nProvider>,
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
```
|
|
@@ -77,15 +77,21 @@ function DashboardContent({ postCount }: { postCount: number }) {
|
|
|
77
77
|
{/* 2. With variables */}
|
|
78
78
|
<p>
|
|
79
79
|
{t(
|
|
80
|
-
{
|
|
81
|
-
|
|
80
|
+
{
|
|
81
|
+
message: "You have {count} posts",
|
|
82
|
+
comment: "@context: Post count message",
|
|
83
|
+
},
|
|
84
|
+
{ count: postCount },
|
|
82
85
|
)}
|
|
83
86
|
</p>
|
|
84
87
|
|
|
85
88
|
{/* 3. With embedded components - use Trans */}
|
|
86
89
|
<p>
|
|
87
90
|
<Trans comment="@context: Help text">
|
|
88
|
-
Read the
|
|
91
|
+
Read the{" "}
|
|
92
|
+
<a href="/docs" class="underline">
|
|
93
|
+
documentation
|
|
94
|
+
</a>
|
|
89
95
|
</Trans>
|
|
90
96
|
</p>
|
|
91
97
|
</div>
|
|
@@ -99,7 +105,7 @@ dashRoute.get("/", async (c) => {
|
|
|
99
105
|
return c.html(
|
|
100
106
|
<I18nProvider c={c}>
|
|
101
107
|
<DashboardContent postCount={posts.length} />
|
|
102
|
-
</I18nProvider
|
|
108
|
+
</I18nProvider>,
|
|
103
109
|
);
|
|
104
110
|
});
|
|
105
111
|
```
|
|
@@ -118,8 +124,8 @@ dashRoute.get("/", async (c) => {
|
|
|
118
124
|
|
|
119
125
|
return c.html(
|
|
120
126
|
<Layout title={i18n._({ message: "Dashboard", comment: "@context: ..." })}>
|
|
121
|
-
<MyComponent c={c} />
|
|
122
|
-
</Layout
|
|
127
|
+
<MyComponent c={c} /> {/* Need to pass c prop */}
|
|
128
|
+
</Layout>,
|
|
123
129
|
);
|
|
124
130
|
});
|
|
125
131
|
|
|
@@ -138,14 +144,14 @@ dashRoute.get("/", async (c) => {
|
|
|
138
144
|
return c.html(
|
|
139
145
|
<I18nProvider c={c}>
|
|
140
146
|
<Layout>
|
|
141
|
-
<MyComponent />
|
|
147
|
+
<MyComponent /> {/* No need to pass c prop */}
|
|
142
148
|
</Layout>
|
|
143
|
-
</I18nProvider
|
|
149
|
+
</I18nProvider>,
|
|
144
150
|
);
|
|
145
151
|
});
|
|
146
152
|
|
|
147
153
|
function MyComponent() {
|
|
148
|
-
const { t } = useLingui();
|
|
154
|
+
const { t } = useLingui(); // Like React hook
|
|
149
155
|
return <h1>{t({ message: "Hello", comment: "@context: ..." })}</h1>;
|
|
150
156
|
}
|
|
151
157
|
```
|
|
@@ -159,10 +165,10 @@ function MyComponent() {
|
|
|
159
165
|
```tsx
|
|
160
166
|
// ✅ Correct - comment is crucial for AI translation
|
|
161
167
|
const { t } = useLingui();
|
|
162
|
-
t({ message: "Dashboard", comment: "@context: Page title" })
|
|
168
|
+
t({ message: "Dashboard", comment: "@context: Page title" });
|
|
163
169
|
|
|
164
170
|
// ❌ Wrong - missing comment reduces translation quality
|
|
165
|
-
t({ message: "Dashboard" })
|
|
171
|
+
t({ message: "Dashboard" });
|
|
166
172
|
```
|
|
167
173
|
|
|
168
174
|
### 2. **I18nProvider must wrap your app**
|
|
@@ -172,11 +178,11 @@ t({ message: "Dashboard" })
|
|
|
172
178
|
c.html(
|
|
173
179
|
<I18nProvider c={c}>
|
|
174
180
|
<App />
|
|
175
|
-
</I18nProvider
|
|
176
|
-
)
|
|
181
|
+
</I18nProvider>,
|
|
182
|
+
);
|
|
177
183
|
|
|
178
184
|
// ❌ Wrong - useLingui() will throw error
|
|
179
|
-
c.html(<App />)
|
|
185
|
+
c.html(<App />); // useLingui() inside App won't find i18n context
|
|
180
186
|
```
|
|
181
187
|
|
|
182
188
|
### 3. **useLingui() only works inside components**
|
|
@@ -201,10 +207,17 @@ dashRoute.get("/", async (c) => {
|
|
|
201
207
|
const { t } = useLingui();
|
|
202
208
|
|
|
203
209
|
// ✅ Correct - values as second parameter
|
|
204
|
-
t(
|
|
210
|
+
t(
|
|
211
|
+
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
212
|
+
{ name: "Alice" },
|
|
213
|
+
);
|
|
205
214
|
|
|
206
215
|
// ❌ Wrong - values inside first parameter (not supported)
|
|
207
|
-
t({
|
|
216
|
+
t({
|
|
217
|
+
message: "Hello {name}",
|
|
218
|
+
comment: "@context: Greeting",
|
|
219
|
+
values: { name: "Alice" },
|
|
220
|
+
});
|
|
208
221
|
```
|
|
209
222
|
|
|
210
223
|
---
|
|
@@ -227,14 +240,14 @@ Provides i18n context to all child components. Must wrap your app in route handl
|
|
|
227
240
|
|
|
228
241
|
```tsx
|
|
229
242
|
interface I18nProviderProps {
|
|
230
|
-
c: Context;
|
|
243
|
+
c: Context; // Hono context
|
|
231
244
|
children: JSX.Element;
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
// Usage
|
|
235
248
|
<I18nProvider c={c}>
|
|
236
249
|
<YourApp />
|
|
237
|
-
</I18nProvider
|
|
250
|
+
</I18nProvider>;
|
|
238
251
|
```
|
|
239
252
|
|
|
240
253
|
### `useLingui()`
|
|
@@ -243,10 +256,10 @@ Hook to access i18n functionality inside components. Must be used within `I18nPr
|
|
|
243
256
|
|
|
244
257
|
```tsx
|
|
245
258
|
function useLingui(): {
|
|
246
|
-
i18n: I18n;
|
|
259
|
+
i18n: I18n; // Lingui i18n instance
|
|
247
260
|
t: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
|
|
248
261
|
_: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
|
|
249
|
-
}
|
|
262
|
+
};
|
|
250
263
|
|
|
251
264
|
// Usage
|
|
252
265
|
function MyComponent() {
|
|
@@ -263,15 +276,15 @@ Component for translations with embedded JSX elements. Simplified implementation
|
|
|
263
276
|
|
|
264
277
|
```tsx
|
|
265
278
|
interface TransProps {
|
|
266
|
-
comment?: string;
|
|
267
|
-
id?: string;
|
|
268
|
-
children: JSX.Element;
|
|
279
|
+
comment?: string; // @context comment for translators
|
|
280
|
+
id?: string; // Optional message ID
|
|
281
|
+
children: JSX.Element; // JSX content with embedded elements
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
// Usage
|
|
272
285
|
<Trans comment="@context: Help text">
|
|
273
286
|
Read the <a href="/docs">documentation</a>
|
|
274
|
-
</Trans
|
|
287
|
+
</Trans>;
|
|
275
288
|
```
|
|
276
289
|
|
|
277
290
|
**Note**: This is a simplified implementation. For complex translations with dynamic content, use `t()` with placeholders instead.
|
|
@@ -290,6 +303,7 @@ This implementation mimics React's Context API but is optimized for Hono JSX SSR
|
|
|
290
303
|
### Why Global State is Safe
|
|
291
304
|
|
|
292
305
|
Unlike React (client-side with multiple re-renders), Hono JSX renders once per request on the server:
|
|
306
|
+
|
|
293
307
|
- Request arrives → I18nProvider sets global i18n → Components render → Response sent
|
|
294
308
|
- Next request → New i18n instance → Components render → Response sent
|
|
295
309
|
|
package/src/i18n/context.tsx
CHANGED
|
@@ -79,14 +79,11 @@ export function useLingui() {
|
|
|
79
79
|
if (!currentI18n) {
|
|
80
80
|
throw new Error(
|
|
81
81
|
"useLingui() called outside of I18nProvider. " +
|
|
82
|
-
|
|
82
|
+
"Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>",
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// Create translation function that accepts both pre-macro and post-macro formats
|
|
87
86
|
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
87
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- currentI18n is checked above
|
|
91
88
|
return currentI18n!._(descriptor as MessageDescriptor);
|
|
92
89
|
};
|
package/src/i18n/detect.ts
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Language Detection Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
|
|
7
|
-
|
|
8
|
-
export const LANGUAGE_COOKIE_NAME = "lang";
|
|
5
|
+
import { locales, isLocale, type Locale } from "./locales.js";
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* Get display name for a language code
|
|
@@ -35,66 +32,3 @@ export function getSupportedLanguages(): Array<{ code: Locale; name: string }> {
|
|
|
35
32
|
export function isValidLanguage(lang: unknown): lang is Locale {
|
|
36
33
|
return isLocale(lang);
|
|
37
34
|
}
|
|
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
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* i18n Runtime using @lingui/core
|
|
2
|
+
* i18n Runtime using @lingui/core
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
4
|
+
* The SWC Lingui plugin adds hash-based IDs to t() calls when imports come
|
|
5
|
+
* from @lingui/react/macro. The runtimeConfigModule setting rewrites those
|
|
6
|
+
* imports to our custom Hono JSX implementation at build time.
|
|
17
7
|
*/
|
|
18
8
|
|
|
9
|
+
import type { Messages } from "@lingui/core";
|
|
19
10
|
import { I18n } from "@lingui/core";
|
|
20
11
|
import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
|
|
21
12
|
import { messages as messagesEn } from "./locales/en.js";
|
|
@@ -27,6 +18,13 @@ export { locales, baseLocale, isLocale, type Locale };
|
|
|
27
18
|
// Export I18n type for convenience
|
|
28
19
|
export type { I18n };
|
|
29
20
|
|
|
21
|
+
// Pre-compute merged catalogs at module load time (done once, not per request)
|
|
22
|
+
// For non-English locales, merge English as fallback so missing translations
|
|
23
|
+
// fall back to English rather than showing hash IDs.
|
|
24
|
+
const catalogEn: Messages = messagesEn;
|
|
25
|
+
const catalogZhHans: Messages = { ...messagesEn, ...messagesZhHans };
|
|
26
|
+
const catalogZhHant: Messages = { ...messagesEn, ...messagesZhHant };
|
|
27
|
+
|
|
30
28
|
/**
|
|
31
29
|
* Create a new i18n instance for a specific locale.
|
|
32
30
|
* IMPORTANT: In Cloudflare Workers (concurrent environment), we must create
|
|
@@ -35,12 +33,10 @@ export type { I18n };
|
|
|
35
33
|
export function createI18n(locale: Locale): I18n {
|
|
36
34
|
const i18n = new I18n({});
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
i18n.load("
|
|
40
|
-
i18n.load("zh-
|
|
41
|
-
i18n.load("zh-Hant", { ...messagesEn, ...messagesZhHant });
|
|
36
|
+
i18n.load("en", catalogEn);
|
|
37
|
+
i18n.load("zh-Hans", catalogZhHans);
|
|
38
|
+
i18n.load("zh-Hant", catalogZhHant);
|
|
42
39
|
|
|
43
|
-
// Activate locale after loading messages to avoid warnings
|
|
44
40
|
i18n.activate(locale);
|
|
45
41
|
|
|
46
42
|
return i18n;
|
package/src/i18n/index.ts
CHANGED
|
@@ -53,12 +53,9 @@ export { Trans } from "./Trans.js";
|
|
|
53
53
|
|
|
54
54
|
// Language detection utilities
|
|
55
55
|
export {
|
|
56
|
-
detectLanguage,
|
|
57
56
|
isValidLanguage,
|
|
58
|
-
parseAcceptLanguage,
|
|
59
57
|
getLanguageDisplayName,
|
|
60
58
|
getSupportedLanguages,
|
|
61
|
-
LANGUAGE_COOKIE_NAME,
|
|
62
59
|
} from "./detect.js";
|
|
63
60
|
|
|
64
61
|
// Hono middleware
|
package/src/i18n/middleware.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { MiddlewareHandler } from "hono";
|
|
6
6
|
import type { I18n } from "@lingui/core";
|
|
7
|
-
import {
|
|
8
|
-
import { createI18n, isLocale, type Locale } from "./i18n.js";
|
|
7
|
+
import { createI18n, isLocale, baseLocale, type Locale } from "./i18n.js";
|
|
9
8
|
import type { Services } from "../services/index.js";
|
|
10
9
|
|
|
11
10
|
declare module "hono" {
|
|
@@ -19,33 +18,22 @@ declare module "hono" {
|
|
|
19
18
|
* Hono middleware for internationalization.
|
|
20
19
|
* Creates a per-request i18n instance to avoid race conditions in concurrent environments.
|
|
21
20
|
*
|
|
22
|
-
* Language
|
|
23
|
-
*
|
|
24
|
-
* 2. Database SITE_LANGUAGE setting (site default)
|
|
25
|
-
* 3. Accept-Language header
|
|
26
|
-
* 4. Default locale (en)
|
|
21
|
+
* Language is determined by the database SITE_LANGUAGE setting (single source of truth).
|
|
22
|
+
* Falls back to the default locale (en) if not set.
|
|
27
23
|
*/
|
|
28
24
|
export function i18nMiddleware(): MiddlewareHandler {
|
|
29
25
|
return async (c, next) => {
|
|
30
|
-
|
|
31
|
-
let lang = detectLanguage(c);
|
|
26
|
+
let lang: Locale = baseLocale;
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const services = c.get("services") as Services | undefined;
|
|
40
|
-
if (services) {
|
|
41
|
-
try {
|
|
42
|
-
const siteLang = await services.settings.get("SITE_LANGUAGE");
|
|
43
|
-
if (siteLang && isLocale(siteLang)) {
|
|
44
|
-
lang = siteLang;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Ignore errors, fall back to detected language
|
|
28
|
+
const services = c.get("services") as Services | undefined;
|
|
29
|
+
if (services) {
|
|
30
|
+
try {
|
|
31
|
+
const siteLang = await services.settings.get("SITE_LANGUAGE");
|
|
32
|
+
if (siteLang && isLocale(siteLang)) {
|
|
33
|
+
lang = siteLang;
|
|
48
34
|
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore errors, fall back to default locale
|
|
49
37
|
}
|
|
50
38
|
}
|
|
51
39
|
|
package/src/lib/constants.ts
CHANGED
|
@@ -64,4 +64,5 @@ export const ONBOARDING_STATUS = {
|
|
|
64
64
|
COMPLETED: "completed",
|
|
65
65
|
} as const;
|
|
66
66
|
|
|
67
|
-
export type OnboardingStatus =
|
|
67
|
+
export type OnboardingStatus =
|
|
68
|
+
(typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];
|