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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/assets/agent-templates/content-translator/GOAL.md +8 -11
  3. package/assets/agent-templates/content-translator/capabilities.yaml +1 -3
  4. package/assets/agent-templates/content-translator/scripts/translate-entries.ts +10 -7
  5. package/assets/content-templates/hooks-template.eta +73 -23
  6. package/assets/skills/fimo/SKILL.md +3 -3
  7. package/assets/skills/fimo/references/forms.md +1 -1
  8. package/assets/skills/fimo/references/setup-plain-vite.md +1 -1
  9. package/assets/skills/fimo/references/setup-react-router.md +1 -1
  10. package/assets/skills/fimo/references/translations.md +42 -14
  11. package/assets/skills/fimo/references/ui.md +4 -4
  12. package/assets/skills/fimo-cli/SKILL.md +3 -3
  13. package/assets/skills/fimo-cli/references/content.md +1 -1
  14. package/assets/skills/fimo-cli/references/forms.md +1 -1
  15. package/assets/skills/fimo-cli/references/translations.md +45 -19
  16. package/dist/build/vite/plugins/fimo-config.d.ts.map +1 -1
  17. package/dist/build/vite/plugins/fimo-config.js +16 -0
  18. package/dist/build/vite/plugins/fimo-config.test.d.ts +2 -0
  19. package/dist/build/vite/plugins/fimo-config.test.d.ts.map +1 -0
  20. package/dist/build/vite/plugins/fimo-config.test.js +46 -0
  21. package/dist/build/vite/plugins/translations.d.ts +7 -6
  22. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  23. package/dist/build/vite/plugins/translations.js +366 -33
  24. package/dist/build/vite/plugins/translations.test.d.ts +2 -0
  25. package/dist/build/vite/plugins/translations.test.d.ts.map +1 -0
  26. package/dist/build/vite/plugins/translations.test.js +177 -0
  27. package/dist/cli/bundle.json +2 -2
  28. package/dist/cli/index.js +1305 -1078
  29. package/dist/runtime/app/FimoScripts.d.ts.map +1 -1
  30. package/dist/runtime/app/FimoScripts.js +35 -1
  31. package/dist/runtime/app/prefetch.d.ts.map +1 -1
  32. package/dist/runtime/app/prefetch.js +6 -1
  33. package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
  34. package/dist/runtime/paths/get-fimo-paths.js +9 -4
  35. package/dist/runtime/primitives/components/Text.d.ts +1 -1
  36. package/dist/runtime/primitives/components/Text.js +1 -1
  37. package/dist/runtime/primitives/lib/query.d.ts +5 -0
  38. package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
  39. package/dist/runtime/primitives/lib/template.d.ts +1 -1
  40. package/dist/runtime/primitives/lib/template.js +1 -1
  41. package/dist/runtime/primitives/translations.d.ts +9 -3
  42. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  43. package/dist/runtime/primitives/translations.js +32 -5
  44. package/dist/runtime/seo/htmlProps.d.ts +1 -1
  45. package/dist/runtime/seo/htmlProps.js +2 -2
  46. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  47. package/dist/runtime/shared/fimo-config.server.js +1 -0
  48. package/dist/runtime/shared/fimo-config.types.d.ts +7 -0
  49. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  50. package/dist/scripts/extract-translations.d.ts +8 -8
  51. package/dist/scripts/extract-translations.js +22 -57
  52. package/dist/scripts/lint-translation-keys.js +24 -5
  53. package/dist/scripts/lint-translation-keys.test.d.ts +1 -0
  54. package/dist/scripts/lint-translation-keys.test.js +16 -0
  55. package/package.json +1 -1
  56. package/release.json +2 -2
  57. package/templates/react-router/fimo-config.json +4 -0
  58. package/templates/react-router/package.json +1 -1
  59. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +0 -66
  60. package/dist/scripts/inject-translations.d.ts +0 -6
  61. package/dist/scripts/inject-translations.js +0 -168
  62. 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 translations from the CLI:
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 translations list # manage locales
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 locale into every
4
- non-default locale declared in `fimo.config.json`. Writes go through
5
- the `agent` branch and never directly to `main` the M19 lifecycle
6
- engine picks the branch up and either auto-merges or proposes it for
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.config.json`).
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. Commit the change on the agent's branch.
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, writes translations + i18n + files. Net for the translation provider.
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 translatable fields missing translations,
5
- * call the configured translation provider, and write back through the
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 non-default-locale field that's empty while the
28
- // default-locale field is populated.
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 — PATCH the entry via `/api/management/projects/:id/envs/:env/cms/:type/:id`.
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 = (entry.data[fromLocale] as Record<string, string> | undefined)?.title;
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
- export async function getById(id: string): Promise<<%= model.pascal %>> {
149
- const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, { headers: fimoHeaders() });
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}/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, { headers: fimoHeaders() });
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}/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, { headers: fimoHeaders() });
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({ data: payload })
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({ data: payload })
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, syncing translations, 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.
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', 'Default text')`. **Never** write `<h1>Welcome</h1>` directly — write `<Text value={t('home.title', 'Welcome')} as="h1" />`.
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, syncing translations, deploying) live in the **`fimo-cli`** skill's matching references.
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', 'Default')` — see `references/translations.md`. This includes form labels, placeholders, button text, success/error messages.
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
- sync schemas/forms/translations before `pnpm build`.
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
- sync schemas/forms/translations before `pnpm build`.
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
- # Translations — package surface
1
+ # Labels — package surface
2
2
 
3
- > For the CLI workflow (`fimo validate`, when to run validation, what sync does), see **`fimo-cli/references/translations.md`**.
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', 'Default text')`.
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
- Translations are wired automatically by the `fimo/vite` build plugin. It exposes a `virtual:translations` module sourced from `<projectRoot>/translations/en.json`, 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`; you never import `translations/en.json` in app code. Pages and components only call `useTranslations()`.
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', 'Welcome to Acme')} />
54
+ <Text value={t('hero.title')} />
28
55
  </h1>
29
56
  <p>
30
- <Text value={t('hero.subtitle', 'The fastest way to ship your idea.')} />
57
+ <Text value={t('hero.subtitle')} />
31
58
  </p>
32
- <input placeholder={String(t('hero.emailPlaceholder', 'you@example.com'))} />
33
- <img alt={String(t('hero.imageAlt', 'Team collaborating'))} src="..." />
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, default)` returns a `FimoString` (wrapped for source tracking). Render it via `<Text value={...} />` inside JSX.
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, item.label);
76
+ t(item.key);
49
77
 
50
78
  // ❌ BAD — template with expression
51
- t(`nav.${page}`, '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', 'Home') }, { label: t('nav.about', 'About') }];
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
- - The default (second arg) is the English copy — make it final, not a TODO; it ships to production if no translation exists for the active locale.
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` (or rely on `fimo deploy` doing it). See `fimo-cli/references/translations.md`.
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','By')} ${post.author}`} />`` |
21
- | `useTranslations()` | `t('key', 'Default')` hook | see `references/translations.md` |
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. **Translated text → `<Text value={t('key', 'Default')} />`**, not `<h1>{t(...)}</h1>`. See `references/translations.md`.
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', 'Default')`.
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 + sync schemas/forms/translations to DB
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 "make local state match the backend" — it covers schema/form codegen, route metadata checks, lint, and translations sync. The underlying phases are also available individually under the hidden `fimo scripts` namespace for advanced/debug use.
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
- - "Sync translations / extract t() strings to the DB" → `references/translations.md` (workflow) **+ load `fimo/references/translations.md` for the `t()` helper and wrap rules**
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, translations sync
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, translations sync
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: