fimo 0.2.4 → 0.2.5-experimental.1782327181771
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/README.md +2 -2
- package/assets/agent-templates/content-translator/GOAL.md +8 -11
- package/assets/agent-templates/content-translator/capabilities.yaml +1 -3
- package/assets/agent-templates/content-translator/scripts/translate-entries.ts +10 -7
- package/assets/content-templates/hooks-template.eta +73 -23
- package/assets/skills/fimo/SKILL.md +3 -3
- package/assets/skills/fimo/references/forms.md +1 -1
- package/assets/skills/fimo/references/setup-plain-vite.md +1 -1
- package/assets/skills/fimo/references/setup-react-router.md +1 -1
- package/assets/skills/fimo/references/translations.md +42 -14
- package/assets/skills/fimo/references/ui.md +4 -4
- package/assets/skills/fimo-cli/SKILL.md +3 -3
- package/assets/skills/fimo-cli/references/content.md +1 -1
- package/assets/skills/fimo-cli/references/forms.md +1 -1
- package/assets/skills/fimo-cli/references/translations.md +45 -19
- package/dist/build/vite/plugins/fimo-config.d.ts.map +1 -1
- package/dist/build/vite/plugins/fimo-config.js +16 -0
- package/dist/build/vite/plugins/fimo-config.test.d.ts +2 -0
- package/dist/build/vite/plugins/fimo-config.test.d.ts.map +1 -0
- package/dist/build/vite/plugins/fimo-config.test.js +46 -0
- package/dist/build/vite/plugins/translations.d.ts +7 -6
- package/dist/build/vite/plugins/translations.d.ts.map +1 -1
- package/dist/build/vite/plugins/translations.js +366 -33
- package/dist/build/vite/plugins/translations.test.d.ts +2 -0
- package/dist/build/vite/plugins/translations.test.d.ts.map +1 -0
- package/dist/build/vite/plugins/translations.test.js +177 -0
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +1305 -1078
- package/dist/runtime/app/FimoScripts.d.ts.map +1 -1
- package/dist/runtime/app/FimoScripts.js +35 -1
- package/dist/runtime/app/prefetch.d.ts.map +1 -1
- package/dist/runtime/app/prefetch.js +6 -1
- package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +9 -4
- package/dist/runtime/primitives/components/Text.d.ts +1 -1
- package/dist/runtime/primitives/components/Text.js +1 -1
- package/dist/runtime/primitives/lib/query.d.ts +5 -0
- package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/template.d.ts +1 -1
- package/dist/runtime/primitives/lib/template.js +1 -1
- package/dist/runtime/primitives/translations.d.ts +9 -3
- package/dist/runtime/primitives/translations.d.ts.map +1 -1
- package/dist/runtime/primitives/translations.js +32 -5
- package/dist/runtime/seo/htmlProps.d.ts +1 -1
- package/dist/runtime/seo/htmlProps.js +2 -2
- package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- package/dist/runtime/shared/fimo-config.types.d.ts +7 -0
- package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
- package/dist/scripts/extract-translations.d.ts +8 -8
- package/dist/scripts/extract-translations.js +22 -57
- package/dist/scripts/lint-translation-keys.js +24 -5
- package/dist/scripts/lint-translation-keys.test.d.ts +1 -0
- package/dist/scripts/lint-translation-keys.test.js +16 -0
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/fimo-config.json +4 -0
- package/templates/react-router/package.json +1 -1
- package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +0 -66
- package/dist/scripts/inject-translations.d.ts +0 -6
- package/dist/scripts/inject-translations.js +0 -168
- package/templates/react-router/translations/en.json +0 -1
package/README.md
CHANGED
|
@@ -50,13 +50,13 @@ npm install
|
|
|
50
50
|
npm run dev
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Manage content, schemas, media, and
|
|
53
|
+
Manage content, schemas, media, and labels from the CLI:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
fimo schemas push # sync your content models
|
|
57
57
|
fimo entries list # browse content
|
|
58
58
|
fimo assets upload <file> # add media (or `fimo assets generate` with AI)
|
|
59
|
-
fimo
|
|
59
|
+
fimo labels list # manage labels and localized copy
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Deploying
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# Content translator
|
|
2
2
|
|
|
3
|
-
Translate CMS entries from the project's default
|
|
4
|
-
non-default locale declared in `fimo
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
review.
|
|
3
|
+
Translate CMS entries and DB-backed labels from the project's default
|
|
4
|
+
locale into every non-default locale declared in `fimo-config.json`.
|
|
5
|
+
Writes go through Fimo's content and label APIs. Do not create locale
|
|
6
|
+
JSON files or put translated values in source code.
|
|
8
7
|
|
|
9
8
|
## Inputs
|
|
10
9
|
|
|
11
10
|
- The list of CMS entries that have a non-empty default-locale field
|
|
12
11
|
but an empty translation for any non-default locale.
|
|
13
|
-
- The configured target locales (read from `fimo
|
|
12
|
+
- The configured target locales (read from `fimo-config.json`).
|
|
14
13
|
|
|
15
14
|
## What to do
|
|
16
15
|
|
|
@@ -19,16 +18,14 @@ For each entry:
|
|
|
19
18
|
1. Read the default-locale field via `cms:read`.
|
|
20
19
|
2. Translate to every missing locale (via `net:fetch` to your translation
|
|
21
20
|
provider of choice).
|
|
22
|
-
3. Write the translation back to the entry via `cms:write`.
|
|
23
|
-
4.
|
|
21
|
+
3. Write the translation back to the matching locale entry via `cms:write`.
|
|
22
|
+
4. For static UI labels, update values with the i18n/labels API rather
|
|
23
|
+
than editing files.
|
|
24
24
|
|
|
25
25
|
## What NOT to do
|
|
26
26
|
|
|
27
27
|
- Don't translate fields that the entry's schema marks `translate: false`.
|
|
28
28
|
- Don't overwrite an existing translation, even if it looks lower quality.
|
|
29
|
-
- Don't auto-merge to main — the lifecycle engine will propose a PR
|
|
30
|
-
when `agents.auto_merge.enabled` is true and the diff classifies as
|
|
31
|
-
`low_risk` or `trivial`.
|
|
32
29
|
|
|
33
30
|
## v1.x note
|
|
34
31
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
# Reads CMS
|
|
1
|
+
# Reads CMS and writes DB-backed localized content / labels. Net is for the translation provider.
|
|
2
2
|
- cms:read
|
|
3
3
|
- cms:write
|
|
4
4
|
- i18n:read
|
|
5
5
|
- i18n:write
|
|
6
|
-
- files:read
|
|
7
|
-
- files:write
|
|
8
6
|
- net:fetch
|
|
9
7
|
- git:branch
|
|
10
8
|
- git:commit
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool: translate-entries
|
|
3
3
|
*
|
|
4
|
-
* Walk every CMS entry, find
|
|
5
|
-
* call the configured translation provider, and write back
|
|
6
|
-
* Fimo API. Skips fields marked `translate: false` in the schema.
|
|
4
|
+
* Walk every default-locale CMS entry, find missing non-default-locale
|
|
5
|
+
* document rows, call the configured translation provider, and write back
|
|
6
|
+
* through the Fimo API. Skips fields marked `translate: false` in the schema.
|
|
7
7
|
*
|
|
8
8
|
* Provider is configured via env (FIMO_AGENT_TRANSLATE_PROVIDER) — the
|
|
9
9
|
* runtime threads project secrets through when the v1.x secrets API
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
interface Entry {
|
|
14
14
|
id: string;
|
|
15
|
+
documentId: string;
|
|
16
|
+
locale: string;
|
|
15
17
|
type: string;
|
|
16
18
|
data: Record<string, unknown>;
|
|
17
19
|
}
|
|
@@ -24,8 +26,8 @@ interface TranslateOptions {
|
|
|
24
26
|
|
|
25
27
|
async function listMissing(_opts: TranslateOptions): Promise<Entry[]> {
|
|
26
28
|
// Stub — wired to the Fimo API by the runtime. Returns entries with
|
|
27
|
-
// at least one
|
|
28
|
-
//
|
|
29
|
+
// at least one missing target-locale document row while the default-locale
|
|
30
|
+
// row is populated.
|
|
29
31
|
return [];
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -35,7 +37,8 @@ async function translate(text: string, _to: string): Promise<string> {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
async function writeBack(_entry: Entry, _locale: string, _value: string): Promise<void> {
|
|
38
|
-
// Stub —
|
|
40
|
+
// Stub — create/update the target locale entry via
|
|
41
|
+
// `/api/management/projects/:id/envs/:env/tenant/entries/:type`.
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
async function main(): Promise<void> {
|
|
@@ -50,7 +53,7 @@ async function main(): Promise<void> {
|
|
|
50
53
|
const entries = await listMissing({ fromLocale, toLocales, api });
|
|
51
54
|
for (const entry of entries) {
|
|
52
55
|
for (const locale of toLocales) {
|
|
53
|
-
const sourceValue =
|
|
56
|
+
const sourceValue = typeof entry.data.title === 'string' ? entry.data.title : undefined;
|
|
54
57
|
if (!sourceValue) {
|
|
55
58
|
continue;
|
|
56
59
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// AUTO-GENERATED ⚠️ – do not edit by hand
|
|
11
11
|
import { useSuspenseQuery, useQuery, useMutation, useQueryClient, QueryKey } from "@tanstack/react-query";
|
|
12
12
|
import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type Fields, type Projected, type Query, type Sort, type Where } from "fimo/ui";
|
|
13
|
+
// @ts-ignore - virtual module provided by Fimo's Vite plugin
|
|
14
|
+
import fimoConfig from "virtual:fimo-config";
|
|
13
15
|
|
|
14
16
|
/*───────────────────────────────────────────────────────────────*/
|
|
15
17
|
/* ──────────── 📄 Type definitions ──────────── */
|
|
@@ -20,8 +22,13 @@ import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type Fields
|
|
|
20
22
|
*/
|
|
21
23
|
export interface <%= model.pascal %> {
|
|
22
24
|
id: string;
|
|
25
|
+
documentId: string;
|
|
23
26
|
slug: FimoString;
|
|
24
27
|
contentType: string;
|
|
28
|
+
locale: string;
|
|
29
|
+
sourceLocale: string | null;
|
|
30
|
+
translationStatus: string;
|
|
31
|
+
translationMeta: Record<string, unknown> | null;
|
|
25
32
|
createdAt: string;
|
|
26
33
|
updatedAt: string;
|
|
27
34
|
<%
|
|
@@ -85,8 +92,13 @@ function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
|
|
|
85
92
|
|
|
86
93
|
return {
|
|
87
94
|
id,
|
|
95
|
+
documentId: entry.documentId,
|
|
88
96
|
slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
|
|
89
97
|
contentType: entry.contentType,
|
|
98
|
+
locale: entry.locale,
|
|
99
|
+
sourceLocale: entry.sourceLocale,
|
|
100
|
+
translationStatus: entry.translationStatus,
|
|
101
|
+
translationMeta: entry.translationMeta,
|
|
90
102
|
createdAt: entry.createdAt,
|
|
91
103
|
updatedAt: entry.updatedAt,
|
|
92
104
|
<%
|
|
@@ -137,6 +149,10 @@ if (model.fields) {
|
|
|
137
149
|
// @ts-ignore - meta.env is a vite feature
|
|
138
150
|
const base = import.meta.env.VITE_API_URL;
|
|
139
151
|
|
|
152
|
+
function defaultLocale(): string {
|
|
153
|
+
return (fimoConfig as { i18n?: { defaultLocale?: string } }).i18n?.defaultLocale ?? "en";
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
141
157
|
const h: Record<string, string> = { ...extra };
|
|
142
158
|
// @ts-ignore
|
|
@@ -145,23 +161,53 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
|
145
161
|
return h;
|
|
146
162
|
}
|
|
147
163
|
|
|
148
|
-
|
|
149
|
-
const
|
|
164
|
+
function withLocale(url: string, locale?: string): string {
|
|
165
|
+
const u = new URL(url, "http://fimo.local");
|
|
166
|
+
u.searchParams.set("locale", locale ?? defaultLocale());
|
|
167
|
+
return `${u.pathname}${u.search}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string, unknown> {
|
|
171
|
+
const {
|
|
172
|
+
id,
|
|
173
|
+
documentId,
|
|
174
|
+
contentType,
|
|
175
|
+
locale,
|
|
176
|
+
sourceLocale,
|
|
177
|
+
translationStatus,
|
|
178
|
+
translationMeta,
|
|
179
|
+
createdAt,
|
|
180
|
+
updatedAt,
|
|
181
|
+
...data
|
|
182
|
+
} = payload as Record<string, unknown>;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
data,
|
|
186
|
+
...(typeof locale === "string" && locale.length > 0 ? { locale } : {}),
|
|
187
|
+
...(typeof documentId === "string" && documentId.length > 0 ? { documentId } : {}),
|
|
188
|
+
...(typeof sourceLocale === "string" && sourceLocale.length > 0 ? { sourceLocale } : {}),
|
|
189
|
+
...(typeof translationStatus === "string" && translationStatus.length > 0 ? { translationStatus } : {}),
|
|
190
|
+
...(translationMeta && typeof translationMeta === "object" ? { translationMeta } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function getById(id: string, options: { locale?: string } = {}): Promise<<%= model.pascal %>> {
|
|
195
|
+
const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/${id}`, options.locale)}`, { headers: fimoHeaders() });
|
|
150
196
|
if (!res.ok) throw new Error(await res.text());
|
|
151
197
|
const data = await res.json();
|
|
152
198
|
return wrapWithSource(data.data);
|
|
153
199
|
}
|
|
154
200
|
|
|
155
|
-
export async function getBySlug(slug: string): Promise<<%= model.pascal %> | null> {
|
|
156
|
-
const res = await fetch(`${base}
|
|
201
|
+
export async function getBySlug(slug: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
|
|
202
|
+
const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options.locale)}`, { headers: fimoHeaders() });
|
|
157
203
|
if (res.status === 404) return null;
|
|
158
204
|
if (!res.ok) throw new Error(await res.text());
|
|
159
205
|
const data = await res.json();
|
|
160
206
|
return wrapWithSource(data.data);
|
|
161
207
|
}
|
|
162
208
|
|
|
163
|
-
export async function getByField(fieldName: string, value: string): Promise<<%= model.pascal %> | null> {
|
|
164
|
-
const res = await fetch(`${base}
|
|
209
|
+
export async function getByField(fieldName: string, value: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
|
|
210
|
+
const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options.locale)}`, { headers: fimoHeaders() });
|
|
165
211
|
if (res.status === 404) return null;
|
|
166
212
|
if (!res.ok) throw new Error(await res.text());
|
|
167
213
|
const data = await res.json();
|
|
@@ -189,6 +235,10 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
|
|
|
189
235
|
query.set("where", JSON.stringify(where));
|
|
190
236
|
}
|
|
191
237
|
|
|
238
|
+
if (!query.has("locale")) {
|
|
239
|
+
query.set("locale", defaultLocale());
|
|
240
|
+
}
|
|
241
|
+
|
|
192
242
|
return query.toString();
|
|
193
243
|
}
|
|
194
244
|
|
|
@@ -204,7 +254,7 @@ export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%=
|
|
|
204
254
|
const res = await fetch(`${base}/entries/<%= model.uid %>`, {
|
|
205
255
|
method: "POST",
|
|
206
256
|
headers: fimoHeaders({ "Content-Type": "application/json" }),
|
|
207
|
-
body: JSON.stringify(
|
|
257
|
+
body: JSON.stringify(splitWritePayload(payload))
|
|
208
258
|
});
|
|
209
259
|
if (!res.ok) throw new Error(await res.text());
|
|
210
260
|
const data = await res.json();
|
|
@@ -215,7 +265,7 @@ export async function update(id: string, payload: Partial<<%= model.pascal %>>):
|
|
|
215
265
|
const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, {
|
|
216
266
|
method: "PUT",
|
|
217
267
|
headers: fimoHeaders({ "Content-Type": "application/json" }),
|
|
218
|
-
body: JSON.stringify(
|
|
268
|
+
body: JSON.stringify(splitWritePayload(payload))
|
|
219
269
|
});
|
|
220
270
|
if (!res.ok) throw new Error(await res.text());
|
|
221
271
|
const data = await res.json();
|
|
@@ -233,38 +283,38 @@ export async function remove(id: string): Promise<void> {
|
|
|
233
283
|
|
|
234
284
|
const qk = {
|
|
235
285
|
root : ["<%= model.pascal %>"] as const,
|
|
236
|
-
byId : (id: string) => ["<%= model.pascal %>", id] as const,
|
|
237
|
-
bySlug : (slug: string) => ["<%= model.pascal %>", "slug", slug] as const,
|
|
238
|
-
byField : (field: string, value: string) => ["<%= model.pascal %>", field, value] as const,
|
|
286
|
+
byId : (id: string, locale?: string) => ["<%= model.pascal %>", id, locale ?? defaultLocale()] as const,
|
|
287
|
+
bySlug : (slug: string, locale?: string) => ["<%= model.pascal %>", "slug", slug, locale ?? defaultLocale()] as const,
|
|
288
|
+
byField : (field: string, value: string, locale?: string) => ["<%= model.pascal %>", field, value, locale ?? defaultLocale()] as const,
|
|
239
289
|
list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
|
|
240
290
|
};
|
|
241
291
|
|
|
242
|
-
export function useGetById(id: string) {
|
|
243
|
-
return useQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id), queryFn: () => getById(id) });
|
|
292
|
+
export function useGetById(id: string, options: { locale?: string } = {}) {
|
|
293
|
+
return useQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, options.locale), queryFn: () => getById(id, options) });
|
|
244
294
|
}
|
|
245
295
|
|
|
246
|
-
export function useGetBySlug(slug: string) {
|
|
247
|
-
return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug), queryFn: () => getBySlug(slug), enabled: !!slug });
|
|
296
|
+
export function useGetBySlug(slug: string, options: { locale?: string } = {}) {
|
|
297
|
+
return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, options.locale), queryFn: () => getBySlug(slug, options), enabled: !!slug });
|
|
248
298
|
}
|
|
249
299
|
|
|
250
|
-
export function useGetByField(fieldName: string, value: string) {
|
|
251
|
-
return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value), queryFn: () => getByField(fieldName, value), enabled: !!value });
|
|
300
|
+
export function useGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
|
|
301
|
+
return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, options.locale), queryFn: () => getByField(fieldName, value, options), enabled: !!value });
|
|
252
302
|
}
|
|
253
303
|
|
|
254
304
|
export function useGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
|
|
255
305
|
return useQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(params), queryFn: () => get(params) });
|
|
256
306
|
}
|
|
257
307
|
|
|
258
|
-
export function useSuspenseGetById(id: string) {
|
|
259
|
-
return useSuspenseQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id), queryFn: () => getById(id) });
|
|
308
|
+
export function useSuspenseGetById(id: string, options: { locale?: string } = {}) {
|
|
309
|
+
return useSuspenseQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, options.locale), queryFn: () => getById(id, options) });
|
|
260
310
|
}
|
|
261
311
|
|
|
262
|
-
export function useSuspenseGetBySlug(slug: string) {
|
|
263
|
-
return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug), queryFn: () => getBySlug(slug) });
|
|
312
|
+
export function useSuspenseGetBySlug(slug: string, options: { locale?: string } = {}) {
|
|
313
|
+
return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, options.locale), queryFn: () => getBySlug(slug, options) });
|
|
264
314
|
}
|
|
265
315
|
|
|
266
|
-
export function useSuspenseGetByField(fieldName: string, value: string) {
|
|
267
|
-
return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value), queryFn: () => getByField(fieldName, value) });
|
|
316
|
+
export function useSuspenseGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
|
|
317
|
+
return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, options.locale), queryFn: () => getByField(fieldName, value, options) });
|
|
268
318
|
}
|
|
269
319
|
|
|
270
320
|
export function useSuspenseGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
|
|
@@ -14,14 +14,14 @@ You are writing code in a Fimo project — a content + media + forms + analytics
|
|
|
14
14
|
|
|
15
15
|
This skill is version-locked to this project's installed `fimo` package — the shapes here match what your code can actually use.
|
|
16
16
|
|
|
17
|
-
For CLI commands and workflows (creating a project, pushing schemas/forms, deploying,
|
|
17
|
+
For CLI commands and workflows (creating a project, pushing schemas/forms, deploying, managing labels, etc.), the **`fimo-cli`** skill is the source of truth. If `fimo-cli` isn't loaded for your AI tool, run `fimo skills install` to register it.
|
|
18
18
|
|
|
19
19
|
## Critical: NEVER hardcode user-facing things
|
|
20
20
|
|
|
21
21
|
This is the load-bearing rule for every code change. **Fimo's whole point is that content editors edit content in the dashboard, not in code.** Anything hardcoded breaks that.
|
|
22
22
|
|
|
23
23
|
- **Content** (posts, products, pages, lists, anything structured) → declare a schema, render with the generated `useGet()` hook + `fimo/ui` components. **Never** write `const posts = [...]` in JSX.
|
|
24
|
-
- **Strings** (labels, CTAs, headings, descriptions, any visible text) → wrap in `t('key'
|
|
24
|
+
- **Strings** (labels, CTAs, headings, descriptions, any visible text) → wrap in `t('key')` and create the default-locale value with `fimo labels set`. Use `--locale <locale>` only when intentionally writing a non-default locale. **Never** write `<h1>Welcome</h1>` directly — write `<Text value={t('home.title')} as="h1" />`.
|
|
25
25
|
- **Media** (images, videos, files) → use `<Image>` / `<Video>` from `fimo/ui` for schema media, `<StaticImage>` for hard-coded URLs. **Never** use `<img src={...}>`.
|
|
26
26
|
- **Forms** → use the generated Zod schema + `submitX` helper from `@/forms/<name>`. Never hand-roll fetch or validation.
|
|
27
27
|
|
|
@@ -37,7 +37,7 @@ These define the package surface for the load-bearing primitives. **Read all fiv
|
|
|
37
37
|
- `references/translations.md` — `t()` helper, `useTranslations()`, wrap rules
|
|
38
38
|
- `references/ui.md` — `fimo/ui` components + JSX code conventions + app code rules
|
|
39
39
|
|
|
40
|
-
Workflow / CLI-side concerns (pushing schemas, generating images,
|
|
40
|
+
Workflow / CLI-side concerns (pushing schemas, generating images, managing labels, deploying) live in the **`fimo-cli`** skill's matching references.
|
|
41
41
|
|
|
42
42
|
## Decision Tree (specific tasks)
|
|
43
43
|
|
|
@@ -99,7 +99,7 @@ export function ContactForm() {
|
|
|
99
99
|
|
|
100
100
|
## Labels and copy
|
|
101
101
|
|
|
102
|
-
Wrap every visible string in `t('key'
|
|
102
|
+
Wrap every visible string in `t('key')` and create the DB value with `fimo labels set` — see `references/translations.md`. This includes form labels, placeholders, button text, success/error messages.
|
|
103
103
|
|
|
104
104
|
## Forms vs. schemas
|
|
105
105
|
|
|
@@ -45,7 +45,7 @@ pnpm add -D vite @vitejs/plugin-react @types/react @types/react-dom typescript
|
|
|
45
45
|
|
|
46
46
|
Validate runs automatically during `fimo deploy` and API-side sandbox builds.
|
|
47
47
|
For a purely local build, run `fimo validate` manually first when you need to
|
|
48
|
-
|
|
48
|
+
regenerate schemas/forms or check missing labels before `pnpm build`.
|
|
49
49
|
|
|
50
50
|
## File: `vite.config.ts`
|
|
51
51
|
|
|
@@ -36,7 +36,7 @@ pnpm add -D @react-router/dev @react-router/node @tailwindcss/vite tailwindcss v
|
|
|
36
36
|
|
|
37
37
|
Validate runs automatically during `fimo deploy` and API-side sandbox builds.
|
|
38
38
|
For a purely local build, run `fimo validate` manually first when you need to
|
|
39
|
-
|
|
39
|
+
regenerate schemas/forms or check missing labels before `pnpm build`.
|
|
40
40
|
|
|
41
41
|
## File: `vite.config.ts`
|
|
42
42
|
|
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Labels — package surface
|
|
2
2
|
|
|
3
|
-
> For the CLI workflow (`fimo validate`,
|
|
3
|
+
> For the CLI workflow (`fimo validate`, `fimo labels set`, bulk updates), see **`fimo-cli/references/translations.md`**.
|
|
4
4
|
|
|
5
5
|
This file covers the **package surface**: the `t()` helper, the `useTranslations()` hook, and the code-side rules for wrapping strings.
|
|
6
6
|
|
|
7
7
|
## The rule
|
|
8
8
|
|
|
9
|
-
Every piece of **user-facing static text** in the app (page titles, button labels, nav items, form placeholders, empty-state copy, alt text, meta tags) MUST be wrapped in `t('key'
|
|
9
|
+
Every piece of **user-facing static text** in the app (page titles, button labels, nav items, form placeholders, empty-state copy, alt text, meta tags) MUST be wrapped in `t('key')`.
|
|
10
10
|
|
|
11
11
|
Raw string literals in JSX are a bug — they won't show up in the Fimo admin as editable content and won't be translatable. **Hard-coded JSX text (`<h1>Welcome</h1>`) skips the entire i18n + inline-editing pipeline and becomes invisible to the editor.**
|
|
12
12
|
|
|
13
13
|
## Setup (already wired)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Labels are wired automatically by the `fimo/vite` build plugin. It exposes a `virtual:translations` module sourced from the tenant DB, and `useTranslations()` reads from that virtual module — there's no provider to add or remove. The app entry is `src/root.tsx`, which wraps the app in `<FimoProviders>` from `fimo/react-router`. Pages and components only call `useTranslations()`.
|
|
16
|
+
|
|
17
|
+
In development, the generated app reloads when DB label values change. In production, the build embeds the DB-backed label bundle.
|
|
18
|
+
|
|
19
|
+
## Locale behavior
|
|
20
|
+
|
|
21
|
+
The default label locale is `i18n.defaultLocale` in `fimo-config.json`, falling back to `en` when it is not set. `fimo validate`, the generated app runtime, and `fimo labels set` without `--locale` all use that default locale.
|
|
22
|
+
|
|
23
|
+
For non-default locales, create explicit DB values with `fimo labels set --locale <locale>` or `fimo labels set-many --locale <locale>`. Keep the code shape the same for every locale: `t('hero.title')` declares the key, and the DB decides which locale value is shown.
|
|
24
|
+
|
|
25
|
+
`fimo-config.json` owns the locale list:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"i18n": {
|
|
30
|
+
"defaultLocale": "en",
|
|
31
|
+
"locales": ["en", "es"],
|
|
32
|
+
"autoTranslateLabels": true,
|
|
33
|
+
"autoTranslateContent": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `locales` is the enabled locale list and must include `defaultLocale`.
|
|
39
|
+
- `autoTranslateLabels` defaults to enabled when omitted. Set it to `false` to stop default-locale label edits from creating machine translations for other locales.
|
|
40
|
+
- `autoTranslateContent` does the same for schema entries.
|
|
41
|
+
- `seo.locale` is SEO/html metadata. It does not select the DB locale by itself.
|
|
42
|
+
- Public locale routing is project/framework code. Do not assume `/es/...` exists unless the project has implemented that strategy.
|
|
16
43
|
|
|
17
44
|
## Usage
|
|
18
45
|
|
|
@@ -24,20 +51,21 @@ export function Hero() {
|
|
|
24
51
|
return (
|
|
25
52
|
<section>
|
|
26
53
|
<h1>
|
|
27
|
-
<Text value={t('hero.title'
|
|
54
|
+
<Text value={t('hero.title')} />
|
|
28
55
|
</h1>
|
|
29
56
|
<p>
|
|
30
|
-
<Text value={t('hero.subtitle'
|
|
57
|
+
<Text value={t('hero.subtitle')} />
|
|
31
58
|
</p>
|
|
32
|
-
<input placeholder={String(t('hero.emailPlaceholder'
|
|
33
|
-
<img alt={String(t('hero.imageAlt'
|
|
59
|
+
<input placeholder={String(t('hero.emailPlaceholder'))} />
|
|
60
|
+
<img alt={String(t('hero.imageAlt'))} src="..." />
|
|
34
61
|
</section>
|
|
35
62
|
);
|
|
36
63
|
}
|
|
37
64
|
```
|
|
38
65
|
|
|
39
|
-
- `t(key
|
|
66
|
+
- `t(key)` returns a `FimoString` (wrapped for source tracking). Render it via `<Text value={...} />` inside JSX.
|
|
40
67
|
- For HTML attributes (`placeholder`, `alt`, `title`, `aria-label`) cast with `String(t(...))` — the attribute can't accept a `FimoString` object directly.
|
|
68
|
+
- If a key is missing from the DB, it renders empty. Run `fimo validate` and add missing values with `fimo labels set`.
|
|
41
69
|
|
|
42
70
|
## Hard rules (enforced by the pre-build linter)
|
|
43
71
|
|
|
@@ -45,13 +73,13 @@ export function Hero() {
|
|
|
45
73
|
|
|
46
74
|
```tsx
|
|
47
75
|
// ❌ BAD — variable key
|
|
48
|
-
t(item.key
|
|
76
|
+
t(item.key);
|
|
49
77
|
|
|
50
78
|
// ❌ BAD — template with expression
|
|
51
|
-
t(`nav.${page}
|
|
79
|
+
t(`nav.${page}`);
|
|
52
80
|
|
|
53
81
|
// ✅ GOOD — put the t() call where the literal lives
|
|
54
|
-
const items = [{ label: t('nav.home'
|
|
82
|
+
const items = [{ label: t('nav.home') }, { label: t('nav.about') }];
|
|
55
83
|
items.map((item) => <li>{item.label}</li>);
|
|
56
84
|
```
|
|
57
85
|
|
|
@@ -59,7 +87,7 @@ items.map((item) => <li>{item.label}</li>);
|
|
|
59
87
|
|
|
60
88
|
- Dot-namespaced, lowercase, kebab or camelCase leaves: `hero.title`, `nav.signIn`, `footer.copyright`, `errors.required`.
|
|
61
89
|
- Namespace by **page/section/component**, not by feature (`contact.form.submitLabel`, not `forms.contactSubmit`).
|
|
62
|
-
-
|
|
90
|
+
- Values live in the DB. Do not put user-visible fallback copy in code.
|
|
63
91
|
|
|
64
92
|
## What NOT to translate
|
|
65
93
|
|
|
@@ -72,4 +100,4 @@ items.map((item) => <li>{item.label}</li>);
|
|
|
72
100
|
|
|
73
101
|
If a human reader sees the text AND the project owner might want to reword it in the admin, it goes through `t()`. When in doubt: wrap it.
|
|
74
102
|
|
|
75
|
-
After adding or changing `t()` calls, run `fimo validate
|
|
103
|
+
After adding or changing `t()` calls, run `fimo validate`, then fill missing values with `fimo labels set` or `fimo labels set-many`. See `fimo-cli/references/translations.md`.
|
|
@@ -17,15 +17,15 @@ Use these to render content-type field values consistently. They carry source me
|
|
|
17
17
|
| `<Video>` | Schema `media` video fields | `<Video value={post.heroVideo} />` |
|
|
18
18
|
| `<Date>` | Schema date fields | `<Date value={post.publishDate} />` |
|
|
19
19
|
| `<Boolean>` | Schema boolean fields | `<Boolean value={product.inStock} />` |
|
|
20
|
-
| `` fimo`...` `` | Compose tracked values (mix `t()` + schema text) | ``<Text value={fimo`${t('by'
|
|
21
|
-
| `useTranslations()` | `t('key'
|
|
20
|
+
| `` fimo`...` `` | Compose tracked values (mix `t()` + schema text) | ``<Text value={fimo`${t('by')} ${post.author}`} />`` |
|
|
21
|
+
| `useTranslations()` | `t('key')` hook | see `references/translations.md` |
|
|
22
22
|
|
|
23
23
|
### Rules of thumb
|
|
24
24
|
|
|
25
25
|
1. **Schema text → `<Text>`**, never `<h1>{post.title}</h1>`.
|
|
26
26
|
2. **Schema media → `<Image>` / `<Video>`**, never `<img src={post.coverImage.url} />`. Provide `width` / `height` when known so Fimo can optimize and reserve layout space.
|
|
27
27
|
3. **Non-schema images** (decorative, hard-coded URLs, generated assets referenced directly) → `<StaticImage>`. Never hand-construct a `FimoMedia` to feed `<Image>`.
|
|
28
|
-
4. **
|
|
28
|
+
4. **Label text → `<Text value={t('key')} />`**, not `<h1>{t(...)}</h1>`. Label values live in the DB for `i18n.defaultLocale` from `fimo-config.json` unless a non-default locale is explicitly targeted. Public locale routes such as `/es/...` only exist if the project has implemented that routing strategy. See `references/translations.md`.
|
|
29
29
|
5. **Mixed schema + translated text** → the `` fimo`...` `` template literal inside `<Text>`.
|
|
30
30
|
6. **Never hand-roll richtext rendering** — always `<RichText>`. To restyle a node, pass `components={{ heading: ..., link: ..., ... }}`.
|
|
31
31
|
|
|
@@ -77,7 +77,7 @@ The template uses **Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + rea
|
|
|
77
77
|
|
|
78
78
|
- Raw `<a href>` for internal nav → use `<Link>` / `<NavLink>`.
|
|
79
79
|
- Raw `<img>` for schema media → use `<Image>` (with width/height).
|
|
80
|
-
- Hardcoded strings in JSX → wrap in `t('key'
|
|
80
|
+
- Hardcoded strings in JSX → wrap in `t('key')` and add the DB value with `fimo labels set`.
|
|
81
81
|
- Calling `t()` with a dynamic first argument → key must be a string literal.
|
|
82
82
|
- Editing generated `.ts` files under `src/schemas/` or `src/forms/` → regenerated on every build.
|
|
83
83
|
- Writing raw HTML into a `richtext` field → must be Tiptap JSONContent.
|
|
@@ -47,12 +47,12 @@ fimo create my-site --org <id> # scaffold (writes .fimo.settings.jso
|
|
|
47
47
|
fimo clone # interactively choose an existing project to clone locally
|
|
48
48
|
cd my-site # the `fimo` project skill auto-loads from here
|
|
49
49
|
fimo credits # inspect the owning org's AI credit balance (read-only)
|
|
50
|
-
fimo validate # run all checks +
|
|
50
|
+
fimo validate # run all checks + codegen; fails on missing DB labels
|
|
51
51
|
fimo deploy # push to preview (runs validate internally)
|
|
52
52
|
fimo deploy --publish # publish live (only when user says "ship it")
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
`fimo validate` is the single entrypoint for "
|
|
55
|
+
`fimo validate` is the single entrypoint for "prove this project is ready" — it covers schema/form codegen, route metadata checks, lint, and missing label checks for `i18n.defaultLocale` from `fimo-config.json` (fallback: `en`). Use `fimo labels set` / `set-many` to create or update label values in the DB. The underlying phases are also available individually under the hidden `fimo scripts` namespace for advanced/debug use.
|
|
56
56
|
|
|
57
57
|
## Decision Tree
|
|
58
58
|
|
|
@@ -64,7 +64,7 @@ Load the matching reference when the user's request fires its trigger. Recipes t
|
|
|
64
64
|
- "View form submissions / who filled out the contact form / inspect what users sent / how many signups" → `references/forms.md` § Reading submissions
|
|
65
65
|
- "Upload an image / hero / cover / existing media file" → `references/assets.md` (workflow: upload, browse, open, delete) **+ load `fimo/references/assets.md` for the `FimoMedia` shape and rendering**
|
|
66
66
|
- "Generate AI image / video / draft preview / image edit / cover from a prompt" → `references/media.md` (the `fimo generate <model>` surface — recipes, defaults, the one-verb shape) **+ load `fimo/references/assets.md` for the `FimoMedia` shape if you'll plug the result into UI**
|
|
67
|
-
- "
|
|
67
|
+
- "Add labels / update static copy / fix missing `t()` values" → `references/translations.md` (workflow) **+ load `fimo/references/translations.md` for the `t()` helper and wrap rules**
|
|
68
68
|
- "What's in this project / show the project structure / describe the project / what schemas does it have / how many routes / project tree / show me what's here" → `references/describe.md`
|
|
69
69
|
- "Deploy / publish / go live / push my changes" → `references/deploy.md`
|
|
70
70
|
- "Get the preview URL / open the preview / what's the URL for this env / refresh the preview link / I just want to see the site / the preview URL stopped working" → `references/preview.md`
|
|
@@ -71,7 +71,7 @@ Use the canonical read query flags: `--search`, `--sort`, `--where`, `--limit`,
|
|
|
71
71
|
The `fimo/vite` plugin auto-regenerates `src/schemas/*.ts` on `vite dev` / `vite build`. If you need to regen outside vite (e.g. CI, scripting), use:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
fimo validate # runs all phases: codegen, lint, route validation,
|
|
74
|
+
fimo validate # runs all phases: codegen, lint, route validation, label checks
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Advanced/internal (hidden from `fimo --help`) — for the bare regen phase:
|
|
@@ -57,7 +57,7 @@ Use this when the user asks "who filled out the contact form?", "how many signup
|
|
|
57
57
|
The `fimo/vite` plugin auto-regenerates `src/forms/*.ts`. To regen outside vite, use:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
fimo validate # runs all phases: codegen, lint, route validation,
|
|
60
|
+
fimo validate # runs all phases: codegen, lint, route validation, label checks
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Advanced/internal (hidden from `fimo --help`) — for the bare regen phase:
|