fimo 0.2.4 → 0.2.5-experimental.1782327181771

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/assets/agent-templates/content-translator/GOAL.md +8 -11
  3. package/assets/agent-templates/content-translator/capabilities.yaml +1 -3
  4. package/assets/agent-templates/content-translator/scripts/translate-entries.ts +10 -7
  5. package/assets/content-templates/hooks-template.eta +73 -23
  6. package/assets/skills/fimo/SKILL.md +3 -3
  7. package/assets/skills/fimo/references/forms.md +1 -1
  8. package/assets/skills/fimo/references/setup-plain-vite.md +1 -1
  9. package/assets/skills/fimo/references/setup-react-router.md +1 -1
  10. package/assets/skills/fimo/references/translations.md +42 -14
  11. package/assets/skills/fimo/references/ui.md +4 -4
  12. package/assets/skills/fimo-cli/SKILL.md +3 -3
  13. package/assets/skills/fimo-cli/references/content.md +1 -1
  14. package/assets/skills/fimo-cli/references/forms.md +1 -1
  15. package/assets/skills/fimo-cli/references/translations.md +45 -19
  16. package/dist/build/vite/plugins/fimo-config.d.ts.map +1 -1
  17. package/dist/build/vite/plugins/fimo-config.js +16 -0
  18. package/dist/build/vite/plugins/fimo-config.test.d.ts +2 -0
  19. package/dist/build/vite/plugins/fimo-config.test.d.ts.map +1 -0
  20. package/dist/build/vite/plugins/fimo-config.test.js +46 -0
  21. package/dist/build/vite/plugins/translations.d.ts +7 -6
  22. package/dist/build/vite/plugins/translations.d.ts.map +1 -1
  23. package/dist/build/vite/plugins/translations.js +366 -33
  24. package/dist/build/vite/plugins/translations.test.d.ts +2 -0
  25. package/dist/build/vite/plugins/translations.test.d.ts.map +1 -0
  26. package/dist/build/vite/plugins/translations.test.js +177 -0
  27. package/dist/cli/bundle.json +2 -2
  28. package/dist/cli/index.js +1305 -1078
  29. package/dist/runtime/app/FimoScripts.d.ts.map +1 -1
  30. package/dist/runtime/app/FimoScripts.js +35 -1
  31. package/dist/runtime/app/prefetch.d.ts.map +1 -1
  32. package/dist/runtime/app/prefetch.js +6 -1
  33. package/dist/runtime/paths/get-fimo-paths.d.ts.map +1 -1
  34. package/dist/runtime/paths/get-fimo-paths.js +9 -4
  35. package/dist/runtime/primitives/components/Text.d.ts +1 -1
  36. package/dist/runtime/primitives/components/Text.js +1 -1
  37. package/dist/runtime/primitives/lib/query.d.ts +5 -0
  38. package/dist/runtime/primitives/lib/query.d.ts.map +1 -1
  39. package/dist/runtime/primitives/lib/template.d.ts +1 -1
  40. package/dist/runtime/primitives/lib/template.js +1 -1
  41. package/dist/runtime/primitives/translations.d.ts +9 -3
  42. package/dist/runtime/primitives/translations.d.ts.map +1 -1
  43. package/dist/runtime/primitives/translations.js +32 -5
  44. package/dist/runtime/seo/htmlProps.d.ts +1 -1
  45. package/dist/runtime/seo/htmlProps.js +2 -2
  46. package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
  47. package/dist/runtime/shared/fimo-config.server.js +1 -0
  48. package/dist/runtime/shared/fimo-config.types.d.ts +7 -0
  49. package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
  50. package/dist/scripts/extract-translations.d.ts +8 -8
  51. package/dist/scripts/extract-translations.js +22 -57
  52. package/dist/scripts/lint-translation-keys.js +24 -5
  53. package/dist/scripts/lint-translation-keys.test.d.ts +1 -0
  54. package/dist/scripts/lint-translation-keys.test.js +16 -0
  55. package/package.json +1 -1
  56. package/release.json +2 -2
  57. package/templates/react-router/fimo-config.json +4 -0
  58. package/templates/react-router/package.json +1 -1
  59. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +0 -66
  60. package/dist/scripts/inject-translations.d.ts +0 -6
  61. package/dist/scripts/inject-translations.js +0 -168
  62. package/templates/react-router/translations/en.json +0 -1
