fimo 0.2.4 → 0.2.5-experimental.1782379858530

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 (129) 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 +89 -28
  6. package/assets/skills/fimo/SKILL.md +10 -8
  7. package/assets/skills/fimo/assets/react-router-locales/navigation.tsx +13 -0
  8. package/assets/skills/fimo/assets/react-router-locales/routes.ts +11 -0
  9. package/assets/skills/fimo/references/content.md +36 -2
  10. package/assets/skills/fimo/references/forms.md +1 -1
  11. package/assets/skills/fimo/references/labels.md +106 -0
  12. package/assets/skills/fimo/references/locales.md +138 -0
  13. package/assets/skills/fimo/references/setup-plain-vite.md +1 -1
  14. package/assets/skills/fimo/references/setup-react-router.md +9 -3
  15. package/assets/skills/fimo/references/ui.md +16 -15
  16. package/assets/skills/fimo-cli/SKILL.md +6 -5
  17. package/assets/skills/fimo-cli/references/content.md +30 -3
  18. package/assets/skills/fimo-cli/references/create.md +1 -1
  19. package/assets/skills/fimo-cli/references/forms.md +2 -2
  20. package/assets/skills/fimo-cli/references/labels.md +71 -0
  21. package/assets/skills/fimo-cli/references/locales.md +104 -0
  22. package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +14 -11
  23. package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +19 -8
  24. package/dist/build/vite/plugins/fimo-config.d.ts.map +1 -1
  25. package/dist/build/vite/plugins/fimo-config.js +16 -0
  26. package/dist/build/vite/plugins/fimo-config.test.d.ts +2 -0
  27. package/dist/build/vite/plugins/fimo-config.test.d.ts.map +1 -0
  28. package/dist/build/vite/plugins/fimo-config.test.js +46 -0
  29. package/dist/build/vite/plugins/translations.d.ts +8 -6
  30. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  31. package/dist/build/vite/plugins/translations.js +406 -33
  32. package/dist/build/vite/plugins/translations.test.d.ts +2 -0
  33. package/dist/build/vite/plugins/translations.test.d.ts.map +1 -0
  34. package/dist/build/vite/plugins/translations.test.js +197 -0
  35. package/dist/cli/bundle.json +2 -2
  36. package/dist/cli/index.js +1449 -1095
  37. package/dist/runtime/app/FimoProviders.d.ts +4 -2
  38. package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
  39. package/dist/runtime/app/FimoProviders.js +30 -5
  40. package/dist/runtime/app/FimoScripts.d.ts.map +1 -1
  41. package/dist/runtime/app/FimoScripts.js +35 -1
  42. package/dist/runtime/app/index.d.ts +5 -1
  43. package/dist/runtime/app/index.d.ts.map +1 -1
  44. package/dist/runtime/app/index.js +2 -0
  45. package/dist/runtime/app/prefetch.d.ts +1 -0
  46. package/dist/runtime/app/prefetch.d.ts.map +1 -1
  47. package/dist/runtime/app/prefetch.js +12 -3
  48. package/dist/runtime/i18n/FimoLocaleContext.d.ts +9 -0
  49. package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -0
  50. package/dist/runtime/i18n/FimoLocaleContext.js +17 -0
  51. package/dist/runtime/i18n/locale.d.ts +23 -0
  52. package/dist/runtime/i18n/locale.d.ts.map +1 -0
  53. package/dist/runtime/i18n/locale.js +112 -0
  54. package/dist/runtime/i18n/locale.test.d.ts +2 -0
  55. package/dist/runtime/i18n/locale.test.d.ts.map +1 -0
  56. package/dist/runtime/i18n/locale.test.js +58 -0
  57. package/dist/runtime/index.d.ts +4 -0
  58. package/dist/runtime/index.d.ts.map +1 -1
  59. package/dist/runtime/index.js +2 -0
  60. package/dist/runtime/paths/get-fimo-paths.d.ts +3 -0
  61. package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
  62. package/dist/runtime/paths/get-fimo-paths.js +51 -34
  63. package/dist/runtime/paths/types.d.ts +2 -0
  64. package/dist/runtime/paths/types.d.ts.map +1 -1
  65. package/dist/runtime/primitives/components/Text.d.ts +1 -1
  66. package/dist/runtime/primitives/components/Text.js +1 -1
  67. package/dist/runtime/primitives/index.d.ts +1 -1
  68. package/dist/runtime/primitives/index.d.ts.map +1 -1
  69. package/dist/runtime/primitives/index.js +2 -2
  70. package/dist/runtime/primitives/lib/query.d.ts +15 -4
  71. package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
  72. package/dist/runtime/primitives/lib/template.d.ts +1 -1
  73. package/dist/runtime/primitives/lib/template.js +1 -1
  74. package/dist/runtime/primitives/translations.d.ts +19 -5
  75. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  76. package/dist/runtime/primitives/translations.js +48 -11
  77. package/dist/runtime/react-router/index.d.ts +8 -1
  78. package/dist/runtime/react-router/index.d.ts.map +1 -1
  79. package/dist/runtime/react-router/index.js +4 -0
  80. package/dist/runtime/react-router/navigation-utils.d.ts +8 -0
  81. package/dist/runtime/react-router/navigation-utils.d.ts.map +1 -0
  82. package/dist/runtime/react-router/navigation-utils.js +36 -0
  83. package/dist/runtime/react-router/navigation.d.ts +12 -0
  84. package/dist/runtime/react-router/navigation.d.ts.map +1 -0
  85. package/dist/runtime/react-router/navigation.js +28 -0
  86. package/dist/runtime/react-router/navigation.test.d.ts +2 -0
  87. package/dist/runtime/react-router/navigation.test.d.ts.map +1 -0
  88. package/dist/runtime/react-router/navigation.test.js +35 -0
  89. package/dist/runtime/react-router/routes.d.ts +15 -0
  90. package/dist/runtime/react-router/routes.d.ts.map +1 -0
  91. package/dist/runtime/react-router/routes.js +71 -0
  92. package/dist/runtime/react-router/routes.test.d.ts +2 -0
  93. package/dist/runtime/react-router/routes.test.d.ts.map +1 -0
  94. package/dist/runtime/react-router/routes.test.js +67 -0
  95. package/dist/runtime/seo/htmlProps.d.ts +5 -2
  96. package/dist/runtime/seo/htmlProps.d.ts.map +1 -1
  97. package/dist/runtime/seo/htmlProps.js +6 -5
  98. package/dist/runtime/seo/index.d.ts +1 -0
  99. package/dist/runtime/seo/index.d.ts.map +1 -1
  100. package/dist/runtime/seo/index.js +3 -2
  101. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  102. package/dist/runtime/shared/fimo-config.server.js +1 -0
  103. package/dist/runtime/shared/fimo-config.types.d.ts +23 -0
  104. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  105. package/dist/scripts/extract-translations.d.ts +8 -8
  106. package/dist/scripts/extract-translations.js +22 -57
  107. package/dist/scripts/lib/parse-routes-file.js +4 -3
  108. package/dist/scripts/lint-translation-keys.js +24 -5
  109. package/dist/scripts/lint-translation-keys.test.d.ts +1 -0
  110. package/dist/scripts/lint-translation-keys.test.js +16 -0
  111. package/dist/scripts/validate-route-metadata.js +104 -0
  112. package/dist/scripts/validate-route-metadata.test.d.ts +1 -0
  113. package/dist/scripts/validate-route-metadata.test.js +116 -0
  114. package/package.json +5 -1
  115. package/release.json +2 -2
  116. package/scripts/lib/postinstall-onboarding.mjs +3 -1
  117. package/scripts/postinstall.mjs +2 -4
  118. package/scripts/publish-npm.mjs +3 -1
  119. package/templates/react-router/fimo-config.json +9 -0
  120. package/templates/react-router/package.json +1 -1
  121. package/templates/react-router/src/index.css +11 -5
  122. package/templates/react-router/src/pages/legal/LegalPage.tsx +5 -5
  123. package/templates/react-router/src/routes.ts +4 -1
  124. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +0 -66
  125. package/assets/skills/fimo/references/translations.md +0 -75
  126. package/assets/skills/fimo-cli/references/translations.md +0 -43
  127. package/dist/scripts/inject-translations.d.ts +0 -6
  128. package/dist/scripts/inject-translations.js +0 -168
  129. 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
  }
