@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,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "6",
|
|
8
|
+
"when": 1769858252020,
|
|
9
|
+
"tag": "0000_solid_moon_knight",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "6",
|
|
15
|
+
"when": 1769859000000,
|
|
16
|
+
"tag": "0001_add_search_fts",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "6",
|
|
22
|
+
"when": 1769860000000,
|
|
23
|
+
"tag": "0002_collection_path",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "6",
|
|
29
|
+
"when": 1769861000000,
|
|
30
|
+
"tag": "0003_collection_path_nullable",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "6",
|
|
36
|
+
"when": 1770024000000,
|
|
37
|
+
"tag": "0004_media_uuid",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle Schema
|
|
3
|
+
*
|
|
4
|
+
* Database schema for Jant
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Posts
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export const posts = sqliteTable("posts", {
|
|
14
|
+
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"] })
|
|
17
|
+
.notNull()
|
|
18
|
+
.default("quiet"),
|
|
19
|
+
title: text("title"),
|
|
20
|
+
path: text("path"),
|
|
21
|
+
content: text("content"),
|
|
22
|
+
contentHtml: text("content_html"),
|
|
23
|
+
sourceUrl: text("source_url"),
|
|
24
|
+
sourceName: text("source_name"),
|
|
25
|
+
sourceDomain: text("source_domain"),
|
|
26
|
+
replyToId: integer("reply_to_id"),
|
|
27
|
+
threadId: integer("thread_id"),
|
|
28
|
+
deletedAt: integer("deleted_at"),
|
|
29
|
+
publishedAt: integer("published_at").notNull(),
|
|
30
|
+
createdAt: integer("created_at").notNull(),
|
|
31
|
+
updatedAt: integer("updated_at").notNull(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Media
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export const media = sqliteTable("media", {
|
|
39
|
+
id: text("id").primaryKey(), // UUIDv7
|
|
40
|
+
postId: integer("post_id").references(() => posts.id),
|
|
41
|
+
filename: text("filename").notNull(),
|
|
42
|
+
originalName: text("original_name").notNull(),
|
|
43
|
+
mimeType: text("mime_type").notNull(),
|
|
44
|
+
size: integer("size").notNull(),
|
|
45
|
+
r2Key: text("r2_key").notNull(),
|
|
46
|
+
width: integer("width"),
|
|
47
|
+
height: integer("height"),
|
|
48
|
+
alt: text("alt"),
|
|
49
|
+
createdAt: integer("created_at").notNull(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Collections
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
export const collections = sqliteTable("collections", {
|
|
57
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
58
|
+
path: text("path").unique(),
|
|
59
|
+
title: text("title").notNull(),
|
|
60
|
+
description: text("description"),
|
|
61
|
+
createdAt: integer("created_at").notNull(),
|
|
62
|
+
updatedAt: integer("updated_at").notNull(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Post-Collections (Many-to-Many)
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
export const postCollections = sqliteTable(
|
|
70
|
+
"post_collections",
|
|
71
|
+
{
|
|
72
|
+
postId: integer("post_id")
|
|
73
|
+
.notNull()
|
|
74
|
+
.references(() => posts.id),
|
|
75
|
+
collectionId: integer("collection_id")
|
|
76
|
+
.notNull()
|
|
77
|
+
.references(() => collections.id),
|
|
78
|
+
addedAt: integer("added_at").notNull(),
|
|
79
|
+
},
|
|
80
|
+
(table) => [primaryKey({ columns: [table.postId, table.collectionId] })]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Redirects
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export const redirects = sqliteTable("redirects", {
|
|
88
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
89
|
+
fromPath: text("from_path").notNull().unique(),
|
|
90
|
+
toPath: text("to_path").notNull(),
|
|
91
|
+
type: integer("type", { mode: "number" }).notNull().default(301),
|
|
92
|
+
createdAt: integer("created_at").notNull(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Settings (Key-Value)
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export const settings = sqliteTable("settings", {
|
|
100
|
+
key: text("key").primaryKey(),
|
|
101
|
+
value: text("value").notNull(),
|
|
102
|
+
updatedAt: integer("updated_at").notNull(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// better-auth tables
|
|
107
|
+
// Note: Using { mode: "timestamp" } so drizzle auto-converts Date <-> integer
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
export const user = sqliteTable("user", {
|
|
111
|
+
id: text("id").primaryKey(),
|
|
112
|
+
name: text("name").notNull(),
|
|
113
|
+
email: text("email").notNull().unique(),
|
|
114
|
+
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
|
|
115
|
+
image: text("image"),
|
|
116
|
+
role: text("role").default("admin"),
|
|
117
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
118
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const session = sqliteTable("session", {
|
|
122
|
+
id: text("id").primaryKey(),
|
|
123
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
124
|
+
token: text("token").notNull().unique(),
|
|
125
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
126
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
127
|
+
ipAddress: text("ip_address"),
|
|
128
|
+
userAgent: text("user_agent"),
|
|
129
|
+
userId: text("user_id")
|
|
130
|
+
.notNull()
|
|
131
|
+
.references(() => user.id),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const account = sqliteTable("account", {
|
|
135
|
+
id: text("id").primaryKey(),
|
|
136
|
+
accountId: text("account_id").notNull(),
|
|
137
|
+
providerId: text("provider_id").notNull(),
|
|
138
|
+
userId: text("user_id")
|
|
139
|
+
.notNull()
|
|
140
|
+
.references(() => user.id),
|
|
141
|
+
accessToken: text("access_token"),
|
|
142
|
+
refreshToken: text("refresh_token"),
|
|
143
|
+
idToken: text("id_token"),
|
|
144
|
+
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
|
|
145
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
|
|
146
|
+
scope: text("scope"),
|
|
147
|
+
password: text("password"),
|
|
148
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
149
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const verification = sqliteTable("verification", {
|
|
153
|
+
id: text("id").primaryKey(),
|
|
154
|
+
identifier: text("identifier").notNull(),
|
|
155
|
+
value: text("value").notNull(),
|
|
156
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
157
|
+
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
158
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
159
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# i18n Usage Examples
|
|
2
|
+
|
|
3
|
+
## New API: useLingui() Hook
|
|
4
|
+
|
|
5
|
+
We now use a React-like hook API that eliminates prop drilling and makes code cleaner.
|
|
6
|
+
|
|
7
|
+
### Basic Pattern
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { I18nProvider, useLingui } from "@/i18n";
|
|
11
|
+
|
|
12
|
+
// 1. Wrap your app in route handler
|
|
13
|
+
dashRoute.get("/", async (c) => {
|
|
14
|
+
return c.html(
|
|
15
|
+
<I18nProvider c={c}>
|
|
16
|
+
<MyApp />
|
|
17
|
+
</I18nProvider>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 2. Use useLingui() hook inside components
|
|
22
|
+
function MyApp() {
|
|
23
|
+
const { t } = useLingui();
|
|
24
|
+
|
|
25
|
+
return <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Why the `comment` field?
|
|
30
|
+
|
|
31
|
+
The `comment` field provides context for AI translators, improving translation quality:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// ✅ Good - clear context
|
|
35
|
+
t({ message: "Dashboard", comment: "@context: Page title" })
|
|
36
|
+
t({ message: "Dashboard", comment: "@context: Navigation link" })
|
|
37
|
+
|
|
38
|
+
// ❌ Bad - no context (translator might choose wrong word)
|
|
39
|
+
t({ message: "Dashboard" })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Complete Example
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { I18nProvider, useLingui, Trans } from "@/i18n";
|
|
48
|
+
|
|
49
|
+
// Route handler: wrap in I18nProvider
|
|
50
|
+
dashRoute.get("/", async (c) => {
|
|
51
|
+
const posts = await c.var.services.posts.list();
|
|
52
|
+
|
|
53
|
+
return c.html(
|
|
54
|
+
<I18nProvider c={c}>
|
|
55
|
+
<Dashboard postCount={posts.length} username="Alice" />
|
|
56
|
+
</I18nProvider>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Component: use useLingui() hook
|
|
61
|
+
function Dashboard({ postCount, username }: { postCount: number; username: string }) {
|
|
62
|
+
const { t } = useLingui();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
{/* 1. Simple translation */}
|
|
67
|
+
<h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
|
|
68
|
+
|
|
69
|
+
{/* 2. With variables */}
|
|
70
|
+
<p>
|
|
71
|
+
{t(
|
|
72
|
+
{ message: "Welcome back, {name}!", comment: "@context: Welcome message" },
|
|
73
|
+
{ name: username }
|
|
74
|
+
)}
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
{/* 3. With dynamic values */}
|
|
78
|
+
<p>
|
|
79
|
+
{t(
|
|
80
|
+
{ message: "You have {count} posts", comment: "@context: Post count" },
|
|
81
|
+
{ count: postCount }
|
|
82
|
+
)}
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
{/* 4. With embedded components */}
|
|
86
|
+
<p>
|
|
87
|
+
<Trans comment="@context: Help text">
|
|
88
|
+
Read the <a href="/docs" class="underline">documentation</a> for help
|
|
89
|
+
</Trans>
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Trans Component: Embedded JSX
|
|
99
|
+
|
|
100
|
+
The `Trans` component is a simplified implementation that renders children as-is. It's useful for translations with embedded links or formatting.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { Trans } from "@/i18n";
|
|
104
|
+
|
|
105
|
+
// Simple link
|
|
106
|
+
<Trans comment="@context: Website link">
|
|
107
|
+
Visit <a href="/" class="text-primary">our website</a>
|
|
108
|
+
</Trans>
|
|
109
|
+
|
|
110
|
+
// Multiple elements
|
|
111
|
+
<Trans comment="@context: Learn more link">
|
|
112
|
+
Click <strong class="font-bold">here</strong> to <a href="/learn" class="underline">learn more</a>
|
|
113
|
+
</Trans>
|
|
114
|
+
|
|
115
|
+
// With formatting
|
|
116
|
+
<Trans comment="@context: Welcome message">
|
|
117
|
+
Welcome <strong class="font-semibold">back</strong>!
|
|
118
|
+
</Trans>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Note**: This is a simplified implementation that renders children directly. For complex translations with dynamic placeholders, use the `t()` function instead:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const { t } = useLingui();
|
|
125
|
+
|
|
126
|
+
// For dynamic content, use t() with placeholders
|
|
127
|
+
<p>
|
|
128
|
+
{t(
|
|
129
|
+
{ message: "Visit {linkStart}our website{linkEnd}", comment: "@context: Website link" },
|
|
130
|
+
{
|
|
131
|
+
linkStart: '<a href="/" class="text-primary">',
|
|
132
|
+
linkEnd: '</a>',
|
|
133
|
+
}
|
|
134
|
+
)}
|
|
135
|
+
</p>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Comparison: Before vs Now
|
|
141
|
+
|
|
142
|
+
### Before (Prop drilling required)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { getI18n } from "@/i18n";
|
|
146
|
+
|
|
147
|
+
dashRoute.get("/", async (c) => {
|
|
148
|
+
const i18n = getI18n(c);
|
|
149
|
+
|
|
150
|
+
return c.html(
|
|
151
|
+
<Layout>
|
|
152
|
+
<MyComponent c={c} /> {/* Must pass c prop */}
|
|
153
|
+
</Layout>
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
function MyComponent({ c }: { c: Context }) {
|
|
158
|
+
const i18n = getI18n(c);
|
|
159
|
+
const title = i18n._({ message: "Dashboard", comment: "@context: Title" });
|
|
160
|
+
const greeting = i18n._(
|
|
161
|
+
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
162
|
+
{ name: "Alice" }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return <h1>{title}</h1>;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Now (No prop drilling)
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { I18nProvider, useLingui } from "@/i18n";
|
|
173
|
+
|
|
174
|
+
dashRoute.get("/", async (c) => {
|
|
175
|
+
return c.html(
|
|
176
|
+
<I18nProvider c={c}>
|
|
177
|
+
<Layout>
|
|
178
|
+
<MyComponent /> {/* No props needed */}
|
|
179
|
+
</Layout>
|
|
180
|
+
</I18nProvider>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
function MyComponent() {
|
|
185
|
+
const { t } = useLingui();
|
|
186
|
+
const title = t({ message: "Dashboard", comment: "@context: Title" });
|
|
187
|
+
const greeting = t(
|
|
188
|
+
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
189
|
+
{ name: "Alice" }
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return <h1>{title}</h1>;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Best Practices
|
|
199
|
+
|
|
200
|
+
1. **Always wrap in I18nProvider**: Wrap your app in `<I18nProvider c={c}>` in route handlers
|
|
201
|
+
2. **Use useLingui() hook**: Call `const { t } = useLingui()` inside components
|
|
202
|
+
3. **Always include comment**: Provide `@context:` comment for better AI translations
|
|
203
|
+
4. **Variables as second param**: `t({ message: "Hello {name}", comment: "..." }, { name })`
|
|
204
|
+
5. **Use Trans for embedded JSX**: Use `<Trans>` for links and formatting
|
|
205
|
+
6. **Extract translations**: Run `pnpm i18n:extract` after adding new strings
|
|
206
|
+
7. **Compile translations**: Run `pnpm i18n:compile` to generate catalog files
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Common Questions
|
|
211
|
+
|
|
212
|
+
### Q: Why can't I use `t("Dashboard")` directly?
|
|
213
|
+
|
|
214
|
+
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
|
+
|
|
216
|
+
You must use the object syntax:
|
|
217
|
+
```tsx
|
|
218
|
+
t({ message: "Dashboard", comment: "@context: Page title" })
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Q: Can I use Lingui's official Trans component?
|
|
222
|
+
|
|
223
|
+
A: Lingui's `Trans` component is designed for React and requires React Context. Since we use Hono JSX (not React), we provide a simplified `Trans` component that works with our SSR setup. It renders children directly without complex transformation.
|
|
224
|
+
|
|
225
|
+
### Q: Why use useLingui() instead of getI18n(c)?
|
|
226
|
+
|
|
227
|
+
A: The `useLingui()` hook provides a cleaner API that eliminates prop drilling. Instead of passing the Hono context `c` to every component, you wrap your app once in `I18nProvider` and all child components can access i18n via the hook.
|
|
228
|
+
|
|
229
|
+
### Q: Is this safe for concurrent requests?
|
|
230
|
+
|
|
231
|
+
A: Yes! Each request creates its own i18n instance via `I18nProvider`. The global state is only used during the synchronous rendering phase, so there's no risk of race conditions between concurrent requests.
|
|
232
|
+
|
|
233
|
+
### Q: What if I call useLingui() outside I18nProvider?
|
|
234
|
+
|
|
235
|
+
A: You'll get an error: "useLingui() called outside of I18nProvider". This is intentional - the hook must be used within the provider context. Always wrap your app in `<I18nProvider c={c}>` in your route handlers.
|