fimo 0.17.0 → 0.19.0
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/assets/astro/components/DateTime.astro +22 -0
- package/assets/astro/components/index.d.ts +1 -0
- package/assets/astro/components/index.js +1 -0
- package/assets/content-templates/shared/types.eta +2 -0
- package/assets/skills/fimo-cli/SKILL.md +8 -6
- package/assets/skills/fimo-cli/references/branches.md +2 -2
- package/assets/skills/fimo-cli/references/content.md +2 -2
- package/assets/skills/fimo-cli/references/deploy.md +17 -9
- package/assets/skills/fimo-cli/references/hosting.md +114 -0
- package/assets/skills/fimo-cli/references/init.md +13 -6
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
- package/assets/skills/fimo-cli/references/locales.md +38 -14
- package/assets/skills/fimo-cli/references/pages.md +70 -0
- package/assets/skills/fimo-cli/references/preview.md +26 -20
- package/assets/skills/fimo-cli/references/webhooks.md +1 -1
- package/assets/skills/fimo-code/SKILL.md +11 -0
- package/assets/skills/fimo-code/assets/react-router-locales/LocaleLayout.tsx +12 -0
- package/assets/skills/fimo-code/assets/react-router-locales/routes.ts +7 -6
- package/assets/skills/fimo-code/references/best-practices/astro.md +47 -0
- package/assets/skills/fimo-code/references/best-practices/next.md +42 -0
- package/assets/skills/fimo-code/references/best-practices/sveltekit.md +17 -0
- package/assets/skills/fimo-code/references/content/richtext.md +5 -0
- package/assets/skills/fimo-code/references/content/schema.md +4 -1
- package/assets/skills/fimo-code/references/frameworks/astro.md +5 -3
- package/assets/skills/fimo-code/references/frameworks/hosting.md +5 -2
- package/assets/skills/fimo-code/references/frameworks/next/app-router.md +6 -0
- package/assets/skills/fimo-code/references/frameworks/next/locales.md +12 -2
- package/assets/skills/fimo-code/references/frameworks/next.md +7 -0
- package/assets/skills/fimo-code/references/frameworks/react-router.md +15 -7
- package/assets/skills/fimo-code/references/frameworks/sveltekit.md +15 -8
- package/assets/skills/fimo-code/references/labels.md +9 -1
- package/assets/skills/fimo-code/references/locales.md +8 -2
- package/assets/skills/fimo-code/references/migrations/react-router-route-ownership.md +93 -0
- package/assets/skills/fimo-code/references/ui.md +21 -2
- package/assets/skills/fimo-migration/SKILL.md +276 -51
- package/assets/skills/fimo-migration/references/content.md +119 -35
- package/assets/skills/fimo-migration/references/decision.md +72 -0
- package/assets/skills/fimo-migration/references/forms.md +38 -6
- package/assets/skills/fimo-migration/references/frameworks/nextjs.md +179 -23
- package/assets/skills/fimo-migration/references/import-contract.md +71 -19
- package/assets/skills/fimo-migration/references/manifest.md +11 -161
- package/assets/skills/fimo-migration/references/media.md +33 -1
- package/assets/skills/fimo-migration/references/platforms.md +26 -2
- package/assets/skills/fimo-migration/references/qa.md +167 -13
- package/assets/skills/fimo-migration/references/workers/content-execution.md +39 -0
- package/assets/skills/fimo-migration/references/workers/content-planner.md +64 -0
- package/assets/skills/fimo-migration/references/workers/framework.md +37 -0
- package/assets/skills/fimo-migration/references/workers/validator.md +25 -0
- package/assets/skills/fimo-migration/references/workers.md +82 -0
- package/assets/skills/fimo-migration/references/workflow.md +140 -192
- package/assets/skills/fimo-migration/scripts/apply-decision.mjs +534 -0
- package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +7 -4
- package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +191 -4
- package/assets/skills/fimo-studio/references/ai-chat.md +1 -1
- package/assets/skills/fimo-studio/references/cms.md +25 -7
- package/assets/skills/fimo-studio/references/webhooks.md +1 -1
- package/assets/sveltekit/components/DateTime.svelte +35 -0
- package/assets/sveltekit/components/FimoProvider.svelte +2 -2
- package/assets/sveltekit/components/index.d.ts +1 -0
- package/assets/sveltekit/components/index.js +1 -0
- package/dist/build/next/config.d.ts +4 -1
- package/dist/build/next/config.js +21 -5
- package/dist/build/sveltekit/vite.d.ts +3 -4
- package/dist/build/sveltekit/vite.js +3 -4
- package/dist/build/vite/plugins/runtime-env.d.ts +2 -1
- package/dist/build/vite/plugins/runtime-env.js +14 -10
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +113 -108
- package/dist/runtime/app/FimoProviders.js +25 -14
- package/dist/runtime/app/prefetch.d.ts +3 -1
- package/dist/runtime/app/prefetch.js +11 -55
- package/dist/runtime/astro/preview.js +7 -9
- package/dist/runtime/content/client/internal.js +1 -0
- package/dist/runtime/i18n/locale.d.ts +6 -3
- package/dist/runtime/i18n/locale.js +16 -41
- package/dist/runtime/next/client-runtime.js +5 -1
- package/dist/runtime/next/client.d.ts +3 -2
- package/dist/runtime/next/client.js +2 -1
- package/dist/runtime/next/labels.d.ts +2 -2
- package/dist/runtime/next/labels.js +2 -2
- package/dist/runtime/next/preview.d.ts +13 -0
- package/dist/runtime/next/preview.js +34 -0
- package/dist/runtime/next/primitives/server.d.ts +2 -0
- package/dist/runtime/next/primitives/server.js +8 -0
- package/dist/runtime/next/provider.d.ts +5 -0
- package/dist/runtime/next/provider.js +17 -3
- package/dist/runtime/next/server-runtime.js +4 -1
- package/dist/runtime/next/server.d.ts +1 -1
- package/dist/runtime/next/server.js +1 -1
- package/dist/runtime/paths/get-fimo-paths.d.ts +6 -5
- package/dist/runtime/paths/get-fimo-paths.js +43 -42
- package/dist/runtime/preview/location.d.ts +19 -2
- package/dist/runtime/preview/location.js +16 -2
- package/dist/runtime/primitives/index.d.ts +1 -0
- package/dist/runtime/primitives/index.js +1 -0
- package/dist/runtime/react/primitives/editable/DateTime.d.ts +4 -0
- package/dist/runtime/react/primitives/editable/DateTime.js +13 -0
- package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +7 -1
- package/dist/runtime/react/primitives/renderers/DateRenderer.js +11 -8
- package/dist/runtime/react-router/{routes.d.ts → legacy/routes.d.ts} +6 -1
- package/dist/runtime/react-router/{routes.js → legacy/routes.js} +22 -7
- package/dist/runtime/routes/index.d.ts +162 -0
- package/dist/runtime/routes/index.js +335 -0
- package/dist/runtime/routing/AppRoutesProvider.js +4 -3
- package/dist/runtime/seo/resolver.d.ts +5 -0
- package/dist/runtime/seo/resolver.js +10 -42
- package/dist/runtime/shared/fimo-config.types.d.ts +14 -13
- package/dist/runtime/shared/preview-location.d.ts +1 -22
- package/dist/runtime/shared/preview-location.js +5 -93
- package/dist/runtime/sveltekit/preview.d.ts +12 -3
- package/dist/runtime/sveltekit/preview.js +13 -8
- package/dist/runtime/templates.d.ts +19 -0
- package/dist/runtime/templates.js +33 -0
- package/dist/scripts/sync-forms.js +1 -1
- package/dist/scripts/sync-schemas.js +1 -4
- package/dist/scripts/validate-fimo-config.js +5 -1
- package/dist/scripts/validate-page-registry.d.ts +2 -0
- package/dist/scripts/validate-page-registry.js +167 -0
- package/dist/scripts/validate-route-metadata.js +132 -41
- package/package.json +10 -5
- package/templates/astro/.fimo/config.json +13 -5
- package/templates/astro/AGENTS.md +5 -1
- package/templates/astro/astro.config.mjs +7 -0
- package/templates/astro/pnpm-workspace.yaml +0 -3
- package/templates/astro/src/components/ProjectOverview.astro +9 -6
- package/templates/astro/src/layouts/Base.astro +1 -1
- package/templates/astro/src/locales.ts +4 -0
- package/templates/astro/src/pages/[locale]/index.astro +19 -0
- package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
- package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
- package/templates/astro/src/pages/index.astro +5 -2
- package/templates/astro/src/pages/projects/[slug].astro +4 -2
- package/templates/astro/src/pages/projects/index.astro +5 -2
- package/templates/nextjs/.fimo/config.json +2 -1
- package/templates/nextjs/AGENTS.md +5 -0
- package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
- package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
- package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
- package/templates/nextjs/src/components/Document.tsx +26 -0
- package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
- package/templates/nextjs/src/locales.ts +13 -0
- package/templates/react-router/.fimo/config.json +3 -6
- package/templates/react-router/AGENTS.md +4 -0
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/pnpm-workspace.yaml +1 -3
- package/templates/react-router/react-router.config.ts +4 -10
- package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
- package/templates/react-router/src/root.tsx +2 -4
- package/templates/react-router/src/routes.ts +5 -3
- package/templates/sveltekit/.fimo/config.json +2 -1
- package/templates/sveltekit/AGENTS.md +5 -0
- package/templates/sveltekit/src/app.html +1 -1
- package/templates/sveltekit/src/hooks.server.ts +11 -0
- package/templates/sveltekit/src/locales.ts +8 -0
- package/templates/sveltekit/src/params/locale.ts +5 -0
- package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
- package/templates/sveltekit/src/routes/+layout.svelte +5 -0
- package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
- package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
- package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
- package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
- package/templates/nextjs/src/app/layout.tsx +0 -17
- /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
- /package/templates/sveltekit/src/routes/{+page.svelte → [[locale=locale]]/+page.svelte} +0 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { getLabels } from 'fimo/sveltekit';
|
|
2
2
|
|
|
3
|
+
import { resolveLocale } from '../locales';
|
|
4
|
+
|
|
3
5
|
export async function load(event) {
|
|
6
|
+
const locale = resolveLocale(event.params);
|
|
7
|
+
|
|
4
8
|
return {
|
|
5
|
-
|
|
9
|
+
locale,
|
|
10
|
+
labels: await getLabels({ event, locale }),
|
|
6
11
|
};
|
|
7
12
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { afterNavigate } from '$app/navigation';
|
|
2
3
|
import { FimoProvider } from 'fimo/sveltekit/components';
|
|
3
4
|
import type { Snippet } from 'svelte';
|
|
4
5
|
|
|
5
6
|
import type { LayoutData } from './$types';
|
|
6
7
|
|
|
7
8
|
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
|
9
|
+
|
|
10
|
+
afterNavigate(() => {
|
|
11
|
+
document.documentElement.lang = data.locale;
|
|
12
|
+
});
|
|
8
13
|
</script>
|
|
9
14
|
|
|
10
15
|
<FimoProvider labels={data.labels}>
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Deterministic inventory coordination
|
|
2
|
-
|
|
3
|
-
Prepare before running lanes:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
fimo migrate inventory <project-root> --prepare
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Read `inventory-plan.json`, then run each listed area in an independent worker
|
|
10
|
-
that treats application source as read-only:
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
fimo migrate inventory <project-root> --area runtime-config
|
|
14
|
-
fimo migrate inventory <project-root> --area locales
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Run only areas present in the plan. The `locales` area is currently planned
|
|
18
|
-
only when the source package manifest declares `next-intl`.
|
|
19
|
-
|
|
20
|
-
Workers must return the written deterministic fragment, not a prose summary.
|
|
21
|
-
They must read only the source root and write only their planned fragment and
|
|
22
|
-
execution receipt. A worker may report structured unresolved claims in its
|
|
23
|
-
fragment; process outcome and timing belong in the run receipt.
|
|
24
|
-
|
|
25
|
-
If independent workers are unavailable, run the same areas sequentially:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
fimo migrate inventory <project-root> --sequential
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
After every fan-out, run deterministic fan-in:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
fimo migrate inventory <project-root> --reconcile
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Use a separate validator worker that did not produce fragments to compare the
|
|
38
|
-
plan, source evidence, fragments, and unresolved claims. If it finds a gap,
|
|
39
|
-
reopen only the affected area and reconcile again. The CLI never starts an
|
|
40
|
-
LLM or chooses a harness.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# next-intl locale inventory
|
|
2
|
-
|
|
3
|
-
Use the shipped analyzer after `package.json` proves a next-intl dependency:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
node <this-skill>/scripts/inventory/locales/next-intl.mjs <source-root>
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
It detects conventional JSON dictionaries, JSON imports with one dynamic locale
|
|
10
|
-
segment, and static `defineRouting` locale/default-locale values. It emits
|
|
11
|
-
deterministic facts and file/line evidence without changing the source.
|
|
12
|
-
|
|
13
|
-
If request configuration does not reveal a custom JSON dictionary layout, let
|
|
14
|
-
the agent select one relative pattern containing `{locale}` exactly once:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
node <this-skill>/scripts/inventory/locales/next-intl.mjs <source-root> \
|
|
18
|
-
--messages-pattern 'src/translations/{locale}/common.json'
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The script rejects absolute paths, paths outside the source root, non-JSON
|
|
22
|
-
patterns, and ambiguous placeholders.
|
|
23
|
-
|
|
24
|
-
## Support boundary
|
|
25
|
-
|
|
26
|
-
The analyzer intentionally leaves a claim unresolved when its static source
|
|
27
|
-
shape is unknown. Current gaps include:
|
|
28
|
-
|
|
29
|
-
- locales/default locale declared only through legacy middleware or navigation
|
|
30
|
-
helpers;
|
|
31
|
-
- dynamic or remote locale configuration;
|
|
32
|
-
- non-JSON dictionaries such as extracted PO catalogs;
|
|
33
|
-
- public route reachability.
|
|
34
|
-
|
|
35
|
-
Treat package version as evidence that helps prioritize likely source shapes,
|
|
36
|
-
not as the primary switch. If the analyzer cannot prove a claim, follow the
|
|
37
|
-
temporary read-only strategy in `references/inventory/locales.md`. A zero exit
|
|
38
|
-
code means the analyzer ran successfully; read `unresolved[]` as the exact
|
|
39
|
-
claims still requiring evidence.
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Locale inventory
|
|
2
|
-
|
|
3
|
-
Inventory locale evidence without deciding that every dictionary is configured
|
|
4
|
-
or publicly routable.
|
|
5
|
-
|
|
6
|
-
## Evidence contract
|
|
7
|
-
|
|
8
|
-
Keep these facts distinct:
|
|
9
|
-
|
|
10
|
-
- **dictionary candidates**: locale-like values derived from message files or
|
|
11
|
-
records;
|
|
12
|
-
- **configured locales**: values proven by static application configuration;
|
|
13
|
-
- **default locale**: one statically proven fallback when present;
|
|
14
|
-
- **public locales**: values proven reachable through the application's routes;
|
|
15
|
-
- **unresolved claims**: individual facts that no strategy proved.
|
|
16
|
-
|
|
17
|
-
A successful script run means evidence collection executed correctly. It does
|
|
18
|
-
not mean every locale claim is known. Preserve proven facts when another claim
|
|
19
|
-
remains unresolved.
|
|
20
|
-
|
|
21
|
-
## Bounded fallback
|
|
22
|
-
|
|
23
|
-
1. Run the shipped strategy for the detected locale system.
|
|
24
|
-
2. If the dictionary layout is the only unknown, pass a supported bounded path
|
|
25
|
-
pattern to that strategy.
|
|
26
|
-
3. If a claim remains unresolved, inspect only the files likely to own it.
|
|
27
|
-
4. When repeated manual reads would be slower or less reliable, let the agent
|
|
28
|
-
create one disposable script in the harness or operating-system temporary
|
|
29
|
-
directory, outside the application source tree. The script must:
|
|
30
|
-
- read only the selected source root;
|
|
31
|
-
- perform no network calls or source writes;
|
|
32
|
-
- have explicit file, byte, or time bounds;
|
|
33
|
-
- print deterministic facts, evidence paths, and unresolved claims;
|
|
34
|
-
- avoid claiming route reachability from dictionary files alone.
|
|
35
|
-
5. Validate the output, copy its evidence into the canonical ledger, and remove
|
|
36
|
-
the temporary script. Do not commit it to the migrated application.
|
|
37
|
-
6. At fan-in, compare configured, default, dictionary, and public locales. Keep
|
|
38
|
-
every mismatch or unproven claim unresolved until evidence settles it.
|
|
39
|
-
|
|
40
|
-
If the same temporary strategy is needed repeatedly across migrations, turn it
|
|
41
|
-
into a tested shipped script in a separate Fimo change.
|
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import process from 'node:process';
|
|
6
|
-
|
|
7
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
8
|
-
'.git',
|
|
9
|
-
'.next',
|
|
10
|
-
'.output',
|
|
11
|
-
'.turbo',
|
|
12
|
-
'build',
|
|
13
|
-
'coverage',
|
|
14
|
-
'dist',
|
|
15
|
-
'node_modules',
|
|
16
|
-
'public',
|
|
17
|
-
]);
|
|
18
|
-
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx']);
|
|
19
|
-
const MAX_SOURCE_FILES = 5_000;
|
|
20
|
-
const MAX_SOURCE_BYTES = 1_000_000;
|
|
21
|
-
const MAX_DICTIONARY_FILES = 10_000;
|
|
22
|
-
const CONVENTIONAL_PATTERN = 'messages/{locale}.json';
|
|
23
|
-
|
|
24
|
-
function fail(message) {
|
|
25
|
-
process.stderr.write(`${message}\n`);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseArguments(argv) {
|
|
30
|
-
if (argv.length === 0) {
|
|
31
|
-
fail('Usage: next-intl.mjs <source-root> [--messages-pattern <relative-pattern>]');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const sourceRoot = argv[0];
|
|
35
|
-
let messagesPattern;
|
|
36
|
-
|
|
37
|
-
for (let index = 1; index < argv.length; index += 1) {
|
|
38
|
-
if (argv[index] !== '--messages-pattern' || messagesPattern !== undefined || !argv[index + 1]) {
|
|
39
|
-
fail('Usage: next-intl.mjs <source-root> [--messages-pattern <relative-pattern>]');
|
|
40
|
-
}
|
|
41
|
-
messagesPattern = argv[index + 1];
|
|
42
|
-
index += 1;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { sourceRoot, messagesPattern };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toPosix(value) {
|
|
49
|
-
return value.split(path.sep).join('/');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function lineOf(source, offset) {
|
|
53
|
-
return source.slice(0, offset).split('\n').length;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function validatePattern(patternValue, root) {
|
|
57
|
-
if (path.isAbsolute(patternValue)) {
|
|
58
|
-
fail('The messages pattern must be relative to the source root.');
|
|
59
|
-
}
|
|
60
|
-
if ((patternValue.match(/\{locale\}/g) ?? []).length !== 1) {
|
|
61
|
-
fail('The messages pattern must contain {locale} exactly once.');
|
|
62
|
-
}
|
|
63
|
-
if (!patternValue.endsWith('.json')) {
|
|
64
|
-
fail('The messages pattern must point to JSON files.');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const normalized = path.normalize(patternValue);
|
|
68
|
-
if ((normalized.match(/\{locale\}/g) ?? []).length !== 1) {
|
|
69
|
-
fail('The normalized messages pattern must retain {locale} exactly once.');
|
|
70
|
-
}
|
|
71
|
-
const resolved = path.resolve(root, normalized.replace('{locale}', '__locale__'));
|
|
72
|
-
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
73
|
-
fail('The messages pattern must stay inside the source root.');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return toPosix(normalized);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function listSourceFiles(root) {
|
|
80
|
-
const files = [];
|
|
81
|
-
let truncated = false;
|
|
82
|
-
|
|
83
|
-
function visit(directory) {
|
|
84
|
-
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
|
|
85
|
-
left.name.localeCompare(right.name)
|
|
86
|
-
)) {
|
|
87
|
-
if (files.length >= MAX_SOURCE_FILES) {
|
|
88
|
-
truncated = true;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const absolute = path.join(directory, entry.name);
|
|
92
|
-
if (entry.isSymbolicLink()) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (entry.isDirectory()) {
|
|
96
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
97
|
-
visit(absolute);
|
|
98
|
-
}
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
entry.isFile() &&
|
|
103
|
-
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
|
|
104
|
-
lstatSync(absolute).size <= MAX_SOURCE_BYTES
|
|
105
|
-
) {
|
|
106
|
-
files.push(absolute);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
visit(root);
|
|
112
|
-
return { files, truncated };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function readPackageEvidence(root) {
|
|
116
|
-
const packagePath = path.join(root, 'package.json');
|
|
117
|
-
if (!existsSync(packagePath)) {
|
|
118
|
-
return { declared: false, evidence: [] };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
let manifest;
|
|
122
|
-
try {
|
|
123
|
-
manifest = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
124
|
-
} catch {
|
|
125
|
-
return { declared: false, evidence: [] };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const sections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
129
|
-
for (const section of sections) {
|
|
130
|
-
if (typeof manifest[section]?.['next-intl'] === 'string') {
|
|
131
|
-
const source = readFileSync(packagePath, 'utf8');
|
|
132
|
-
const offset = source.indexOf('"next-intl"');
|
|
133
|
-
return {
|
|
134
|
-
declared: true,
|
|
135
|
-
evidence: [
|
|
136
|
-
{
|
|
137
|
-
claim: 'next-intl.declared',
|
|
138
|
-
path: 'package.json',
|
|
139
|
-
line: lineOf(source, Math.max(offset, 0)),
|
|
140
|
-
value: manifest[section]['next-intl'],
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { declared: false, evidence: [] };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function deriveMessagePatterns(files, root) {
|
|
151
|
-
const findings = [];
|
|
152
|
-
const importPattern = /import\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*\.json)`\s*\)/g;
|
|
153
|
-
|
|
154
|
-
for (const absolute of files) {
|
|
155
|
-
const source = readFileSync(absolute, 'utf8');
|
|
156
|
-
for (const match of source.matchAll(importPattern)) {
|
|
157
|
-
const template = match[1];
|
|
158
|
-
if ((template.match(/\$\{[^}]+\}/g) ?? []).length !== 1) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const relativeTemplate = template.replace(/\$\{[^}]+\}/, '{locale}');
|
|
162
|
-
const resolvedTemplate = path.resolve(path.dirname(absolute), relativeTemplate.replace('{locale}', '__locale__'));
|
|
163
|
-
if (resolvedTemplate !== root && !resolvedTemplate.startsWith(`${root}${path.sep}`)) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const rootRelative = toPosix(path.relative(root, resolvedTemplate)).replace('__locale__', '{locale}');
|
|
167
|
-
findings.push({
|
|
168
|
-
pattern: rootRelative,
|
|
169
|
-
source: 'request-config',
|
|
170
|
-
path: toPosix(path.relative(root, absolute)),
|
|
171
|
-
line: lineOf(source, match.index ?? 0),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return findings;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function parseQuotedValues(value) {
|
|
180
|
-
return [...value.matchAll(/(['"])(.*?)\1/g)].map((match) => match[2]);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function readStaticBindings(source) {
|
|
184
|
-
const arrays = new Map();
|
|
185
|
-
const strings = new Map();
|
|
186
|
-
|
|
187
|
-
for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\[([^\]]*)\](?:\s+as\s+const)?/g)) {
|
|
188
|
-
const values = parseQuotedValues(match[2]);
|
|
189
|
-
if (values.length > 0) {
|
|
190
|
-
arrays.set(match[1], values);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(['"])(.*?)\2/g)) {
|
|
194
|
-
strings.set(match[1], match[3]);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return { arrays, strings };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function readRoutingCandidates(files, root) {
|
|
201
|
-
const localeCandidates = [];
|
|
202
|
-
const defaultLocaleCandidates = [];
|
|
203
|
-
|
|
204
|
-
for (const absolute of files) {
|
|
205
|
-
const source = readFileSync(absolute, 'utf8');
|
|
206
|
-
if (!source.includes('defineRouting')) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const bindings = readStaticBindings(source);
|
|
210
|
-
|
|
211
|
-
for (const match of source.matchAll(/\blocales\s*:\s*(\[[^\]]*\]|[A-Za-z_$][\w$]*)/g)) {
|
|
212
|
-
const expression = match[1];
|
|
213
|
-
const locales = expression.startsWith('[') ? parseQuotedValues(expression) : bindings.arrays.get(expression);
|
|
214
|
-
if (locales?.length) {
|
|
215
|
-
localeCandidates.push({
|
|
216
|
-
locales: [...new Set(locales)].sort(),
|
|
217
|
-
path: toPosix(path.relative(root, absolute)),
|
|
218
|
-
line: lineOf(source, match.index ?? 0),
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for (const match of source.matchAll(/\bdefaultLocale\s*:\s*(?:(['"])(.*?)\1|([A-Za-z_$][\w$]*))/g)) {
|
|
224
|
-
const locale = match[2] ?? bindings.strings.get(match[3]);
|
|
225
|
-
if (locale) {
|
|
226
|
-
defaultLocaleCandidates.push({
|
|
227
|
-
locale,
|
|
228
|
-
path: toPosix(path.relative(root, absolute)),
|
|
229
|
-
line: lineOf(source, match.index ?? 0),
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return { localeCandidates, defaultLocaleCandidates };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function uniqueBy(items, keyOf) {
|
|
239
|
-
const seen = new Set();
|
|
240
|
-
return items.filter((item) => {
|
|
241
|
-
const key = keyOf(item);
|
|
242
|
-
if (seen.has(key)) {
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
seen.add(key);
|
|
246
|
-
return true;
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function selectConsistent(items, valueOf) {
|
|
251
|
-
const values = uniqueBy(items.map(valueOf), (value) => JSON.stringify(value));
|
|
252
|
-
return values.length === 1 ? values[0] : null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function inspectDictionaries(root, patterns) {
|
|
256
|
-
const dictionaries = [];
|
|
257
|
-
let truncated = false;
|
|
258
|
-
|
|
259
|
-
for (const patternEntry of patterns) {
|
|
260
|
-
const escaped = patternEntry.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('\\{locale\\}', '([^/]+)');
|
|
261
|
-
const matcher = new RegExp(`^${escaped}$`);
|
|
262
|
-
const prefix = patternEntry.pattern.slice(0, patternEntry.pattern.indexOf('{locale}'));
|
|
263
|
-
const scanRootRelative = prefix.includes('/') ? prefix.slice(0, prefix.lastIndexOf('/')) : '';
|
|
264
|
-
const scanRoot = path.resolve(root, scanRootRelative || '.');
|
|
265
|
-
if (!existsSync(scanRoot) || !lstatSync(scanRoot).isDirectory()) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const candidates = [];
|
|
270
|
-
function visit(directory) {
|
|
271
|
-
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
|
|
272
|
-
left.name.localeCompare(right.name)
|
|
273
|
-
)) {
|
|
274
|
-
if (candidates.length >= MAX_DICTIONARY_FILES) {
|
|
275
|
-
truncated = true;
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const absolute = path.join(directory, entry.name);
|
|
279
|
-
if (entry.isSymbolicLink()) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (entry.isDirectory()) {
|
|
283
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
284
|
-
visit(absolute);
|
|
285
|
-
}
|
|
286
|
-
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
287
|
-
candidates.push(absolute);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
visit(scanRoot);
|
|
292
|
-
|
|
293
|
-
for (const absolute of candidates) {
|
|
294
|
-
const relative = toPosix(path.relative(root, absolute));
|
|
295
|
-
const match = relative.match(matcher);
|
|
296
|
-
if (match) {
|
|
297
|
-
dictionaries.push({ locale: match[1], path: relative, pattern: patternEntry.pattern });
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
dictionaries: uniqueBy(dictionaries, (item) => `${item.locale}\0${item.path}\0${item.pattern}`).sort(
|
|
304
|
-
(left, right) => left.locale.localeCompare(right.locale) || left.path.localeCompare(right.path)
|
|
305
|
-
),
|
|
306
|
-
truncated,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const { sourceRoot: sourceRootArgument, messagesPattern: suppliedPattern } = parseArguments(process.argv.slice(2));
|
|
311
|
-
const sourceRoot = path.resolve(sourceRootArgument);
|
|
312
|
-
if (!existsSync(sourceRoot) || !lstatSync(sourceRoot).isDirectory()) {
|
|
313
|
-
fail(`Source root is not a directory: ${sourceRootArgument}`);
|
|
314
|
-
}
|
|
315
|
-
const canonicalRoot = realpathSync(sourceRoot);
|
|
316
|
-
|
|
317
|
-
const sourceInspection = listSourceFiles(canonicalRoot);
|
|
318
|
-
const sourceFiles = sourceInspection.files;
|
|
319
|
-
const packageFinding = readPackageEvidence(canonicalRoot);
|
|
320
|
-
const derivedPatterns = deriveMessagePatterns(sourceFiles, canonicalRoot);
|
|
321
|
-
const patterns = [];
|
|
322
|
-
|
|
323
|
-
if (suppliedPattern) {
|
|
324
|
-
patterns.push({ pattern: validatePattern(suppliedPattern, canonicalRoot), source: 'agent-supplied' });
|
|
325
|
-
} else {
|
|
326
|
-
for (const finding of derivedPatterns) {
|
|
327
|
-
patterns.push({ pattern: validatePattern(finding.pattern, canonicalRoot), source: finding.source });
|
|
328
|
-
}
|
|
329
|
-
if (existsSync(path.join(canonicalRoot, 'messages'))) {
|
|
330
|
-
patterns.push({ pattern: CONVENTIONAL_PATTERN, source: 'conventional' });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const uniquePatterns = uniqueBy(patterns, (item) => item.pattern).sort((left, right) =>
|
|
335
|
-
left.pattern.localeCompare(right.pattern)
|
|
336
|
-
);
|
|
337
|
-
const dictionaryInspection = inspectDictionaries(canonicalRoot, uniquePatterns);
|
|
338
|
-
const dictionaries = dictionaryInspection.dictionaries;
|
|
339
|
-
const routing = readRoutingCandidates(sourceFiles, canonicalRoot);
|
|
340
|
-
const configuredLocales = selectConsistent(routing.localeCandidates, (item) => item.locales);
|
|
341
|
-
const defaultLocale = selectConsistent(routing.defaultLocaleCandidates, (item) => item.locale);
|
|
342
|
-
const dictionaryLocales = [...new Set(dictionaries.map((item) => item.locale))].sort();
|
|
343
|
-
const unresolved = [];
|
|
344
|
-
|
|
345
|
-
if (sourceInspection.truncated) {
|
|
346
|
-
unresolved.push({ code: 'source-scan-truncated', claim: `Source scanning stopped after ${MAX_SOURCE_FILES} files.` });
|
|
347
|
-
}
|
|
348
|
-
if (!packageFinding.declared) {
|
|
349
|
-
unresolved.push({
|
|
350
|
-
code: 'next-intl-not-declared',
|
|
351
|
-
claim: 'Whether this project uses next-intl is not proven by package.json.',
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
if (uniquePatterns.length === 0) {
|
|
355
|
-
unresolved.push({
|
|
356
|
-
code: 'messages-pattern-not-found',
|
|
357
|
-
claim: 'The location of next-intl message dictionaries is not proven.',
|
|
358
|
-
});
|
|
359
|
-
} else if (dictionaries.length === 0) {
|
|
360
|
-
unresolved.push({
|
|
361
|
-
code: 'message-dictionaries-not-found',
|
|
362
|
-
claim: 'No JSON message dictionaries matched the selected pattern.',
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
if (dictionaryInspection.truncated) {
|
|
366
|
-
unresolved.push({
|
|
367
|
-
code: 'dictionary-scan-truncated',
|
|
368
|
-
claim: `Dictionary scanning stopped after ${MAX_DICTIONARY_FILES} JSON files.`,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
if (routing.localeCandidates.length === 0) {
|
|
372
|
-
unresolved.push({
|
|
373
|
-
code: 'configured-locales-not-found',
|
|
374
|
-
claim: 'The statically configured next-intl locales are not proven.',
|
|
375
|
-
});
|
|
376
|
-
} else if (configuredLocales === null) {
|
|
377
|
-
unresolved.push({
|
|
378
|
-
code: 'configured-locales-conflict',
|
|
379
|
-
claim: 'Static routing files declare different locale lists.',
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
if (routing.defaultLocaleCandidates.length === 0) {
|
|
383
|
-
unresolved.push({
|
|
384
|
-
code: 'default-locale-not-found',
|
|
385
|
-
claim: 'The statically configured next-intl default locale is not proven.',
|
|
386
|
-
});
|
|
387
|
-
} else if (defaultLocale === null) {
|
|
388
|
-
unresolved.push({
|
|
389
|
-
code: 'default-locale-conflict',
|
|
390
|
-
claim: 'Static routing files declare different default locales.',
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (configuredLocales) {
|
|
394
|
-
for (const locale of configuredLocales.filter((value) => !dictionaryLocales.includes(value))) {
|
|
395
|
-
unresolved.push({
|
|
396
|
-
code: 'configured-locale-without-dictionary',
|
|
397
|
-
claim: `Configured locale ${locale} has no matching message dictionary.`,
|
|
398
|
-
locale,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
for (const locale of dictionaryLocales.filter((value) => !configuredLocales.includes(value))) {
|
|
402
|
-
unresolved.push({
|
|
403
|
-
code: 'dictionary-locale-not-configured',
|
|
404
|
-
claim: `Message dictionary locale ${locale} is not in the static routing configuration.`,
|
|
405
|
-
locale,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (defaultLocale && configuredLocales && !configuredLocales.includes(defaultLocale)) {
|
|
410
|
-
unresolved.push({
|
|
411
|
-
code: 'default-locale-not-configured',
|
|
412
|
-
claim: `Default locale ${defaultLocale} is not in the static routing configuration.`,
|
|
413
|
-
locale: defaultLocale,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const evidence = [
|
|
418
|
-
...packageFinding.evidence,
|
|
419
|
-
...derivedPatterns
|
|
420
|
-
.filter((item) => uniquePatterns.some((patternEntry) => patternEntry.pattern === item.pattern))
|
|
421
|
-
.map((item) => ({ claim: 'messages.pattern', path: item.path, line: item.line, value: item.pattern })),
|
|
422
|
-
...dictionaries.map((item) => ({ claim: 'messages.dictionary', path: item.path, line: 1, value: item.locale })),
|
|
423
|
-
...routing.localeCandidates.map((item) => ({
|
|
424
|
-
claim: 'routing.locales',
|
|
425
|
-
path: item.path,
|
|
426
|
-
line: item.line,
|
|
427
|
-
value: item.locales,
|
|
428
|
-
})),
|
|
429
|
-
...routing.defaultLocaleCandidates.map((item) => ({
|
|
430
|
-
claim: 'routing.defaultLocale',
|
|
431
|
-
path: item.path,
|
|
432
|
-
line: item.line,
|
|
433
|
-
value: item.locale,
|
|
434
|
-
})),
|
|
435
|
-
].sort(
|
|
436
|
-
(left, right) =>
|
|
437
|
-
left.path.localeCompare(right.path) || left.line - right.line || left.claim.localeCompare(right.claim)
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const result = {
|
|
441
|
-
version: 1,
|
|
442
|
-
area: 'locales',
|
|
443
|
-
facts: {
|
|
444
|
-
system: 'next-intl',
|
|
445
|
-
declared: packageFinding.declared,
|
|
446
|
-
messagesPatterns: uniquePatterns,
|
|
447
|
-
dictionaryCandidates: dictionaries,
|
|
448
|
-
configuredLocaleCandidates: routing.localeCandidates,
|
|
449
|
-
configuredLocales,
|
|
450
|
-
defaultLocaleCandidates: routing.defaultLocaleCandidates,
|
|
451
|
-
defaultLocale,
|
|
452
|
-
},
|
|
453
|
-
evidence,
|
|
454
|
-
unresolved: unresolved.sort(
|
|
455
|
-
(left, right) => left.code.localeCompare(right.code) || (left.locale ?? '').localeCompare(right.locale ?? '')
|
|
456
|
-
),
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { FimoProvider } from 'fimo/next/client';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
import './globals.css';
|
|
5
|
-
|
|
6
|
-
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
|
7
|
-
const previewScriptUrl = process.env.NEXT_PUBLIC_FIMO_PREVIEW_SCRIPT_URL;
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<html lang="en">
|
|
11
|
-
<body>
|
|
12
|
-
<FimoProvider>{children}</FimoProvider>
|
|
13
|
-
{previewScriptUrl ? <script src={previewScriptUrl} defer /> : null}
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|