@@ -9,7 +9,9 @@
9
9
  %>
10
10
  // AUTO-GENERATED ⚠️ – do not edit by hand
11
11
  import { useSuspenseQuery, useQuery, useMutation, useQueryClient, QueryKey } from "@tanstack/react-query";
12
- import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type Fields, type Projected, type Query, type Sort, type Where } from "fimo/ui";
12
+ import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, useLocale, 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 ──────────── */
@@ -18,12 +20,19 @@ import { FimoString, FimoDate, FimoMedia, FimoBoolean, FimoRichText, type Fields
18
20
  /**
19
21
  * <%= model.pascal %> entity interface
20
22
  */
23
+ export interface <%= model.pascal %>FimoMeta {
24
+ contentType: string;
25
+ translationProgress: { state: "queued" | "translating"; fields?: string[] } | null;
26
+ }
27
+
21
28
  export interface <%= model.pascal %> {
22
29
  id: string;
30
+ documentId: string;
23
31
  slug: FimoString;
24
- contentType: string;
32
+ locale: string;
25
33
  createdAt: string;
26
34
  updatedAt: string;
35
+ __fimo: <%= model.pascal %>FimoMeta;
27
36
  <%
28
37
  // Generate interface properties based on fields
29
38
  if (model.fields) {
@@ -82,13 +91,19 @@ export type <%= model.pascal %>Result<TFields extends <%= model.pascal %>Fields
82
91
  function wrapWithSource(entry: Record<string, any>): <%= model.pascal %> {
83
92
  const id = entry.id;
84
93
  const sourcePrefix = `<%= model.pascal %>.${id}`;
94
+ const entryFimo = entry.__fimo && typeof entry.__fimo === "object" ? entry.__fimo : {};
85
95
 
86
96
  return {
87
97
  id,
98
+ documentId: entry.documentId,
88
99
  slug: entry.slug != null ? new FimoString(entry.slug, `${sourcePrefix}.slug`) : entry.slug,
89
- contentType: entry.contentType,
100
+ locale: entry.locale,
90
101
  createdAt: entry.createdAt,
91
102
  updatedAt: entry.updatedAt,
103
+ __fimo: {
104
+ contentType: typeof entryFimo.contentType === "string" ? entryFimo.contentType : "<%= model.uid %>",
105
+ translationProgress: entryFimo.translationProgress && typeof entryFimo.translationProgress === "object" ? entryFimo.translationProgress as <%= model.pascal %>FimoMeta["translationProgress"] : null,
106
+ },
92
107
  <%
93
108
  if (model.fields) {
94
109
  Object.entries(model.fields).forEach(([key, field]) => {
@@ -137,6 +152,14 @@ if (model.fields) {
137
152
  // @ts-ignore - meta.env is a vite feature
138
153
  const base = import.meta.env.VITE_API_URL;
139
154
 
155
+ function defaultLocale(): string {
156
+ return (fimoConfig as { i18n?: { defaultLocale?: string } }).i18n?.defaultLocale ?? "en";
157
+ }
158
+
159
+ function useQueryLocale(locale?: string): string {
160
+ return locale ?? useLocale().locale ?? defaultLocale();
161
+ }
162
+
140
163
  function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
141
164
  const h: Record<string, string> = { ...extra };
142
165
  // @ts-ignore
@@ -145,23 +168,47 @@ function fimoHeaders(extra?: Record<string, string>): Record<string, string> {
145
168
  return h;
146
169
  }
147
170
 
148
- export async function getById(id: string): Promise<<%= model.pascal %>> {
149
- const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, { headers: fimoHeaders() });
171
+ function withLocale(url: string, locale?: string): string {
172
+ const u = new URL(url, "http://fimo.local");
173
+ u.searchParams.set("locale", locale ?? defaultLocale());
174
+ return `${u.pathname}${u.search}`;
175
+ }
176
+
177
+ function splitWritePayload(payload: Partial<<%= model.pascal %>>): Record<string, unknown> {
178
+ const {
179
+ id,
180
+ documentId,
181
+ locale,
182
+ createdAt,
183
+ updatedAt,
184
+ ...data
185
+ } = payload as Record<string, unknown>;
186
+ delete data.__fimo;
187
+
188
+ return {
189
+ data,
190
+ ...(typeof locale === "string" && locale.length > 0 ? { locale } : {}),
191
+ ...(typeof documentId === "string" && documentId.length > 0 ? { documentId } : {}),
192
+ };
193
+ }
194
+
195
+ export async function getById(id: string, options: { locale?: string } = {}): Promise<<%= model.pascal %>> {
196
+ const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/${id}`, options.locale)}`, { headers: fimoHeaders() });
150
197
  if (!res.ok) throw new Error(await res.text());
151
198
  const data = await res.json();
152
199
  return wrapWithSource(data.data);
153
200
  }
154
201
 
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() });
202
+ export async function getBySlug(slug: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
203
+ const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/slug/${encodeURIComponent(slug)}`, options.locale)}`, { headers: fimoHeaders() });
157
204
  if (res.status === 404) return null;
158
205
  if (!res.ok) throw new Error(await res.text());
159
206
  const data = await res.json();
160
207
  return wrapWithSource(data.data);
161
208
  }
162
209
 
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() });
210
+ export async function getByField(fieldName: string, value: string, options: { locale?: string } = {}): Promise<<%= model.pascal %> | null> {
211
+ const res = await fetch(`${base}${withLocale(`/entries/<%= model.uid %>/field/${encodeURIComponent(fieldName)}/${encodeURIComponent(value)}`, options.locale)}`, { headers: fimoHeaders() });
165
212
  if (res.status === 404) return null;
166
213
  if (!res.ok) throw new Error(await res.text());
167
214
  const data = await res.json();
@@ -189,6 +236,10 @@ function toQueryString<TFields extends <%= model.pascal %>Fields | undefined = u
189
236
  query.set("where", JSON.stringify(where));
190
237
  }
191
238
 
239
+ if (!query.has("locale")) {
240
+ query.set("locale", defaultLocale());
241
+ }
242
+
192
243
  return query.toString();
193
244
  }
194
245
 
@@ -204,7 +255,7 @@ export async function create(payload: Partial<<%= model.pascal %>>): Promise<<%=
204
255
  const res = await fetch(`${base}/entries/<%= model.uid %>`, {
205
256
  method: "POST",
206
257
  headers: fimoHeaders({ "Content-Type": "application/json" }),
207
- body: JSON.stringify({ data: payload })
258
+ body: JSON.stringify(splitWritePayload(payload))
208
259
  });
209
260
  if (!res.ok) throw new Error(await res.text());
210
261
  const data = await res.json();
@@ -215,7 +266,7 @@ export async function update(id: string, payload: Partial<<%= model.pascal %>>):
215
266
  const res = await fetch(`${base}/entries/<%= model.uid %>/${id}`, {
216
267
  method: "PUT",
217
268
  headers: fimoHeaders({ "Content-Type": "application/json" }),
218
- body: JSON.stringify({ data: payload })
269
+ body: JSON.stringify(splitWritePayload(payload))
219
270
  });
220
271
  if (!res.ok) throw new Error(await res.text());
221
272
  const data = await res.json();
@@ -233,42 +284,52 @@ export async function remove(id: string): Promise<void> {
233
284
 
234
285
  const qk = {
235
286
  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,
287
+ byId : (id: string, locale?: string) => ["<%= model.pascal %>", id, locale ?? defaultLocale()] as const,
288
+ bySlug : (slug: string, locale?: string) => ["<%= model.pascal %>", "slug", slug, locale ?? defaultLocale()] as const,
289
+ byField : (field: string, value: string, locale?: string) => ["<%= model.pascal %>", field, value, locale ?? defaultLocale()] as const,
239
290
  list : (params: <%= model.pascal %>Query) => ["<%= model.pascal %>", params] as const
240
291
  };
241
292
 
242
- export function useGetById(id: string) {
243
- return useQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id), queryFn: () => getById(id) });
293
+ export function useGetById(id: string, options: { locale?: string } = {}) {
294
+ const locale = useQueryLocale(options.locale);
295
+ return useQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, locale), queryFn: () => getById(id, { ...options, locale }) });
244
296
  }