@@ -1,43 +1,69 @@
1
- # Translations workflow
1
+ # Labels workflow
2
2
 
3
- > For the `t()` helper signature, `useTranslations()` hook, key naming rules, and the "wrap every user-facing string" rule these are package surface and code conventions. See **`fimo/references/translations.md`**.
3
+ > For the `t()` helper signature, `useTranslations()` hook, key naming rules, and the "wrap every user-facing string" rule, see **`fimo/references/translations.md`**.
4
4
 
5
- This file covers the **CLI workflow** for translations.
5
+ This file covers the **CLI workflow** for labels: static UI copy such as nav items, buttons, headings, placeholders, alt text, and SEO strings.
6
6
 
7
- ## What sync does
7
+ ## What validate does
8
8
 
9
- The pre-build step scans `src/**/*.{ts,tsx,js,jsx}` for `t('literal-key', 'literal-default')` calls (skipping `src/ui` and `node_modules`), writes them to `translations/en.json`, and syncs them to the tenant DB. This makes strings editable inline by non-technical users in the Fimo admin and translatable to other locales.
9
+ `fimo validate` scans `src/**/*.{ts,tsx,js,jsx}` for `t('literal-key')` calls (skipping `src/ui` and `node_modules`) and verifies every key has a value in the tenant DB for the default locale.
10
10
 
11
- Code is the source of truth for English defaults.
11
+ The database is the source of truth for visible label values. Code declares keys; it does not create visible copy.
12
12
 
13
13
  ## CLI commands
14
14
 
15
15
  ```bash
16
- fimo validate # run all checks + sync local state to DB (includes translations)
17
- fimo translations list # inspect what's currently in the DB (read-only)
18
- fimo translations list --locale en # filter by locale
16
+ fimo validate # run all checks; fails if label keys are missing in DB
17
+ fimo labels list # inspect labels currently in the DB
18
+ fimo labels list --locale en # filter by locale
19
+ fimo labels set nav.home --value Home # create/update one label
20
+ fimo labels set-many --locale es --data '{"nav.home":"Inicio"}'
21
+ fimo labels set-many --locale es --file /tmp/es-labels.json
22
+ fimo labels delete nav.home --locale en
19
23
  ```
20
24
 
21
- `fimo validate` is the single entrypoint that extracts `t()` calls, rewrites `translations/en.json`, and pushes the diff to the DB. It's idempotent — run it as often as you like. It also runs automatically inside `fimo deploy`: `fimo validate` runs first (lint + codegen + DB sync), then any `translations/en.json` changes are committed and pushed alongside the rest of your code.
25
+ There is no `fimo translations` command. Use `fimo labels`.
22
26
 
23
- Advanced/internal primitives (hidden from `fimo --help`, used for debugging or by the backend sandbox):
27
+ ## Locale config
28
+
29
+ `fimo-config.json` controls label/content locales:
30
+
31
+ ```json
32
+ {
33
+ "i18n": {
34
+ "defaultLocale": "en",
35
+ "locales": ["en", "es"],
36
+ "autoTranslateLabels": true,
37
+ "autoTranslateContent": true
38
+ }
39
+ }
40
+ ```
41
+
42
+ - `defaultLocale` is what `fimo validate` and label commands use when `--locale` is omitted.
43
+ - `locales` is the enabled locale list and must include `defaultLocale`.
44
+ - `autoTranslateLabels` defaults to enabled when omitted. Set it to `false` to stop default-locale label writes from enqueueing machine translations.
45
+ - `autoTranslateContent` does the same for CMS/schema entries.
46
+ - For a non-default locale, pass `--locale <locale>` explicitly.
47
+
48
+ Advanced/internal primitive (hidden from `fimo --help`):
24
49
 
25
50
  ```bash
26
- fimo scripts extract-translations # rewrite translations/en.json locally no DB push
27
- fimo scripts inject-translations # rewrite t() defaults in src/ to match translations/en.json
51
+ fimo scripts extract-translations # legacy helper: scan src/ for literal t() keys
28
52
  ```
29
53
 
30
- Prefer `fimo validate` — these primitives are for narrow use cases.
54
+ Prefer `fimo validate` — this primitive is for narrow debugging/test use.
31
55
 
32
56
  ## When to run validate
33
57
 
