@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.
Files changed (153) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +112 -85
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -1
  6. package/dist/client.js +1 -1
  7. package/dist/db/schema.d.ts.map +1 -1
  8. package/dist/i18n/context.d.ts.map +1 -1
  9. package/dist/i18n/context.js +0 -3
  10. package/dist/i18n/detect.d.ts +0 -11
  11. package/dist/i18n/detect.d.ts.map +1 -1
  12. package/dist/i18n/detect.js +1 -52
  13. package/dist/i18n/i18n.d.ts +4 -14
  14. package/dist/i18n/i18n.d.ts.map +1 -1
  15. package/dist/i18n/i18n.js +19 -25
  16. package/dist/i18n/index.d.ts +1 -1
  17. package/dist/i18n/index.d.ts.map +1 -1
  18. package/dist/i18n/index.js +1 -1
  19. package/dist/i18n/middleware.d.ts +2 -5
  20. package/dist/i18n/middleware.d.ts.map +1 -1
  21. package/dist/i18n/middleware.js +12 -23
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/schemas.d.ts.map +1 -1
  24. package/dist/lib/sse.d.ts +45 -17
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +77 -37
  27. package/dist/middleware/auth.d.ts.map +1 -1
  28. package/dist/routes/api/posts.js +0 -1
  29. package/dist/routes/api/upload.js +13 -3
  30. package/dist/routes/dash/collections.d.ts.map +1 -1
  31. package/dist/routes/dash/collections.js +134 -142
  32. package/dist/routes/dash/index.js +25 -25
  33. package/dist/routes/dash/media.d.ts.map +1 -1
  34. package/dist/routes/dash/media.js +60 -56
  35. package/dist/routes/dash/pages.d.ts.map +1 -1
  36. package/dist/routes/dash/pages.js +64 -66
  37. package/dist/routes/dash/posts.d.ts.map +1 -1
  38. package/dist/routes/dash/posts.js +50 -59
  39. package/dist/routes/dash/redirects.d.ts.map +1 -1
  40. package/dist/routes/dash/redirects.js +63 -60
  41. package/dist/routes/dash/settings.d.ts.map +1 -1
  42. package/dist/routes/dash/settings.js +249 -93
  43. package/dist/routes/feed/rss.js +6 -4
  44. package/dist/routes/pages/archive.js +60 -62
  45. package/dist/routes/pages/collection.js +8 -8
  46. package/dist/routes/pages/home.js +14 -14
  47. package/dist/routes/pages/page.js +7 -6
  48. package/dist/routes/pages/post.js +8 -8
  49. package/dist/routes/pages/search.js +25 -27
  50. package/dist/services/collection.d.ts.map +1 -1
  51. package/dist/services/index.d.ts.map +1 -1
  52. package/dist/services/media.d.ts.map +1 -1
  53. package/dist/services/post.d.ts.map +1 -1
  54. package/dist/services/redirect.d.ts.map +1 -1
  55. package/dist/services/settings.d.ts.map +1 -1
  56. package/dist/theme/components/ActionButtons.d.ts +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  58. package/dist/theme/components/ActionButtons.js +17 -21
  59. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  60. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.js +12 -15
  62. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  63. package/dist/theme/components/PageForm.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.js +58 -56
  65. package/dist/theme/components/Pagination.d.ts.map +1 -1
  66. package/dist/theme/components/Pagination.js +22 -25
  67. package/dist/theme/components/PostForm.d.ts +0 -1
  68. package/dist/theme/components/PostForm.d.ts.map +1 -1
  69. package/dist/theme/components/PostForm.js +85 -77
  70. package/dist/theme/components/PostList.d.ts.map +1 -1
  71. package/dist/theme/components/PostList.js +17 -17
  72. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  73. package/dist/theme/components/ThreadView.js +15 -18
  74. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  75. package/dist/theme/components/TypeBadge.js +20 -20
  76. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  77. package/dist/theme/components/VisibilityBadge.js +14 -14
  78. package/dist/theme/components/index.d.ts +2 -2
  79. package/dist/theme/components/index.d.ts.map +1 -1
  80. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.js +4 -2
  82. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  83. package/dist/theme/layouts/DashLayout.js +29 -29
  84. package/dist/types/lingui-react-macro.d.js +9 -0
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/vendor/datastar.js +1606 -0
  88. package/package.json +7 -15
  89. package/src/app.tsx +222 -59
  90. package/src/auth.ts +5 -1
  91. package/src/client.ts +1 -1
  92. package/src/db/migrations/meta/0000_snapshot.json +16 -47
  93. package/src/db/migrations/meta/_journal.json +1 -1
  94. package/src/db/schema.ts +22 -7
  95. package/src/i18n/EXAMPLES.md +45 -23
  96. package/src/i18n/README.md +39 -25
  97. package/src/i18n/context.tsx +1 -4
  98. package/src/i18n/detect.ts +1 -67
  99. package/src/i18n/i18n.ts +15 -19
  100. package/src/i18n/index.ts +0 -3
  101. package/src/i18n/middleware.ts +12 -24
  102. package/src/lib/constants.ts +2 -1
  103. package/src/lib/image-processor.ts +14 -6
  104. package/src/lib/image.ts +2 -2
  105. package/src/lib/schemas.ts +7 -3
  106. package/src/lib/sse.ts +133 -51
  107. package/src/middleware/auth.ts +6 -2
  108. package/src/routes/api/posts.ts +9 -9
  109. package/src/routes/api/upload.ts +39 -10
  110. package/src/routes/dash/collections.tsx +249 -81
  111. package/src/routes/dash/index.tsx +22 -7
  112. package/src/routes/dash/media.tsx +94 -24
  113. package/src/routes/dash/pages.tsx +132 -54
  114. package/src/routes/dash/posts.tsx +99 -57
  115. package/src/routes/dash/redirects.tsx +117 -36
  116. package/src/routes/dash/settings.tsx +268 -55
  117. package/src/routes/feed/rss.ts +6 -4
  118. package/src/routes/pages/archive.tsx +78 -24
  119. package/src/routes/pages/collection.tsx +32 -8
  120. package/src/routes/pages/home.tsx +38 -10
  121. package/src/routes/pages/page.tsx +15 -5
  122. package/src/routes/pages/post.tsx +17 -6
  123. package/src/routes/pages/search.tsx +50 -13
  124. package/src/services/collection.ts +29 -8
  125. package/src/services/index.ts +4 -1
  126. package/src/services/media.ts +15 -3
  127. package/src/services/post.ts +37 -10
  128. package/src/services/redirect.ts +4 -1
  129. package/src/services/settings.ts +14 -3
  130. package/src/theme/components/ActionButtons.tsx +31 -15
  131. package/src/theme/components/CrudPageHeader.tsx +3 -4
  132. package/src/theme/components/DangerZone.tsx +19 -13
  133. package/src/theme/components/EmptyState.tsx +1 -5
  134. package/src/theme/components/PageForm.tsx +80 -25
  135. package/src/theme/components/Pagination.tsx +34 -31
  136. package/src/theme/components/PostForm.tsx +91 -27
  137. package/src/theme/components/PostList.tsx +23 -6
  138. package/src/theme/components/ThreadView.tsx +25 -10
  139. package/src/theme/components/TypeBadge.tsx +13 -4
  140. package/src/theme/components/VisibilityBadge.tsx +17 -5
  141. package/src/theme/components/index.ts +12 -2
  142. package/src/theme/layouts/BaseLayout.tsx +6 -5
  143. package/src/theme/layouts/DashLayout.tsx +71 -18
  144. package/src/types/lingui-react-macro.d.ts +34 -0
  145. package/src/types.ts +16 -4
  146. package/src/vendor/datastar.js +9 -0
  147. package/src/vendor/datastar.js.map +7 -0
  148. package/dist/plugin.d.ts +0 -3
  149. package/dist/plugin.d.ts.map +0 -1
  150. package/dist/plugin.js +0 -20
  151. package/dist/tailwind.d.ts +0 -12
  152. package/dist/tailwind.d.ts.map +0 -1
  153. 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 { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";
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", { enum: ["note", "article", "link", "quote", "image", "page"] }).notNull(),
16
- visibility: text("visibility", { enum: ["featured", "quiet", "unlisted", "draft"] })
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" }).notNull().default(false),
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", { mode: "timestamp" }),
145
- refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
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(),
@@ -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 <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>;
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({ postCount, username }: { postCount: number; username: string }) {
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
- { message: "Welcome back, {name}!", comment: "@context: Welcome message" },
73
- { name: username }
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
- { message: "You have {count} posts", comment: "@context: Post count" },
81
- { count: postCount }
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 <a href="/docs" class="underline">documentation</a> for help
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
- { message: "Visit {linkStart}our website{linkEnd}", comment: "@context: Website link" },
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: '</a>',
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} /> {/* Must pass c prop */}
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 /> {/* No props needed */}
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?
@@ -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
- { message: "You have {count} posts", comment: "@context: Post count message" },
81
- { count: postCount }
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 <a href="/docs" class="underline">documentation</a>
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} /> {/* Need to pass c prop */}
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 /> {/* No need to pass c prop */}
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(); // Like React hook
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 />) // useLingui() inside App won't find i18n context
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({ message: "Hello {name}", comment: "@context: Greeting" }, { name: "Alice" })
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({ message: "Hello {name}", comment: "@context: Greeting", values: { name: "Alice" } })
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; // Hono 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; // Lingui i18n instance
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; // @context comment for translators
267
- id?: string; // Optional message ID
268
- children: JSX.Element; // JSX content with embedded elements
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
 