245
297
 
246
- export function useGetBySlug(slug: string) {
247
- return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug), queryFn: () => getBySlug(slug), enabled: !!slug });
298
+ export function useGetBySlug(slug: string, options: { locale?: string } = {}) {
299
+ const locale = useQueryLocale(options.locale);
300
+ return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, locale), queryFn: () => getBySlug(slug, { ...options, locale }), enabled: !!slug });
248
301
  }
249
302
 
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 });
303
+ export function useGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
304
+ const locale = useQueryLocale(options.locale);
305
+ return useQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, locale), queryFn: () => getByField(fieldName, value, { ...options, locale }), enabled: !!value });
252
306
  }
253
307
 
254
308
  export function useGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
255
- return useQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(params), queryFn: () => get(params) });
309
+ const locale = useQueryLocale(params.locale);
310
+ const localizedParams = { ...params, locale };
311
+ return useQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
256
312
  }
257
313
 
258
- export function useSuspenseGetById(id: string) {
259
- return useSuspenseQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id), queryFn: () => getById(id) });
314
+ export function useSuspenseGetById(id: string, options: { locale?: string } = {}) {
315
+ const locale = useQueryLocale(options.locale);
316
+ return useSuspenseQuery<<%= model.pascal %>, Error>({ queryKey: qk.byId(id, locale), queryFn: () => getById(id, { ...options, locale }) });
260
317
  }
