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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FimoScripts.d.ts","sourceRoot":"","sources":["../../../src/runtime/app/FimoScripts.tsx"],"names":[],"mappings":"AAKA,OAAO,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"FimoScripts.d.ts","sourceRoot":"","sources":["../../../src/runtime/app/FimoScripts.tsx"],"names":[],"mappings":"AAKA,OAAO,0BAA0B,CAAC;AAgElC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,4CAkB1B"}
|
|
@@ -5,6 +5,8 @@ import rawConfig from 'virtual:fimo-config';
|
|
|
5
5
|
import overlayRuntime from 'virtual:fimo-overlay-runtime';
|
|
6
6
|
// @ts-ignore - side-effect import; vite-served so import.meta.hot works inside
|
|
7
7
|
import 'virtual:fimo-overlay-hmr';
|
|
8
|
+
// @ts-ignore - virtual module provided by translationsPlugin in fimo/vite
|
|
9
|
+
import { signature as labelsSignature } from 'virtual:translations';
|
|
8
10
|
const config = rawConfig;
|
|
9
11
|
const runtime = overlayRuntime;
|
|
10
12
|
function buildSiteSchemas() {
|
|
@@ -29,6 +31,37 @@ function buildSiteSchemas() {
|
|
|
29
31
|
};
|
|
30
32
|
return [organization, website];
|
|
31
33
|
}
|
|
34
|
+
function buildLabelDevInvalidationScript() {
|
|
35
|
+
// @ts-ignore - import.meta.env is a Vite feature
|
|
36
|
+
if (!import.meta.env.DEV) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return `
|
|
40
|
+
(() => {
|
|
41
|
+
if (window.__fimoLabelsPoller) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
window.__fimoLabelsSignature = ${JSON.stringify(labelsSignature)};
|
|
45
|
+
window.__fimoLabelsPoller = window.setInterval(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch('/@id/__x00__virtual:translations', { cache: 'no-store' });
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const source = await response.text();
|
|
52
|
+
const nextSignature = source.match(/signature\\s*=\\s*"([^"]*)"/)?.[1];
|
|
53
|
+
if (!nextSignature || nextSignature === window.__fimoLabelsSignature) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
window.__fimoLabelsSignature = nextSignature;
|
|
57
|
+
window.location.reload();
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}, 2000);
|
|
62
|
+
})();
|
|
63
|
+
`.trim();
|
|
64
|
+
}
|
|
32
65
|
/**
|
|
33
66
|
* Renders extra <script> tags in <head>:
|
|
34
67
|
* - Dev overlay runtime (window.error / unhandledrejection bridge to parent)
|
|
@@ -44,5 +77,6 @@ export function FimoScripts() {
|
|
|
44
77
|
// @ts-ignore - import.meta.env is a Vite feature
|
|
45
78
|
const previewScriptUrl = import.meta.env.VITE_PREVIEW_SCRIPT_URL;
|
|
46
79
|
const siteSchemas = buildSiteSchemas();
|
|
47
|
-
|
|
80
|
+
const labelDevInvalidationScript = buildLabelDevInvalidationScript();
|
|
81
|
+
return (_jsxs(_Fragment, { children: [runtime ? _jsx("script", { type: "module", dangerouslySetInnerHTML: { __html: runtime } }) : null, labelDevInvalidationScript ? (_jsx("script", { type: "module", dangerouslySetInnerHTML: { __html: labelDevInvalidationScript } })) : null, previewScriptUrl ? _jsx("script", { type: "module", src: previewScriptUrl }) : null, siteSchemas.map((schema, index) => (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } }, index)))] }));
|
|
48
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefetch.d.ts","sourceRoot":"","sources":["../../../src/runtime/app/prefetch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefetch.d.ts","sourceRoot":"","sources":["../../../src/runtime/app/prefetch.ts"],"names":[],"mappings":"AA6EA;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAoClG"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo/vite
|
|
2
2
|
import fimoConfig from 'virtual:fimo-config';
|
|
3
|
+
function defaultLocale() {
|
|
4
|
+
return fimoConfig.i18n?.defaultLocale ?? 'en';
|
|
5
|
+
}
|
|
3
6
|
/**
|
|
4
7
|
* Match a concrete URL against the routes in fimo-config.json.
|
|
5
8
|
* Returns the matching route config or null.
|
|
@@ -64,7 +67,9 @@ export async function resolveMetaEntries(url) {
|
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
69
|
const { schema, queryField } = paramInfo;
|
|
67
|
-
const
|
|
70
|
+
const entryUrl = new URL(`${base}/entries/${schema}/field/${encodeURIComponent(queryField)}/${encodeURIComponent(value)}`);
|
|
71
|
+
entryUrl.searchParams.set('locale', defaultLocale());
|
|
72
|
+
const res = await fetch(entryUrl);
|
|
68
73
|
if (res.ok) {
|
|
69
74
|
const json = await res.json();
|
|
70
75
|
data[paramName] = json.data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-fimo-paths.d.ts","sourceRoot":"","sources":["../../../src/runtime/paths/get-fimo-paths.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"get-fimo-paths.d.ts","sourceRoot":"","sources":["../../../src/runtime/paths/get-fimo-paths.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA4FtF"}
|
|
@@ -30,14 +30,18 @@ function readEnvValue(rootDir, key) {
|
|
|
30
30
|
function resolveApiBaseUrl(rootDir) {
|
|
31
31
|
return readEnvValue(rootDir, 'VITE_API_URL') ?? readEnvValue(rootDir, 'VITE_API_URL_DEV');
|
|
32
32
|
}
|
|
33
|
-
async function fetchEntries(baseUrl, schema, rootDir) {
|
|
34
|
-
const url = `${baseUrl.replace(/\/$/, '')}/entries/${schema}
|
|
33
|
+
async function fetchEntries(baseUrl, schema, rootDir, locale) {
|
|
34
|
+
const url = new URL(`${baseUrl.replace(/\/$/, '')}/entries/${schema}`);
|
|
35
|
+
url.searchParams.set('locale', locale);
|
|
36
|
+
if (isValidationBuild) {
|
|
37
|
+
url.searchParams.set('limit', '1');
|
|
38
|
+
}
|
|
35
39
|
const branch = readEnvValue(rootDir, 'VITE_FIMO_BRANCH');
|
|
36
40
|
const headers = {};
|
|
37
41
|
if (branch) {
|
|
38
42
|
headers['X-Fimo-Branch'] = branch;
|
|
39
43
|
}
|
|
40
|
-
const res = await fetch(
|
|
44
|
+
const res = await fetch(url, { headers });
|
|
41
45
|
if (!res.ok) {
|
|
42
46
|
throw new Error(`Failed to fetch ${schema}: ${res.status}`);
|
|
43
47
|
}
|
|
@@ -87,6 +91,7 @@ function matchesAny(routeKey, patterns) {
|
|
|
87
91
|
export async function getFimoPaths(opts = {}) {
|
|
88
92
|
const rootDir = opts.cwd ?? process.cwd();
|
|
89
93
|
const config = getConfigServer(rootDir);
|
|
94
|
+
const locale = config.i18n?.defaultLocale ?? 'en';
|
|
90
95
|
const routesEntries = Object.entries(config.routes ?? {});
|
|
91
96
|
// Pre-filter route keys by include/exclude before doing any network I/O.
|
|
92
97
|
const filteredRoutes = routesEntries.filter(([routeKey]) => {
|
|
@@ -131,7 +136,7 @@ export async function getFimoPaths(opts = {}) {
|
|
|
131
136
|
const schemas = [...new Set(paramEntries.map(([, p]) => p.schema))];
|
|
132
137
|
for (const schema of schemas) {
|
|
133
138
|
if (!schemaCache.has(schema)) {
|
|
134
|
-
schemaCache.set(schema, await fetchEntries(baseUrl, schema, rootDir));
|
|
139
|
+
schemaCache.set(schema, await fetchEntries(baseUrl, schema, rootDir, locale));
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
const entries = schemaCache.get(paramEntries[0][1].schema) ?? [];
|
|
@@ -14,7 +14,7 @@ type TextProps<T extends ElementType = 'span'> = {
|
|
|
14
14
|
* @example
|
|
15
15
|
* <Text value={post.title} />
|
|
16
16
|
* <Text value={post.title} as="h1" className="text-4xl" />
|
|
17
|
-
* <Text value={t('welcome'
|
|
17
|
+
* <Text value={t('welcome')} as="p" />
|
|
18
18
|
*
|
|
19
19
|
* // With render function for formatting
|
|
20
20
|
* <Text value={product.price}>{(p) => `$${Number(p).toLocaleString()}`}</Text>
|
|
@@ -11,7 +11,7 @@ import { getFimoParts, getFimoSource, useFimoLiveValue } from '../lib/index.js';
|
|
|
11
11
|
* @example
|
|
12
12
|
* <Text value={post.title} />
|
|
13
13
|
* <Text value={post.title} as="h1" className="text-4xl" />
|
|
14
|
-
* <Text value={t('welcome'
|
|
14
|
+
* <Text value={t('welcome')} as="p" />
|
|
15
15
|
*
|
|
16
16
|
* // With render function for formatting
|
|
17
17
|
* <Text value={product.price}>{(p) => `$${Number(p).toLocaleString()}`}</Text>
|
|
@@ -27,6 +27,10 @@ export type NullOnlyOps = {
|
|
|
27
27
|
};
|
|
28
28
|
export type FilterFor<T> = [T] extends [FimoString | string] ? string | StringOps : [T] extends [FimoBoolean | boolean] ? boolean | BoolOps : [T] extends [FimoDate | Date] ? string | DateOps : [T] extends [number] ? number | NumberOps : [T] extends [FimoMedia | FimoMedia[]] ? NullOnlyOps : [T] extends [FimoRichText] ? string | StringOps : NullOnlyOps;
|
|
29
29
|
type TopLevelWhere = {
|
|
30
|
+
documentId?: string | StringOps;
|
|
31
|
+
locale?: string | StringOps;
|
|
32
|
+
sourceLocale?: string | StringOps;
|
|
33
|
+
translationStatus?: string | StringOps;
|
|
30
34
|
createdAt?: string | DateOps;
|
|
31
35
|
updatedAt?: string | DateOps;
|
|
32
36
|
};
|
|
@@ -45,6 +49,7 @@ export type Query<T extends {
|
|
|
45
49
|
id: string;
|
|
46
50
|
}, TFields extends Fields<T> | undefined = Fields<T> | undefined> = {
|
|
47
51
|
where?: Where<T>;
|
|
52
|
+
locale?: string;
|
|
48
53
|
search?: string;
|
|
49
54
|
sort?: Sort<T> | Sort<T>[];
|
|
50
55
|
limit?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../../src/runtime/primitives/lib/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { FimoString } from './primitives.js';
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* <Text value={fimo`${post.firstName} ${post.lastName}`} />
|
|
7
|
-
* <Text value={fimo`${t('by'
|
|
7
|
+
* <Text value={fimo`${t('by')} ${post.author}`} />
|
|
8
8
|
*
|
|
9
9
|
* Each interpolated Fimo value preserves its individual source tracking.
|
|
10
10
|
* The <Text> component decomposes composed strings into separately editable spans.
|
|
@@ -4,7 +4,7 @@ import { FIMO_SOURCE, FimoString, hasFimoSource } from './primitives.js';
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* <Text value={fimo`${post.firstName} ${post.lastName}`} />
|
|
7
|
-
* <Text value={fimo`${t('by'
|
|
7
|
+
* <Text value={fimo`${t('by')} ${post.author}`} />
|
|
8
8
|
*
|
|
9
9
|
* Each interpolated Fimo value preserves its individual source tracking.
|
|
10
10
|
* The <Text> component decomposes composed strings into separately editable spans.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { FimoString } from './lib/index.js';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
__fimoLabelsPoller?: number;
|
|
5
|
+
__fimoLabelsSignature?: string;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* Hook to provide a translation function (`t`).
|
|
4
10
|
*
|
|
@@ -9,12 +15,12 @@ import { FimoString } from './lib/index.js';
|
|
|
9
15
|
* const { t } = useTranslations();
|
|
10
16
|
*
|
|
11
17
|
* // With Text component (recommended)
|
|
12
|
-
* <Text value={t('welcome'
|
|
18
|
+
* <Text value={t('welcome')} />
|
|
13
19
|
*
|
|
14
20
|
* // Escape hatch for attributes
|
|
15
|
-
* <input placeholder={String(t('email.placeholder'
|
|
21
|
+
* <input placeholder={String(t('email.placeholder'))} />
|
|
16
22
|
*/
|
|
17
23
|
export declare function useTranslations(): {
|
|
18
|
-
t: (fullKey: string
|
|
24
|
+
t: (fullKey: string) => FimoString;
|
|
19
25
|
};
|
|
20
26
|
//# sourceMappingURL=translations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/translations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/runtime/primitives/translations.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe;iBA4BT,MAAM,KAAG,UAAU;EAOxC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
1
2
|
// @ts-ignore - virtual module provided by translationsPlugin in fimo/vite
|
|
2
|
-
import { translations } from 'virtual:translations';
|
|
3
|
+
import { signature as translationSignature, translations } from 'virtual:translations';
|
|
3
4
|
import { FimoString } from './lib/index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Hook to provide a translation function (`t`).
|
|
@@ -11,14 +12,40 @@ import { FimoString } from './lib/index.js';
|
|
|
11
12
|
* const { t } = useTranslations();
|
|
12
13
|
*
|
|
13
14
|
* // With Text component (recommended)
|
|
14
|
-
* <Text value={t('welcome'
|
|
15
|
+
* <Text value={t('welcome')} />
|
|
15
16
|
*
|
|
16
17
|
* // Escape hatch for attributes
|
|
17
|
-
* <input placeholder={String(t('email.placeholder'
|
|
18
|
+
* <input placeholder={String(t('email.placeholder'))} />
|
|
18
19
|
*/
|
|
19
20
|
export function useTranslations() {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!import.meta.env.DEV || typeof window === 'undefined' || window.__fimoLabelsPoller) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
window.__fimoLabelsSignature = translationSignature;
|
|
26
|
+
window.__fimoLabelsPoller = window.setInterval(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch('/@id/__x00__virtual:translations', { cache: 'no-store' });
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const source = await response.text();
|
|
33
|
+
const nextSignature = source.match(/signature\s*=\s*"([^"]*)"/)?.[1];
|
|
34
|
+
if (!nextSignature) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (nextSignature !== window.__fimoLabelsSignature) {
|
|
38
|
+
window.__fimoLabelsSignature = nextSignature;
|
|
39
|
+
window.location.reload();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}, 2000);
|
|
46
|
+
}, []);
|
|
47
|
+
const t = (fullKey) => {
|
|
48
|
+
const text = (translations && translations[fullKey]) ?? '';
|
|
22
49
|
return new FimoString(text, `__translations.${fullKey}.defaultValue`);
|
|
23
50
|
};
|
|
24
51
|
return { t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns the attributes Fimo injects onto the root `<html>` element.
|
|
3
|
-
* Currently: `lang` derived from `seo.locale`.
|
|
3
|
+
* Currently: `lang` derived from `seo.locale` or `i18n.defaultLocale`.
|
|
4
4
|
* May grow to include `dir`, `data-theme`, or other a11y/theming attributes
|
|
5
5
|
* without requiring changes to root.tsx.
|
|
6
6
|
*/
|
|
@@ -3,12 +3,12 @@ import rawConfig from 'virtual:fimo-config';
|
|
|
3
3
|
const config = rawConfig;
|
|
4
4
|
/**
|
|
5
5
|
* Returns the attributes Fimo injects onto the root `<html>` element.
|
|
6
|
-
* Currently: `lang` derived from `seo.locale`.
|
|
6
|
+
* Currently: `lang` derived from `seo.locale` or `i18n.defaultLocale`.
|
|
7
7
|
* May grow to include `dir`, `data-theme`, or other a11y/theming attributes
|
|
8
8
|
* without requiring changes to root.tsx.
|
|
9
9
|
*/
|
|
10
10
|
export function fimoHtmlProps() {
|
|
11
|
-
const locale = config.seo?.locale;
|
|
11
|
+
const locale = config.seo?.locale ?? config.i18n?.defaultLocale;
|
|
12
12
|
const lang = locale ? locale.replace('_', '-') : 'en';
|
|
13
13
|
return { lang };
|
|
14
14
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fimo-config.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"fimo-config.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAA4C,MAAM,wBAAwB,CAAC;AAiBnG;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAe3D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD"}
|
|
@@ -32,8 +32,15 @@ export interface FimoRouteConfig {
|
|
|
32
32
|
}>;
|
|
33
33
|
seo?: RouteSEO;
|
|
34
34
|
}
|
|
35
|
+
export interface FimoI18nConfig {
|
|
36
|
+
defaultLocale?: string;
|
|
37
|
+
locales?: string[];
|
|
38
|
+
autoTranslateLabels?: boolean;
|
|
39
|
+
autoTranslateContent?: boolean;
|
|
40
|
+
}
|
|
35
41
|
export interface FimoConfig {
|
|
36
42
|
seo?: FimoSEO;
|
|
43
|
+
i18n?: FimoI18nConfig;
|
|
37
44
|
routes?: Record<string, FimoRouteConfig>;
|
|
38
45
|
version?: string | number;
|
|
39
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B"}
|
|
1
|
+
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface LabelKeyDeclaration {
|
|
2
2
|
key: string;
|
|
3
|
-
|
|
4
|
-
locale: string;
|
|
3
|
+
file: string;
|
|
5
4
|
}
|
|
6
|
-
export interface
|
|
7
|
-
|
|
8
|
-
updated: TranslationRecord[];
|
|
9
|
-
deleted: TranslationRecord[];
|
|
5
|
+
export interface TranslationScanResult {
|
|
6
|
+
keys: LabelKeyDeclaration[];
|
|
10
7
|
}
|
|
8
|
+
export declare function extractLabelKeys(options?: {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}): LabelKeyDeclaration[];
|
|
11
11
|
export declare function extractTranslations(options?: {
|
|
12
12
|
cwd?: string;
|
|
13
|
-
}):
|
|
13
|
+
}): TranslationScanResult;
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extract
|
|
2
|
+
* Extract label keys from React components using oxc-parser.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Behavior:
|
|
8
|
-
* 1. Reads current `translations/en.json`
|
|
9
|
-
* 2. Walks every t() call site in `src/`
|
|
10
|
-
* 3. Writes the updated, sorted `translations/en.json`
|
|
11
|
-
* 4. Returns a diff: { created, updated, deleted }
|
|
4
|
+
* Labels are DB-owned. This scanner reports literal `t("key")` call sites so
|
|
5
|
+
* `fimo validate` can verify the tenant DB contains visible values. It never
|
|
6
|
+
* writes label values to local files.
|
|
12
7
|
*/
|
|
13
|
-
import { existsSync,
|
|
14
|
-
import {
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
15
10
|
import { parseSync } from 'oxc-parser';
|
|
16
11
|
function findReactFiles(dir, files = []) {
|
|
17
12
|
if (!existsSync(dir)) {
|
|
@@ -34,7 +29,7 @@ function findReactFiles(dir, files = []) {
|
|
|
34
29
|
return files;
|
|
35
30
|
}
|
|
36
31
|
function extractFromFile(filePath) {
|
|
37
|
-
const
|
|
32
|
+
const declarations = [];
|
|
38
33
|
try {
|
|
39
34
|
const code = readFileSync(filePath, 'utf-8');
|
|
40
35
|
const result = parseSync(filePath, code);
|
|
@@ -50,9 +45,7 @@ function extractFromFile(filePath) {
|
|
|
50
45
|
const firstArg = args[0];
|
|
51
46
|
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
52
47
|
const key = firstArg.value;
|
|
53
|
-
|
|
54
|
-
const defaultValue = secondArg?.type === 'Literal' && typeof secondArg.value === 'string' ? secondArg.value : '';
|
|
55
|
-
translations[key] = defaultValue;
|
|
48
|
+
declarations.push({ key, file: filePath });
|
|
56
49
|
}
|
|
57
50
|
}
|
|
58
51
|
}
|
|
@@ -73,52 +66,24 @@ function extractFromFile(filePath) {
|
|
|
73
66
|
catch (error) {
|
|
74
67
|
console.error(`Failed to parse ${filePath}:`, error);
|
|
75
68
|
}
|
|
76
|
-
return
|
|
69
|
+
return declarations;
|
|
77
70
|
}
|
|
78
|
-
export function
|
|
71
|
+
export function extractLabelKeys(options = {}) {
|
|
79
72
|
const cwd = options.cwd ?? process.cwd();
|
|
80
|
-
const translationsFile = join(cwd, 'translations/en.json');
|
|
81
73
|
const srcDir = join(cwd, 'src');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
currentTranslations = JSON.parse(readFileSync(translationsFile, 'utf-8'));
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// invalid JSON → start fresh
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const extractedTranslations = {};
|
|
74
|
+
const declarations = [];
|
|
75
|
+
const seen = new Set();
|
|
92
76
|
for (const file of findReactFiles(srcDir)) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!currentKeys.has(key)) {
|
|
100
|
-
diff.created.push({ key, defaultValue: extractedTranslations[key], locale: 'en' });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
for (const key of extractedKeys) {
|
|
104
|
-
if (currentKeys.has(key) && currentTranslations[key] !== extractedTranslations[key]) {
|
|
105
|
-
diff.updated.push({ key, defaultValue: extractedTranslations[key], locale: 'en' });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
for (const key of currentKeys) {
|
|
109
|
-
if (!extractedKeys.has(key)) {
|
|
110
|
-
diff.deleted.push({ key, defaultValue: currentTranslations[key], locale: 'en' });
|
|
77
|
+
for (const declaration of extractFromFile(file)) {
|
|
78
|
+
if (seen.has(declaration.key)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.add(declaration.key);
|
|
82
|
+
declarations.push(declaration);
|
|
111
83
|
}
|
|
112
84
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
const dir = dirname(translationsFile);
|
|
119
|
-
if (dir && !existsSync(dir)) {
|
|
120
|
-
mkdirSync(dir, { recursive: true });
|
|
121
|
-
}
|
|
122
|
-
writeFileSync(translationsFile, JSON.stringify(updatedTranslations, null, 2));
|
|
123
|
-
return diff;
|
|
85
|
+
return declarations.sort((a, b) => a.key.localeCompare(b.key));
|
|
86
|
+
}
|
|
87
|
+
export function extractTranslations(options = {}) {
|
|
88
|
+
return { keys: extractLabelKeys(options) };
|
|
124
89
|
}
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Standalone: node .fimo/i18n/scripts/lint-translation-keys.ts
|
|
5
5
|
* As module: import { lintTranslationKeys } from './lint-translation-keys.ts'
|
|
6
6
|
*
|
|
7
|
-
* Detects calls to t() where the first argument is NOT a string literal
|
|
7
|
+
* Detects calls to t() where the first argument is NOT a string literal or
|
|
8
|
+
* where code tries to pass an inline fallback value.
|
|
8
9
|
* This breaks source tracking for inline editing.
|
|
9
10
|
*
|
|
10
|
-
* ❌ BAD: t(item.titleKey
|
|
11
|
-
* ❌ BAD: t(
|
|
12
|
-
* ✅ GOOD: t('nav.home'
|
|
11
|
+
* ❌ BAD: t(item.titleKey) // Dynamic key
|
|
12
|
+
* ❌ BAD: t('nav.home', 'Home') // Inline fallback value
|
|
13
|
+
* ✅ GOOD: t('nav.home') // Literal DB-owned key
|
|
13
14
|
*/
|
|
14
15
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
15
16
|
import { join } from 'path';
|
|
@@ -46,8 +47,15 @@ This breaks source tracking for inline editing. Translation keys must be statica
|
|
|
46
47
|
items.map(item => t(item.key, item.label))
|
|
47
48
|
|
|
48
49
|
// Do this:
|
|
49
|
-
const items = [{ label: t('nav.home'
|
|
50
|
+
const items = [{ label: t('nav.home') }];
|
|
50
51
|
items.map(item => item.label)`;
|
|
52
|
+
const INLINE_DEFAULT_MESSAGE = `Inline label fallback detected. Label values live in the Fimo DB, not in code.
|
|
53
|
+
|
|
54
|
+
❌ BAD: t('nav.home', 'Home')
|
|
55
|
+
|
|
56
|
+
✅ FIX:
|
|
57
|
+
t('nav.home')
|
|
58
|
+
fimo labels set nav.home --value "Home"`;
|
|
51
59
|
// ── Exported Lint Rule ───────────────────────────────────────────────────────
|
|
52
60
|
/**
|
|
53
61
|
* Check a single file for dynamic translation keys.
|
|
@@ -79,6 +87,17 @@ export function lintTranslationKeys(filePath, code) {
|
|
|
79
87
|
code: code.slice(node.start, node.end),
|
|
80
88
|
});
|
|
81
89
|
}
|
|
90
|
+
if (node.arguments.length > 1) {
|
|
91
|
+
const { line, column } = getLineAndColumn(code, node.start);
|
|
92
|
+
errors.push({
|
|
93
|
+
file: filePath,
|
|
94
|
+
line,
|
|
95
|
+
column,
|
|
96
|
+
rule: 'labels/no-inline-default',
|
|
97
|
+
message: INLINE_DEFAULT_MESSAGE,
|
|
98
|
+
code: code.slice(node.start, node.end),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
for (const key of Object.keys(node)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { lintTranslationKeys } from './lint-translation-keys';
|
|
3
|
+
describe('lintTranslationKeys', () => {
|
|
4
|
+
it('allows literal DB-owned label keys', () => {
|
|
5
|
+
const errors = lintTranslationKeys('src/pages/Index.tsx', "const title = t('hero.title');");
|
|
6
|
+
expect(errors).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it('rejects inline fallback values', () => {
|
|
9
|
+
const errors = lintTranslationKeys('src/pages/Index.tsx', "const title = t('hero.title', 'Welcome');");
|
|
10
|
+
expect(errors).toEqual([expect.objectContaining({ rule: 'labels/no-inline-default' })]);
|
|
11
|
+
});
|
|
12
|
+
it('rejects dynamic keys', () => {
|
|
13
|
+
const errors = lintTranslationKeys('src/pages/Index.tsx', 'const title = t(heroKey);');
|
|
14
|
+
expect(errors).toEqual([expect.objectContaining({ rule: 'translation-keys' })]);
|
|
15
|
+
});
|
|
16
|
+
});
|
package/package.json
CHANGED
package/release.json
CHANGED