34
- - After adding or changing any `t()` calls in code run `fimo validate` (or rely on `fimo deploy` doing it for you).
35
- - After renaming/removing `t()` keys the sync phase removes stale entries from the DB.
58
+ - After adding or changing any `t()` keys in code, run `fimo validate`.
59
+ - If validate reports `labels/missing`, add values with `fimo labels set` or `fimo labels set-many`, then run validate again.
60
+ - After renaming/removing keys, clean old DB labels intentionally with `fimo labels delete` when they are no longer used.
36
61
 
37
62
  ## Workflow
38
63
 
39
- 1. Write JSX with `t('key', 'Default')` calls. Load `fimo/references/translations.md` for the rules (literal keys, naming, what NOT to translate).
40
- 2. Run `fimo validate` to push new keys to the DB.
41
- 3. Editors can now change copy in the Fimo admin without code changes.
64
+ 1. Write JSX with `t('key')` calls. Load `fimo/references/translations.md` for the rules.
65
+ 2. Run `fimo validate`.
66
+ 3. Add missing values with `fimo labels set` or `fimo labels set-many`.
67
+ 4. Editors can now change copy in the Fimo admin without code changes.
42
68
 
43
69
  For the helper signatures, hooks, and code patterns, **always** load `fimo/references/translations.md` before writing code that uses `t()`.
@@ -1 +1 @@
1
- {"version":3,"file":"fimo-config.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/fimo-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAKnC;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,IAAI,MAAM,CAsCjD"}
1
+ {"version":3,"file":"fimo-config.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/fimo-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAKnC;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,IAAI,MAAM,CA6CjD"}
@@ -38,6 +38,13 @@ export default function fimoConfigPlugin() {
38
38
  return 'export default {};';
39
39
  }
40
40
  },
41
+ transformIndexHtml(html) {
42
+ const lang = readHtmlLang(configPath);
43
+ if (/<html\b[^>]*\blang=/.test(html)) {
44
+ return html.replace(/(<html\b[^>]*\blang=["'])[^"']*(["'])/i, `$1${lang}$2`);
45
+ }
46
+ return html.replace(/<html\b([^>]*)>/i, `<html$1 lang="${lang}">`);
47
+ },
41
48
  handleHotUpdate(ctx) {
42
49
  if (ctx.file !== configPath) {
43
50
  return;
@@ -50,3 +57,12 @@ export default function fimoConfigPlugin() {
50
57
  },
51
58
  };
52
59
  }