261
318
 
262
- export function useSuspenseGetBySlug(slug: string) {
263
- return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug), queryFn: () => getBySlug(slug) });
319
+ export function useSuspenseGetBySlug(slug: string, options: { locale?: string } = {}) {
320
+ const locale = useQueryLocale(options.locale);
321
+ return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.bySlug(slug, locale), queryFn: () => getBySlug(slug, { ...options, locale }) });
264
322
  }
265
323
 
266
- export function useSuspenseGetByField(fieldName: string, value: string) {
267
- return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value), queryFn: () => getByField(fieldName, value) });
324
+ export function useSuspenseGetByField(fieldName: string, value: string, options: { locale?: string } = {}) {
325
+ const locale = useQueryLocale(options.locale);
326
+ return useSuspenseQuery<<%= model.pascal %> | null, Error>({ queryKey: qk.byField(fieldName, value, locale), queryFn: () => getByField(fieldName, value, { ...options, locale }) });
268
327
  }
269
328
 
270
329
  export function useSuspenseGet<TFields extends <%= model.pascal %>Fields | undefined = undefined>(params: <%= model.pascal %>Query<TFields> = {} as <%= model.pascal %>Query<TFields>) {
271
- return useSuspenseQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(params), queryFn: () => get(params) });
330
+ const locale = useQueryLocale(params.locale);
331
+ const localizedParams = { ...params, locale };
332
+ return useSuspenseQuery<<%= model.pascal %>Result<TFields>[], Error>({ queryKey: qk.list(localizedParams), queryFn: () => get(localizedParams) });
272
333
  }