@@ -79,14 +79,11 @@ export function useLingui() {
79
79
  if (!currentI18n) {
80
80
  throw new Error(
81
81
  "useLingui() called outside of I18nProvider. " +
82
- "Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>"
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
  };
@@ -2,10 +2,7 @@
2
2
  * Language Detection Utilities
3
3
  */
4
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";
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 with Lingui macros
2
+ * i18n Runtime using @lingui/core
3
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.
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
- // 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 });
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
@@ -4,8 +4,7 @@
4
4
 
5
5
  import type { MiddlewareHandler } from "hono";
6
6
  import type { I18n } from "@lingui/core";
7
- import { detectLanguage } from "./detect.js";
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 detection priority:
23
- * 1. Cookie (user preference)
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
- // First try cookie and Accept-Language header
31
- let lang = detectLanguage(c);
26
+ let lang: Locale = baseLocale;
32
27
 
33
- // If no cookie is set, check database SITE_LANGUAGE setting
34
- const cookies = c.req.raw.headers.get("Cookie") ?? "";
35
- const hasCookie = cookies.includes("lang=");
36
-
37
- if (!hasCookie) {
38
- // Check database setting
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
 
@@ -64,4 +64,5 @@ export const ONBOARDING_STATUS = {
64
64
  COMPLETED: "completed",
65
65
  } as const;
66
66
 
67
- export type OnboardingStatus = (typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];
67
+ export type OnboardingStatus =
68
+ (typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];