60
+ function readHtmlLang(configPath) {
61
+ try {
62
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
63
+ return (raw.seo?.locale ?? raw.i18n?.defaultLocale ?? 'en').replace('_', '-');
64
+ }
65
+ catch {
66
+ return 'en';
67
+ }
68
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fimo-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fimo-config.test.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/fimo-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import fimoConfigPlugin from './fimo-config';
6
+ let scratch = null;
7
+ const pluginContext = {
8
+ meta: {},
9
+ error: (error) => {
10
+ throw error;
11
+ },
12
+ info: () => undefined,
13
+ warn: () => undefined,
14
+ debug: () => undefined,
15
+ };
16
+ afterEach(() => {
17
+ if (scratch) {
18
+ rmSync(scratch, { recursive: true, force: true });
19
+ scratch = null;
20
+ }
21
+ });
22
+ function createPluginWithConfig(config) {
23
+ scratch = mkdtempSync(join(tmpdir(), 'fimo-config-plugin-'));
24
+ writeFileSync(join(scratch, 'fimo-config.json'), JSON.stringify(config, null, 2));
25
+ const plugin = fimoConfigPlugin();
26
+ if (typeof plugin.config === 'function') {
27
+ plugin.config.call(pluginContext, { root: scratch }, { command: 'build', mode: 'production' });
28
+ }
29
+ return plugin;
30
+ }
31
+ function transformHtml(plugin, html) {
32
+ if (typeof plugin.transformIndexHtml !== 'function') {
33
+ throw new Error('Expected transformIndexHtml function');
34
+ }
35
+ return plugin.transformIndexHtml.call(pluginContext, html, {});
36
+ }
37
+ describe('fimoConfigPlugin', () => {
38
+ it('sets html lang from seo.locale first', () => {
39
+ const plugin = createPluginWithConfig({ seo: { locale: 'es_ES' }, i18n: { defaultLocale: 'fr' } });
40
+ expect(transformHtml(plugin, '<html><head></head><body></body></html>')).toContain('<html lang="es-ES">');
41
+ });
42
+ it('falls back to i18n.defaultLocale when seo.locale is absent', () => {
43
+ const plugin = createPluginWithConfig({ i18n: { defaultLocale: 'fr' } });
44
+ expect(transformHtml(plugin, '<html lang="en"><head></head><body></body></html>')).toContain('<html lang="fr">');
45
+ });
46
+ });
@@ -1,9 +1,10 @@
1
1
  import type { Plugin } from 'vite';
2
- /**
3
- * Provides the `virtual:translations` module — `useTranslations()` reads
4
- * its data from this. Source: `<projectRoot>/translations/en.json`.
5
- *
6
- * Edits to that JSON trigger HMR on consumers via `handleHotUpdate`.
7
- */
2
+ interface LabelBundle {
3
+ labels: Record<string, string>;
4
+ locale: string;
5
+ updatedAt: string | null;
6
+ }
8
7
  export default function translationsPlugin(): Plugin;
8
+ export declare function loadLabelBundle(rootDir: string): Promise<LabelBundle>;
9
+ export {};
9
10
  //# sourceMappingURL=translations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/translations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,IAAI,MAAM,CA0DnD"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/translations.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAOlD,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAuBD,MAAM,CAAC,OAAO,UAAU,kBAAkB,IAAI,MAAM,CAmMnD;AA8BD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAkC3E"}
@@ -1,22 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { Buffer } from 'node:buffer';
1
3
  import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
2
5
  import { join } from 'node:path';
3
- /**
4
- * Provides the `virtual:translations` module — `useTranslations()` reads
5
- * its data from this. Source: `<projectRoot>/translations/en.json`.
6
- *
7
- * Edits to that JSON trigger HMR on consumers via `handleHotUpdate`.
8
- */
6
+ import { getConfigServer } from '../../../runtime/shared/fimo-config.server.js';
7
+ const VIRTUAL_ID = 'virtual:translations';
8
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
9
9
  export default function translationsPlugin() {
10
- const VIRTUAL_ID = 'virtual:translations';
11
- const RESOLVED_ID = '\0' + VIRTUAL_ID;
12
10
  let rootDir = process.cwd();
13
- let translationsPath;
11
+ let bundle = { labels: {}, locale: 'en', updatedAt: null };
12
+ let command = 'build';
13
+ let devServer = null;
14
+ let eventSockets = [];
15
+ let reconnectTimer;
16
+ let refreshTimer;
17
+ let pollTimer;
18
+ const refreshLabels = async () => {
19
+ bundle = await loadLabelBundle(rootDir);
20
+ return bundle;
21
+ };
22
+ const refreshLabelsAndInvalidateIfChanged = async () => {
23
+ const before = serializeBundle(bundle);
24
+ const next = await refreshLabels();
25
+ if (serializeBundle(next) !== before) {
26
+ invalidateLabels();
27
+ }
28
+ };
29
+ const invalidateLabels = () => {
30
+ if (!devServer) {
31
+ return;
32
+ }
33
+ for (const mod of findVirtualTranslationModules(devServer)) {
34
+ devServer.moduleGraph.invalidateModule(mod);
35
+ }
36
+ devServer.ws.send({ type: 'full-reload', path: '*' });
37
+ };
38
+ const scheduleRefresh = () => {
39
+ if (refreshTimer) {
40
+ clearTimeout(refreshTimer);
41
+ }
42
+ refreshTimer = setTimeout(() => {
43
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
44
+ }, 150);
45
+ };
14
46
  return {
15
47
  name: 'fimo-virtual-translations',
16
48
  enforce: 'pre',
17
49
  configResolved(config) {
18
50
  rootDir = config.root;
19
- translationsPath = join(rootDir, 'translations', 'en.json');
51
+ command = config.command;
52
+ },
53
+ async buildStart() {
54
+ bundle = emptyLabelBundle(rootDir);
55
+ this.addWatchFile(join(rootDir, 'fimo-config.json'));
56
+ this.addWatchFile(join(rootDir, '.env'));
57
+ this.addWatchFile(join(rootDir, '.env.local'));
58
+ if (command === 'build') {
59
+ await refreshLabels();
60
+ }
61
+ },
62
+ configureServer(server) {
63
+ devServer = server;
64
+ server.watcher.add([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
65
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
66
+ connectLabelEvents(rootDir, scheduleRefresh)
67
+ .then((sockets) => {
68
+ eventSockets = sockets;
69
+ })
70
+ .catch(() => undefined);
71
+ pollTimer = setInterval(() => {
72
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
73
+ }, 2000);
20
74
  },
21
75
  resolveId(id) {
22
76
  if (id === VIRTUAL_ID) {
@@ -28,34 +82,313 @@ export default function translationsPlugin() {
28
82
  if (id !== RESOLVED_ID) {
29
83
  return null;
30
84
  }
31
- // For now, only English. Read from project translations/en.json
32
- const filePath = translationsPath || join(rootDir, 'translations', 'en.json');
33
- // Watch the JSON file so edits trigger HMR
34
- this.addWatchFile(filePath);
35
- let json = '{}';
36
- try {
37
- json = await readFile(filePath, 'utf8');
38
- if (!json || json.trim() === '') {
39
- json = '{}';
40
- }
85
+ const current = command === 'serve' ? bundle : await refreshLabels();
86
+ const labels = JSON.stringify(current.labels);
87
+ return [
88
+ `export const translations = ${labels};`,
89
+ `export const labels = translations;`,
90
+ `export const locale = ${JSON.stringify(current.locale)};`,
91
+ `export const updatedAt = ${JSON.stringify(current.updatedAt)};`,
92
+ `export const signature = ${JSON.stringify(labelSignature(current.labels))};`,
93
+ ].join('\n');
94
+ },
95
+ async handleHotUpdate(ctx) {
96
+ const watched = new Set([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
97
+ if (!watched.has(ctx.file)) {
98
+ return;
41
99
  }
42
- catch {
43
- json = '{}';
100
+ await refreshLabels();
101
+ const modules = findVirtualTranslationModules(ctx.server);
102
+ if (modules.length > 0) {
103
+ for (const mod of modules) {
104
+ ctx.server.moduleGraph.invalidateModule(mod);
105
+ }
106
+ return modules;
44
107
  }
45
- // Expose as a flat map
46
- return `export const translations = ${json};`;
108
+ return [];
47
109
  },
48
- handleHotUpdate(ctx) {
49
- // Invalidate the virtual module when the JSON changes
50
- const watched = translationsPath || join(rootDir, 'translations', 'en.json');
51
- if (ctx.file !== watched) {
52
- return;
110
+ closeBundle() {
111
+ for (const socket of eventSockets) {
112
+ socket.close();
113
+ }
114
+ eventSockets = [];
115
+ if (reconnectTimer) {
116
+ clearTimeout(reconnectTimer);
117
+ }
118
+ if (refreshTimer) {
119
+ clearTimeout(refreshTimer);
53
120
  }
54
- const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);
55
- if (mod) {
56
- ctx.server.moduleGraph.invalidateModule(mod);
57
- return [mod];
121
+ if (pollTimer) {
122
+ clearInterval(pollTimer);
58
123
  }
59
124
  },
60
125
  };
126
+ async function connectLabelEvents(root, onLabelsChanged) {
127
+ const env = await readProjectEnv(root);
128
+ const apiUrl = env.VITE_API_URL;
129
+ if (!apiUrl) {
130
+ return [];
131
+ }
132
+ const runtimeConfig = await loadRuntimeConfig(apiUrl);
133
+ const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
134
+ if (!eventsUrl) {
135
+ return [];
136
+ }
137
+ const settings = await readProjectSettings(root);
138
+ if (!settings.projectId) {
139
+ return [];
140
+ }
141
+ const WebSocketCtor = globalThis.WebSocket;
142
+ if (!WebSocketCtor) {
143
+ return [];
144
+ }
145
+ const sockets = [];
146
+ for (const room of deriveEventsRooms(settings.projectId, apiUrl)) {
147
+ const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
148
+ const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
149
+ wsUrl.searchParams.set('authToken', token);
150
+ const socket = new WebSocketCtor(wsUrl.toString());
151
+ socket.onmessage = (event) => {
152
+ const raw = typeof event.data === 'string' ? event.data : '';
153
+ if (!raw) {
154
+ return;
155
+ }
156
+ try {
157
+ const message = JSON.parse(raw);
158
+ if (message.type !== 'labels:changed') {
159
+ return;
160
+ }
161
+ const changedLocales = message.payload?.changes?.map((change) => change.locale).filter(Boolean) ?? [];
162
+ if (changedLocales.length === 0 || changedLocales.includes(bundle.locale)) {
163
+ onLabelsChanged();
164
+ }
165
+ }
166
+ catch {
167
+ return;
168
+ }
169
+ };
170
+ socket.onclose = () => {
171
+ reconnectTimer = setTimeout(() => {
172
+ void connectLabelEvents(root, onLabelsChanged)
173
+ .then((nextSockets) => {
174
+ eventSockets = nextSockets;
175
+ })
176
+ .catch(() => undefined);
177
+ }, 2000);
178
+ };
179
+ socket.onerror = () => undefined;
180
+ sockets.push(socket);
181
+ }
182
+ return sockets;
183
+ }
184
+ }
185
+ function serializeBundle(bundle) {
186
+ return JSON.stringify({ labels: bundle.labels, locale: bundle.locale });
187
+ }
188
+ function emptyLabelBundle(rootDir) {
189
+ const config = getConfigServer(rootDir);
190
+ return {
191
+ labels: {},
192
+ locale: config.i18n?.defaultLocale ?? 'en',
193
+ updatedAt: null,
194
+ };
195
+ }
196
+ function findVirtualTranslationModules(server) {
197
+ const direct = server.moduleGraph.getModuleById(RESOLVED_ID);
198
+ const matches = new Set(direct ? [direct] : []);
199
+ for (const mod of server.moduleGraph.idToModuleMap.values()) {
200
+ if (mod.id?.includes('virtual:translations')) {
201
+ matches.add(mod);
202
+ }
203
+ }
204
+ return [...matches];
205
+ }
206
+ function labelSignature(labels) {
207
+ return Buffer.from(JSON.stringify(labels)).toString('base64');
208
+ }
209
+ export async function loadLabelBundle(rootDir) {
210
+ const config = getConfigServer(rootDir);
211
+ const locale = config.i18n?.defaultLocale ?? 'en';
212
+ const env = await readProjectEnv(rootDir);
213
+ const apiUrl = env.VITE_API_URL;
214
+ if (!apiUrl) {
215
+ return { labels: {}, locale, updatedAt: null };
216
+ }
217
+ try {
218
+ const url = new URL('/labels', apiUrl);
219
+ url.searchParams.set('locale', locale);
220
+ const response = await fetchWithTimeout(url, 4000);
221
+ if (!response.ok) {
222
+ return { labels: {}, locale, updatedAt: null };
223
+ }
224
+ const body = (await response.json());
225
+ if (Array.isArray(body.data)) {
226
+ return {
227
+ labels: Object.fromEntries(body.data.map((row) => [row.key, row.value ?? row.defaultValue ?? ''])),
228
+ locale,
229
+ updatedAt: null,
230
+ };
231
+ }
232
+ return {
233
+ labels: body.data?.labels ?? {},
234
+ locale,
235
+ updatedAt: body.data?.updatedAt ?? null,
236
+ };
237
+ }
238
+ catch {
239
+ return { labels: {}, locale, updatedAt: null };
240
+ }
241
+ }
242
+ async function loadRuntimeConfig(apiUrl) {
243
+ try {
244
+ const response = await fetchWithTimeout(new URL('/runtime-config', apiUrl), 2000);
245
+ if (!response.ok) {
246
+ return {};
247
+ }
248
+ const body = await response.json();
249
+ return parseRuntimeConfig(body);
250
+ }
251
+ catch {
252
+ return {};
253
+ }
254
+ }
255
+ function parseRuntimeConfig(body) {
256
+ if (!body || typeof body !== 'object') {
257
+ return {};
258
+ }
259
+ const envelopeData = body.data;
260
+ const source = envelopeData && typeof envelopeData === 'object' ? envelopeData : body;
261
+ const eventsServerUrl = source.eventsServerUrl;
262
+ return {
263
+ eventsServerUrl: typeof eventsServerUrl === 'string' ? eventsServerUrl : undefined,
264
+ };
265
+ }
266
+ async function fetchWithTimeout(url, timeoutMs) {
267
+ const controller = new AbortController();
268
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
269
+ try {
270
+ return await fetch(url, { signal: controller.signal });
271
+ }
272
+ finally {
273
+ clearTimeout(timeout);
274
+ }
275
+ }
276
+ async function readProjectEnv(rootDir) {
277
+ const values = {
278
+ ...(await readEnvFile(join(rootDir, '.env'))),
279
+ ...(await readEnvFile(join(rootDir, '.env.local'))),
280
+ };
281
+ for (const key of ['VITE_API_URL']) {
282
+ if (process.env[key]) {
283
+ values[key] = process.env[key];
284
+ }
285
+ }
286
+ return values;
287
+ }
288
+ async function readEnvFile(filePath) {
289
+ try {
290
+ const content = await readFile(filePath, 'utf8');
291
+ const values = {};
292
+ for (const line of content.split(/\r?\n/)) {
293
+ const trimmed = line.trim();
294
+ if (!trimmed || trimmed.startsWith('#')) {
295
+ continue;
296
+ }
297
+ const index = trimmed.indexOf('=');
298
+ if (index === -1) {
299
+ continue;
300
+ }
301
+ const key = trimmed.slice(0, index).trim();
302
+ const value = trimmed
303
+ .slice(index + 1)
304
+ .trim()
305
+ .replace(/^['"]|['"]$/g, '');
306
+ values[key] = value;
307
+ }
308
+ return values;
309
+ }
310
+ catch {
311
+ return {};
312
+ }
313
+ }
314
+ async function readProjectSettings(rootDir) {
315
+ try {
316
+ const raw = await readFile(join(rootDir, '.fimo.settings.json'), 'utf8');
317
+ return JSON.parse(raw);
318
+ }
319
+ catch {
320
+ return {};
321
+ }
322
+ }
323
+ function deriveEventsRooms(projectId, tenantApiUrl) {
324
+ const rooms = new Set([projectId]);
325
+ try {
326
+ const firstLabel = new URL(tenantApiUrl).hostname.split('.')[0];
327
+ if (firstLabel.startsWith(`${projectId}-`)) {
328
+ rooms.add(`${projectId}:${firstLabel.slice(projectId.length + 1)}`);
329
+ }
330
+ }
331
+ catch {
332
+ return [...rooms];
333
+ }
334
+ return [...rooms];
335
+ }
336
+ async function generateOneTimeToken(apiUrl) {
337
+ const bearer = process.env.FIMO_API_TOKEN?.trim() || (await readStoredToken(apiUrl));
338
+ if (!bearer) {
339
+ throw new Error('Not signed in to Fimo.');
340
+ }
341
+ const response = await fetch(new URL('/api/auth/one-time-token/generate', apiUrl), {
342
+ method: 'POST',
343
+ headers: {
344
+ Authorization: `Bearer ${bearer}`,
345
+ },
346
+ });
347
+ if (!response.ok) {
348
+ throw new Error(`Failed to generate events auth token: ${response.status}`);
349
+ }
350
+ const body = (await response.json());
351
+ const token = body.token ?? body.data?.token;
352
+ if (!token) {
353
+ throw new Error('Events auth token response did not include a token.');
354
+ }
355
+ return token;
356
+ }
357
+ async function readStoredToken(apiUrl) {
358
+ const configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'fimo');
359
+ const credentialsFile = join(configDir, 'credentials', credentialsFilename(apiUrl));
360
+ try {
361
+ const raw = await readFile(credentialsFile, 'utf8');
362
+ const parsed = JSON.parse(raw);
363
+ return parsed.access_token ?? null;
364
+ }
365
+ catch {
366
+ return null;
367
+ }
368
+ }
369
+ function credentialsFilename(apiUrl) {
370
+ const normalized = normalizeApiUrl(apiUrl);
371
+ let hostLabel = 'unknown';
372
+ try {
373
+ hostLabel = new URL(normalized).host
374
+ .replace(/:/g, '-')
375
+ .replace(/[^a-zA-Z0-9.-]+/g, '-')
376
+ .replace(/^[.-]+|[.-]+$/g, '')
377
+ .toLowerCase();
378
+ }
379
+ catch {
380
+ hostLabel = 'unknown';
381
+ }
382
+ return `${hostLabel || 'unknown'}-${createHash('sha256').update(normalized).digest('hex').slice(0, 6)}.json`;
383
+ }
384
+ function normalizeApiUrl(input) {
385
+ const trimmed = input.trim();
386
+ try {
387
+ const url = new URL(trimmed);
388
+ const pathname = url.pathname.replace(/\/+$/, '');
389
+ return `${url.protocol}//${url.host}${pathname}`;
390
+ }
391
+ catch {
392
+ return trimmed.replace(/\/+$/, '');
393
+ }
61
394
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=translations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translations.test.d.ts","sourceRoot":"","sources":["../../../../src/build/vite/plugins/translations.test.ts"],"names":[],"mappings":""}