273
334
 
274
335
  export function useCreate() {
@@ -3,7 +3,7 @@ name: fimo
3
3
  description: >-
4
4
  How to write code in a Fimo project — the npm package surface. Load when
5
5
  writing JSX that renders schema content, defining schemas or forms (JSON),
6
- using `t()` for translations, plugging media into `fimo/ui` components,
6
+ using `t()` for labels, plugging media into `fimo/ui` components,
7
7
  or applying the project's code conventions. CLI commands and workflows
8
8
  (push, deploy, sync, etc.) live in the `fimo-cli` skill.
9
9
  ---
@@ -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
 
@@ -29,15 +29,16 @@ If you find yourself about to hardcode anything user-facing, **stop and use the
29
29
 
30
30
  ## When starting code work, load these references up front
31
31
 
32
- These define the package surface for the load-bearing primitives. **Read all five before generating significant code:**
32
+ These define the package surface for the load-bearing primitives. **Read all six before generating significant code:**
33
33
 
34
34
  - `references/content.md` — schema JSON shape, field types, Tiptap nodes, generated React Query hooks
35
35
  - `references/forms.md` — form JSON spec, generated Zod + `submitX` helper
36
36
  - `references/assets.md` — `FimoMedia` shape, how to render assets
37
- - `references/translations.md` — `t()` helper, `useTranslations()`, wrap rules
37
+ - `references/labels.md` — `t()` helper, `useLabels()`, wrap rules
38
+ - `references/locales.md` — locale routing helpers, `FimoLink`, custom-framework contract
38
39
  - `references/ui.md` — `fimo/ui` components + JSX code conventions + app code rules
39
40
 
40
- Workflow / CLI-side concerns (pushing schemas, generating images, syncing translations, deploying) live in the **`fimo-cli`** skill's matching references.
41
+ Workflow / CLI-side concerns (pushing schemas, generating images, managing labels, deploying) live in the **`fimo-cli`** skill's matching references.
41
42
 
42
43
  ## Decision Tree (specific tasks)
43
44
 
@@ -47,7 +48,8 @@ Workflow / CLI-side concerns (pushing schemas, generating images, syncing transl
47
48
  - Writing schema JSON / picking field types / Tiptap richtext → `references/content.md`
48
49
  - Writing form JSON / generated form client / form code → `references/forms.md`
49
50
  - Plugging media into a component / `FimoMedia` shape → `references/assets.md`
50
- - ANY user-facing string in code → `references/translations.md`
51
+ - ANY user-facing string in code → `references/labels.md`
52
+ - Locale-aware routes, links, or framework adapters → `references/locales.md`
51
53
  - ANY JSX rendering schema content or `t()` output → `references/ui.md`
52
54
  - Major visual / redesign / hero section / brand → `references/design.md`
53
55
 
@@ -55,7 +57,7 @@ Workflow / CLI-side concerns (pushing schemas, generating images, syncing transl
55
57
 
56
58
  - **Hardcoding content in JSX** — `<h1>My Blog</h1>` + an array of posts in code. Use schemas + entries + `t()`.
57
59
  - **Raw HTML for schema content** — `<img src={post.coverImage.url}>` instead of `<Image value={post.coverImage}>`. The `fimo/ui` components carry metadata for inline editing in the admin.
58
- - **Raw `<a href>` for internal navigation** — causes full page reloads. Use `<Link>` / `<NavLink>` from react-router. Reserve `<a>` for external links.
60
+ - **Raw `<a href>` for internal navigation** — causes full page reloads. Use `<FimoLink>` for locale-aware internal links or React Router `<Link>` when intentionally bypassing locale helpers. Reserve `<a>` for external links.
59
61
  - **Pulling stock photos from external URLs** — editors can't manage them. Use `fimo generate` (CLI).
60
62
  - **Editing generated `.ts` files** under `src/schemas/` or `src/forms/` — regenerated on every build.
61
63
  - **Writing raw HTML in `richtext` fields** — must be Tiptap JSONContent.
@@ -0,0 +1,13 @@
1
+ import { FimoLink } from 'fimo/react-router';
2
+
3
+ export function Navigation() {
4
+ return (
5
+ <nav>
6
+ <FimoLink to="/">Home</FimoLink>
7
+ <FimoLink to="/pricing">Pricing</FimoLink>
8
+ <FimoLink to="/pricing" locale="es">
9
+ Pricing in Spanish
10
+ </FimoLink>
11
+ </nav>
12
+ );
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { RouteConfig } from '@react-router/dev/routes';
2
+ import { index, route } from '@react-router/dev/routes';
3
+ import { fimoRoutes } from 'fimo/react-router/routes';
4
+
5
+ const routes = [
6
+ index('./pages/Index.tsx', { id: 'home' }),
7
+ route('pricing', './pages/Pricing.tsx', { id: 'pricing' }),
8
+ route('blog/:slug', './pages/BlogPost.tsx', { id: 'blog-post' }),
9
+ ] satisfies RouteConfig;
10
+
11
+ export default fimoRoutes(routes) satisfies RouteConfig;
@@ -114,10 +114,15 @@ After pre-build (or `fimo deploy`), `src/schemas/{Uid}.ts` exports typed helpers
114
114
  ```ts
115
115
  interface BlogPost {
116
116
  id: string;
117
+ documentId: string;
117
118
  slug: FimoString;
118
- contentType: string;
119
+ locale: string;
119
120
  createdAt: string;
120
121
  updatedAt: string;
122
+ __fimo: {
123
+ contentType: string;
124
+ translationProgress: { state: 'queued' | 'translating'; fields?: string[] } | null;
125
+ };
121
126
  title: FimoString;
122
127
  body: FimoRichText;
123
128
  coverImage: FimoMedia;
@@ -127,6 +132,34 @@ interface BlogPost {
127
132
  }
128
133
  ```
129
134
 
135
+ Content fields live at the top level. Fimo-owned metadata that is not part of the user schema lives under `__fimo`, so a schema can safely define fields named `contentType`, `sourceLocale`, `translationStatus`, or `translationMeta`.
136
+
137
+ ## Schema i18n
138
+
139
+ Content auto-translation uses `fimo-config.json#i18n.autoTranslateContent` as the global switch. Use schema-level `i18n.autoTranslate` for translatable content types and field-level `i18n.autoTranslate: false` for fields that must be copied exactly.
140
+
141
+ ```json
142
+ {
143
+ "uid": "BlogPost",
144
+ "name": "Blog Post",
145
+ "isSingleton": false,
146
+ "i18n": {
147
+ "autoTranslate": true,
148
+ "translatePrompt": "Use friendly editorial Spanish and keep product names unchanged."
149
+ },
150
+ "fields": {
151
+ "title": { "type": "string" },
152
+ "summary": { "type": "string" },
153
+ "sku": {
154
+ "type": "string",
155
+ "i18n": { "autoTranslate": false }
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ Use schema-level `translatePrompt` for wording rules that apply to the whole content type. Use field-level `i18n.autoTranslate: false` for fields that should be copied exactly.
162
+
130
163
  **Generated API:**
131
164
 
132
165
  ```ts
@@ -170,6 +203,7 @@ BlogPost.useGet({
170
203
  views: { $gte: 100 },
171
204
  title: { $contains: 'React' },
172
205
  coverImage: { $null: false },
206
+ __fimo: { contentType: { $eq: 'BlogPost' } },
173
207
  },
174
208
  sort: ['-publishedAt', 'title'],
175
209
  limit: 12,
@@ -210,7 +244,7 @@ Rules of thumb:
210
244
  - **Pass the whole primitive to the matching `fimo/ui` component** — `<Text value={post.title} />`, `<Image value={post.cover} />`, `<RichText value={post.body} />`, `<Boolean value={p.inStock} />`. Don't unwrap first; the component reads the source metadata so the field stays inline-editable in the dashboard.
211
245
  - **`FimoString extends String`**, so `typeof post.title === 'object'` and `post.title === 'Hi'` is `false`. It stringifies fine inside JSX and template literals; call `.toString()` only when something genuinely needs a primitive `string`.
212
246
  - **Never hand-construct** a `FimoString` / `FimoMedia` / etc. — that bypasses source tracking. For a literal string use `t()`; for a literal image use `<StaticImage>`.
213
- - `t()` also returns a `FimoString` (see `references/translations.md`).
247
+ - `t()` also returns a `FimoString` (see `references/labels.md`).
214
248
 
215
249
  `FimoMedia` and `FimoBoolean` shapes:
216
250
 
@@ -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/labels.md`. This includes form labels, placeholders, button text, success/error messages.
103
103
 
104
104
  ## Forms vs. schemas
105
105
 
@@ -0,0 +1,106 @@
1
+ # Labels — package surface
2
+
3
+ > For the CLI workflow (`fimo validate`, `fimo labels set`, bulk updates), see **`fimo-cli/references/labels.md`**.
4
+ > For locale-aware routing and links (`fimoRoutes`, `FimoLink`, custom framework contract), see **`references/locales.md`**.
5
+
6
+ This file covers the **package surface**: the `t()` helper, the `useLabels()` hook, and the code-side rules for wrapping strings.
7
+
8
+ ## The rule
9
+
10
+ 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')`.
11
+
12
+ 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.**
13
+
14
+ ## Setup (already wired)
15
+
16
+ Labels are wired automatically by the `fimo/vite` build plugin. `useLabels()` reads DB-backed label values for the active locale — 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 call `useLabels()`.
17
+
18
+ `useTranslations()` is kept as a legacy alias for older projects. Use `useLabels()` in new code.
19
+
20
+ In development, the generated app reloads when DB label values change. In production, the build embeds the DB-backed label bundle.
21
+
22
+ ## Locale behavior
23
+
24
+ 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.
25
+
26
+ 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.
27
+
28
+ `fimo-config.json` owns the locale list:
29
+
30
+ ```json
31
+ {
32
+ "i18n": {
33
+ "defaultLocale": "en",
34
+ "locales": ["en"],
35
+ "autoTranslateLabels": false,
36
+ "autoTranslateContent": true
37
+ }
38
+ }
39
+ ```
40
+
41
+ - `locales` is the enabled locale list and must include `defaultLocale`. Do not add non-default locales unless the user asks for multiple languages, localized routes, translations, or a specific locale.
42
+ - `autoTranslateLabels` is explicit opt-in. Runtime default is `false`; when the user asks for locales and does not opt out of automatic label translation, set it to `true`.
43
+ - `autoTranslateContent` is the global content auto-translation switch. Set it to `false` to disable content auto-translation globally.
44
+ - Content auto-translation can be narrowed or customized per schema and per field in `src/schemas/*.json`; see `references/content.md`.
45
+ - Public locale routing is project/framework code. In React Router projects, prefer `fimoRoutes()` plus `FimoLink`; for custom frameworks, pass the active locale to `FimoProviders`. See `references/locales.md`.
46
+
47
+ ## Usage
48
+
49
+ ```tsx
50
+ import { useLabels, Text } from 'fimo/ui';
51
+
52
+ export function Hero() {
53
+ const { t } = useLabels();
54
+ return (
55
+ <section>
56
+ <h1>
57
+ <Text value={t('hero.title')} />
58
+ </h1>
59
+ <p>
60
+ <Text value={t('hero.subtitle')} />
61
+ </p>
62
+ <input placeholder={String(t('hero.emailPlaceholder'))} />
63
+ <img alt={String(t('hero.imageAlt'))} src="..." />
64
+ </section>
65
+ );
66
+ }
67
+ ```
68
+
69
+ - `t(key)` returns a `FimoString` (wrapped for source tracking). Render it via `<Text value={...} />` inside JSX.
70
+ - For HTML attributes (`placeholder`, `alt`, `title`, `aria-label`) cast with `String(t(...))` — the attribute can't accept a `FimoString` object directly.
71
+ - If a key is missing from the DB, it renders empty. Run `fimo validate` and add missing values with `fimo labels set`.
72
+
73
+ ## Hard rules (enforced by the pre-build linter)
74
+
75
+ **The first argument to `t()` MUST be a string literal.** Dynamic keys break source tracking and fail the lint step.
76
+
77
+ ```tsx
78
+ // ❌ BAD — variable key
79
+ t(item.key);
80
+
81
+ // ❌ BAD — template with expression
82
+ t(`nav.${page}`);
83
+
84
+ // ✅ GOOD — put the t() call where the literal lives
85
+ const items = [{ label: t('nav.home') }, { label: t('nav.about') }];
86
+ items.map((item) => <li>{item.label}</li>);
87
+ ```
88
+
89
+ ## Key naming
90
+
91
+ - Dot-namespaced, lowercase, kebab or camelCase leaves: `hero.title`, `nav.signIn`, `footer.copyright`, `errors.required`.
92
+ - Namespace by **page/section/component**, not by feature (`contact.form.submitLabel`, not `forms.contactSubmit`).
93
+ - Values live in the DB. Do not put user-visible fallback copy in code.
94
+
95
+ ## What NOT to translate
96
+
97
+ - Dynamic content from entries or forms — that's already stored in the DB (content via schemas, submissions via forms).
98
+ - Content-type field values rendered via `fimo/ui` primitives (`<Text>`, `<RichText>`, …) — those carry their own admin-editing metadata.
99
+ - Purely decorative / non-text (icons, logos).
100
+ - Developer-only strings (`console.log`, error messages thrown for debugging).
101
+
102
+ ## Rule of thumb
103
+
104
+ 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.
105
+
106
+ 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/labels.md`.