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.
Files changed (164) hide show
  1. package/assets/astro/components/DateTime.astro +22 -0
  2. package/assets/astro/components/index.d.ts +1 -0
  3. package/assets/astro/components/index.js +1 -0
  4. package/assets/content-templates/shared/types.eta +2 -0
  5. package/assets/skills/fimo-cli/SKILL.md +8 -6
  6. package/assets/skills/fimo-cli/references/branches.md +2 -2
  7. package/assets/skills/fimo-cli/references/content.md +2 -2
  8. package/assets/skills/fimo-cli/references/deploy.md +17 -9
  9. package/assets/skills/fimo-cli/references/hosting.md +114 -0
  10. package/assets/skills/fimo-cli/references/init.md +13 -6
  11. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
  12. package/assets/skills/fimo-cli/references/locales.md +38 -14
  13. package/assets/skills/fimo-cli/references/pages.md +70 -0
  14. package/assets/skills/fimo-cli/references/preview.md +26 -20
  15. package/assets/skills/fimo-cli/references/webhooks.md +1 -1
  16. package/assets/skills/fimo-code/SKILL.md +11 -0
  17. package/assets/skills/fimo-code/assets/react-router-locales/LocaleLayout.tsx +12 -0
  18. package/assets/skills/fimo-code/assets/react-router-locales/routes.ts +7 -6
  19. package/assets/skills/fimo-code/references/best-practices/astro.md +47 -0
  20. package/assets/skills/fimo-code/references/best-practices/next.md +42 -0
  21. package/assets/skills/fimo-code/references/best-practices/sveltekit.md +17 -0
  22. package/assets/skills/fimo-code/references/content/richtext.md +5 -0
  23. package/assets/skills/fimo-code/references/content/schema.md +4 -1
  24. package/assets/skills/fimo-code/references/frameworks/astro.md +5 -3
  25. package/assets/skills/fimo-code/references/frameworks/hosting.md +5 -2
  26. package/assets/skills/fimo-code/references/frameworks/next/app-router.md +6 -0
  27. package/assets/skills/fimo-code/references/frameworks/next/locales.md +12 -2
  28. package/assets/skills/fimo-code/references/frameworks/next.md +7 -0
  29. package/assets/skills/fimo-code/references/frameworks/react-router.md +15 -7
  30. package/assets/skills/fimo-code/references/frameworks/sveltekit.md +15 -8
  31. package/assets/skills/fimo-code/references/labels.md +9 -1
  32. package/assets/skills/fimo-code/references/locales.md +8 -2
  33. package/assets/skills/fimo-code/references/migrations/react-router-route-ownership.md +93 -0
  34. package/assets/skills/fimo-code/references/ui.md +21 -2
  35. package/assets/skills/fimo-migration/SKILL.md +276 -51
  36. package/assets/skills/fimo-migration/references/content.md +119 -35
  37. package/assets/skills/fimo-migration/references/decision.md +72 -0
  38. package/assets/skills/fimo-migration/references/forms.md +38 -6
  39. package/assets/skills/fimo-migration/references/frameworks/nextjs.md +179 -23
  40. package/assets/skills/fimo-migration/references/import-contract.md +71 -19
  41. package/assets/skills/fimo-migration/references/manifest.md +11 -161
  42. package/assets/skills/fimo-migration/references/media.md +33 -1
  43. package/assets/skills/fimo-migration/references/platforms.md +26 -2
  44. package/assets/skills/fimo-migration/references/qa.md +167 -13
  45. package/assets/skills/fimo-migration/references/workers/content-execution.md +39 -0
  46. package/assets/skills/fimo-migration/references/workers/content-planner.md +64 -0
  47. package/assets/skills/fimo-migration/references/workers/framework.md +37 -0
  48. package/assets/skills/fimo-migration/references/workers/validator.md +25 -0
  49. package/assets/skills/fimo-migration/references/workers.md +82 -0
  50. package/assets/skills/fimo-migration/references/workflow.md +140 -192
  51. package/assets/skills/fimo-migration/scripts/apply-decision.mjs +534 -0
  52. package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +7 -4
  53. package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +191 -4
  54. package/assets/skills/fimo-studio/references/ai-chat.md +1 -1
  55. package/assets/skills/fimo-studio/references/cms.md +25 -7
  56. package/assets/skills/fimo-studio/references/webhooks.md +1 -1
  57. package/assets/sveltekit/components/DateTime.svelte +35 -0
  58. package/assets/sveltekit/components/FimoProvider.svelte +2 -2
  59. package/assets/sveltekit/components/index.d.ts +1 -0
  60. package/assets/sveltekit/components/index.js +1 -0
  61. package/dist/build/next/config.d.ts +4 -1
  62. package/dist/build/next/config.js +21 -5
  63. package/dist/build/sveltekit/vite.d.ts +3 -4
  64. package/dist/build/sveltekit/vite.js +3 -4
  65. package/dist/build/vite/plugins/runtime-env.d.ts +2 -1
  66. package/dist/build/vite/plugins/runtime-env.js +14 -10
  67. package/dist/cli/bundle.json +2 -2
  68. package/dist/cli/index.js +113 -108
  69. package/dist/runtime/app/FimoProviders.js +25 -14
  70. package/dist/runtime/app/prefetch.d.ts +3 -1
  71. package/dist/runtime/app/prefetch.js +11 -55
  72. package/dist/runtime/astro/preview.js +7 -9
  73. package/dist/runtime/content/client/internal.js +1 -0
  74. package/dist/runtime/i18n/locale.d.ts +6 -3
  75. package/dist/runtime/i18n/locale.js +16 -41
  76. package/dist/runtime/next/client-runtime.js +5 -1
  77. package/dist/runtime/next/client.d.ts +3 -2
  78. package/dist/runtime/next/client.js +2 -1
  79. package/dist/runtime/next/labels.d.ts +2 -2
  80. package/dist/runtime/next/labels.js +2 -2
  81. package/dist/runtime/next/preview.d.ts +13 -0
  82. package/dist/runtime/next/preview.js +34 -0
  83. package/dist/runtime/next/primitives/server.d.ts +2 -0
  84. package/dist/runtime/next/primitives/server.js +8 -0
  85. package/dist/runtime/next/provider.d.ts +5 -0
  86. package/dist/runtime/next/provider.js +17 -3
  87. package/dist/runtime/next/server-runtime.js +4 -1
  88. package/dist/runtime/next/server.d.ts +1 -1
  89. package/dist/runtime/next/server.js +1 -1
  90. package/dist/runtime/paths/get-fimo-paths.d.ts +6 -5
  91. package/dist/runtime/paths/get-fimo-paths.js +43 -42
  92. package/dist/runtime/preview/location.d.ts +19 -2
  93. package/dist/runtime/preview/location.js +16 -2
  94. package/dist/runtime/primitives/index.d.ts +1 -0
  95. package/dist/runtime/primitives/index.js +1 -0
  96. package/dist/runtime/react/primitives/editable/DateTime.d.ts +4 -0
  97. package/dist/runtime/react/primitives/editable/DateTime.js +13 -0
  98. package/dist/runtime/react/primitives/renderers/DateRenderer.d.ts +7 -1
  99. package/dist/runtime/react/primitives/renderers/DateRenderer.js +11 -8
  100. package/dist/runtime/react-router/{routes.d.ts → legacy/routes.d.ts} +6 -1
  101. package/dist/runtime/react-router/{routes.js → legacy/routes.js} +22 -7
  102. package/dist/runtime/routes/index.d.ts +162 -0
  103. package/dist/runtime/routes/index.js +335 -0
  104. package/dist/runtime/routing/AppRoutesProvider.js +4 -3
  105. package/dist/runtime/seo/resolver.d.ts +5 -0
  106. package/dist/runtime/seo/resolver.js +10 -42
  107. package/dist/runtime/shared/fimo-config.types.d.ts +14 -13
  108. package/dist/runtime/shared/preview-location.d.ts +1 -22
  109. package/dist/runtime/shared/preview-location.js +5 -93
  110. package/dist/runtime/sveltekit/preview.d.ts +12 -3
  111. package/dist/runtime/sveltekit/preview.js +13 -8
  112. package/dist/runtime/templates.d.ts +19 -0
  113. package/dist/runtime/templates.js +33 -0
  114. package/dist/scripts/sync-forms.js +1 -1
  115. package/dist/scripts/sync-schemas.js +1 -4
  116. package/dist/scripts/validate-fimo-config.js +5 -1
  117. package/dist/scripts/validate-page-registry.d.ts +2 -0
  118. package/dist/scripts/validate-page-registry.js +167 -0
  119. package/dist/scripts/validate-route-metadata.js +132 -41
  120. package/package.json +10 -5
  121. package/templates/astro/.fimo/config.json +13 -5
  122. package/templates/astro/AGENTS.md +5 -1
  123. package/templates/astro/astro.config.mjs +7 -0
  124. package/templates/astro/pnpm-workspace.yaml +0 -3
  125. package/templates/astro/src/components/ProjectOverview.astro +9 -6
  126. package/templates/astro/src/layouts/Base.astro +1 -1
  127. package/templates/astro/src/locales.ts +4 -0
  128. package/templates/astro/src/pages/[locale]/index.astro +19 -0
  129. package/templates/astro/src/pages/[locale]/projects/[slug].astro +35 -0
  130. package/templates/astro/src/pages/[locale]/projects/index.astro +19 -0
  131. package/templates/astro/src/pages/index.astro +5 -2
  132. package/templates/astro/src/pages/projects/[slug].astro +4 -2
  133. package/templates/astro/src/pages/projects/index.astro +5 -2
  134. package/templates/nextjs/.fimo/config.json +2 -1
  135. package/templates/nextjs/AGENTS.md +5 -0
  136. package/templates/nextjs/src/app/(default)/layout.tsx +10 -0
  137. package/templates/nextjs/src/app/[locale]/layout.tsx +22 -0
  138. package/templates/nextjs/src/app/[locale]/page.tsx +1 -0
  139. package/templates/nextjs/src/components/Document.tsx +26 -0
  140. package/templates/nextjs/src/components/FimoPreviewBridge.tsx +13 -0
  141. package/templates/nextjs/src/locales.ts +13 -0
  142. package/templates/react-router/.fimo/config.json +3 -6
  143. package/templates/react-router/AGENTS.md +4 -0
  144. package/templates/react-router/package.json +1 -1
  145. package/templates/react-router/pnpm-workspace.yaml +1 -3
  146. package/templates/react-router/react-router.config.ts +4 -10
  147. package/templates/react-router/src/pages/LocaleLayout.tsx +12 -0
  148. package/templates/react-router/src/root.tsx +2 -4
  149. package/templates/react-router/src/routes.ts +5 -3
  150. package/templates/sveltekit/.fimo/config.json +2 -1
  151. package/templates/sveltekit/AGENTS.md +5 -0
  152. package/templates/sveltekit/src/app.html +1 -1
  153. package/templates/sveltekit/src/hooks.server.ts +11 -0
  154. package/templates/sveltekit/src/locales.ts +8 -0
  155. package/templates/sveltekit/src/params/locale.ts +5 -0
  156. package/templates/sveltekit/src/routes/+layout.server.ts +6 -1
  157. package/templates/sveltekit/src/routes/+layout.svelte +5 -0
  158. package/assets/skills/fimo-migration/references/inventory/coordinator.md +0 -40
  159. package/assets/skills/fimo-migration/references/inventory/locales/next-intl.md +0 -39
  160. package/assets/skills/fimo-migration/references/inventory/locales.md +0 -41
  161. package/assets/skills/fimo-migration/scripts/inventory/locales/next-intl.mjs +0 -459
  162. package/templates/nextjs/src/app/layout.tsx +0 -17
  163. /package/templates/nextjs/src/app/{page.tsx → (default)/page.tsx} +0 -0
  164. /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
- labels: await getLabels({ event }),
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
- }