lino-i18n 0.2.0 → 0.4.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 (82) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +106 -1
  3. package/bin/lino-i18n.js +67 -34
  4. package/package.json +226 -20
  5. package/src/browser.d.ts +58 -0
  6. package/src/browser.js +58 -0
  7. package/src/catalogs.js +512 -0
  8. package/src/compiler.d.ts +53 -0
  9. package/src/compiler.js +72 -0
  10. package/src/content.js +135 -0
  11. package/src/converters/react-intl.js +10 -26
  12. package/src/dictionary.js +169 -0
  13. package/src/eslint.d.ts +15 -0
  14. package/src/eslint.js +244 -0
  15. package/src/extract-bindings.js +41 -0
  16. package/src/extract-derived.js +85 -0
  17. package/src/extract-dictionary.js +122 -0
  18. package/src/extract-jsx.js +176 -0
  19. package/src/extract-project.js +314 -0
  20. package/src/extract.js +365 -0
  21. package/src/format.js +23 -23
  22. package/src/i18n.js +4 -29
  23. package/src/index.d.ts +25 -7
  24. package/src/index.js +13 -2
  25. package/src/intl.d.ts +176 -0
  26. package/src/intl.js +407 -0
  27. package/src/language.js +46 -0
  28. package/src/loaders.js +9 -481
  29. package/src/message-schema.js +26 -0
  30. package/src/messages.d.ts +187 -0
  31. package/src/messages.js +397 -0
  32. package/src/native-state.js +72 -0
  33. package/src/next/client.d.ts +62 -0
  34. package/src/next/client.js +159 -0
  35. package/src/next/proxy.d.ts +17 -0
  36. package/src/next/proxy.js +39 -0
  37. package/src/next/server.d.ts +28 -0
  38. package/src/next/server.js +101 -0
  39. package/src/node-i18n.js +25 -0
  40. package/src/node.d.ts +37 -0
  41. package/src/node.js +61 -0
  42. package/src/providers/gt-locales.d.ts +2 -0
  43. package/src/providers/gt-locales.js +5 -0
  44. package/src/providers/gt.d.ts +75 -0
  45. package/src/providers/gt.js +158 -0
  46. package/src/python-extract.d.ts +16 -0
  47. package/src/python-extract.js +134 -0
  48. package/src/python-extract.py +300 -0
  49. package/src/react-content.js +69 -0
  50. package/src/react-native.d.ts +92 -0
  51. package/src/react-native.js +110 -0
  52. package/src/react-server.d.ts +14 -0
  53. package/src/react-server.js +28 -0
  54. package/src/react.d.ts +112 -9
  55. package/src/react.js +162 -8
  56. package/src/remark.d.ts +11 -0
  57. package/src/remark.js +35 -0
  58. package/src/rrweb/catalog.d.ts +29 -0
  59. package/src/rrweb/catalog.js +158 -0
  60. package/src/rrweb/harvest.d.ts +24 -0
  61. package/src/rrweb/harvest.js +102 -0
  62. package/src/rrweb/index.d.ts +36 -0
  63. package/src/rrweb/index.js +135 -0
  64. package/src/rrweb/replay.d.ts +1 -0
  65. package/src/rrweb/replay.js +3 -0
  66. package/src/server.d.ts +26 -0
  67. package/src/server.js +137 -0
  68. package/src/source-project-files.js +170 -0
  69. package/src/static-values.js +122 -0
  70. package/src/tanstack-start/client.d.ts +20 -0
  71. package/src/tanstack-start/client.js +97 -0
  72. package/src/tanstack-start/server.d.ts +19 -0
  73. package/src/tanstack-start/server.js +41 -0
  74. package/src/tooling-files.js +134 -0
  75. package/src/tooling.d.ts +56 -0
  76. package/src/tooling.js +90 -0
  77. package/src/transform-jsx.js +236 -0
  78. package/src/vue-content.js +82 -0
  79. package/src/vue-extract.d.ts +8 -0
  80. package/src/vue-extract.js +303 -0
  81. package/src/vue.d.ts +120 -0
  82. package/src/vue.js +309 -0
@@ -0,0 +1,58 @@
1
+ import type {
2
+ CompatibilityAliasOptions,
3
+ I18nCoreInstance,
4
+ I18nOptions,
5
+ } from './index.js';
6
+
7
+ export type {
8
+ CompatibilityAlias,
9
+ CompatibilityAliasOptions,
10
+ I18nCoreInstance,
11
+ I18nOptions,
12
+ TOptions,
13
+ TParams,
14
+ } from './index.js';
15
+ export {
16
+ expandCompatibilityAliases,
17
+ parseLinoCatalog,
18
+ parseLinoCatalogs,
19
+ formatLinoCatalog,
20
+ formatLinoCatalogs,
21
+ loadLocaleFromString,
22
+ } from './index.js';
23
+
24
+ export declare function createI18n(options?: I18nOptions): I18nCoreInstance;
25
+
26
+ export interface LoadCatalogsOptions extends CompatibilityAliasOptions {
27
+ /** Override the platform fetch, for example in tests. */
28
+ fetch?: typeof globalThis.fetch;
29
+ /** Options forwarded to every request, including AbortSignal and cache. */
30
+ requestInit?: RequestInit;
31
+ }
32
+
33
+ /** Merge every locale root, with later URLs overriding earlier keys. */
34
+ export declare function loadCatalogs(
35
+ urls: Array<string | URL>,
36
+ options?: LoadCatalogsOptions
37
+ ): Promise<Record<string, Record<string, string>>>;
38
+
39
+ export interface LanguageOptions {
40
+ /** Available catalogue locales; defaults to ['en']. */
41
+ supportedLanguages?: string[];
42
+ /** Locale used when no preference matches; defaults to 'en'. */
43
+ defaultLocale?: string;
44
+ }
45
+
46
+ /** Match an explicit preference, candidates, then the default or first locale. */
47
+ export declare function resolveLanguage(
48
+ preference?: string | null,
49
+ candidates?: string | string[],
50
+ options?: LanguageOptions
51
+ ): string;
52
+
53
+ /** Read navigator.languages, falling back to navigator.language when empty. */
54
+ export declare function detectLanguage(
55
+ preference?: string | null,
56
+ options?: LanguageOptions
57
+ ): string;
58
+ export * from './intl.js';
package/src/browser.js ADDED
@@ -0,0 +1,58 @@
1
+ // Browser entry point: its entire import graph uses only platform APIs.
2
+ import { parseLinoCatalogs } from './catalogs.js';
3
+ import { expandCompatibilityAliases } from './compatibility.js';
4
+
5
+ export { createI18n } from './i18n.js';
6
+ export {
7
+ parseLinoCatalog,
8
+ parseLinoCatalogs,
9
+ formatLinoCatalog,
10
+ formatLinoCatalogs,
11
+ loadLocaleFromString,
12
+ } from './catalogs.js';
13
+ export { expandCompatibilityAliases } from './compatibility.js';
14
+ export { detectLanguage, resolveLanguage } from './language.js';
15
+ export * from './intl.js';
16
+
17
+ // Fetch concurrently, then merge in input order. Generate aliases only after
18
+ // merging so a later explicit key always wins over an earlier generated alias.
19
+ export async function loadCatalogs(urls, options = {}) {
20
+ const { fetch: fetchCatalog = globalThis.fetch, requestInit } = options;
21
+ if (typeof fetchCatalog !== 'function') {
22
+ throw new Error('loadCatalogs requires fetch');
23
+ }
24
+ const loaded = await Promise.all(
25
+ urls.map(async (url) => {
26
+ try {
27
+ const response = await fetchCatalog(url, requestInit);
28
+ if (!response.ok) {
29
+ throw new Error(`HTTP ${response.status}`);
30
+ }
31
+ return parseLinoCatalogs(await response.text());
32
+ } catch (cause) {
33
+ throw new Error(
34
+ `Failed to load catalog ${url}: ${cause?.message ?? String(cause)}`,
35
+ {
36
+ cause,
37
+ }
38
+ );
39
+ }
40
+ })
41
+ );
42
+
43
+ const catalogues = new Map();
44
+ for (const entries of loaded) {
45
+ for (const { locale, translations } of entries) {
46
+ catalogues.set(locale, {
47
+ ...catalogues.get(locale),
48
+ ...translations,
49
+ });
50
+ }
51
+ }
52
+ return Object.fromEntries(
53
+ Array.from(catalogues, ([locale, translations]) => [
54
+ locale,
55
+ expandCompatibilityAliases(translations, options),
56
+ ])
57
+ );
58
+ }
@@ -0,0 +1,512 @@
1
+ // Loaders translate `.lino` text or files into the flat translation
2
+ // tables consumed by the runtime. The authoring format is an i18n-focused
3
+ // subset of indented Links Notation: each top-level block is a locale, nested
4
+ // blocks become dotted keys, and plural/context selector blocks become
5
+ // underscore suffixes such as `cart.items_one` and `role_female`.
6
+
7
+ import { expandCompatibilityAliases } from './compatibility.js';
8
+
9
+ const SELECTOR_SUFFIXES = new Set([
10
+ 'zero',
11
+ 'one',
12
+ 'two',
13
+ 'few',
14
+ 'many',
15
+ 'other',
16
+ 'male',
17
+ 'female',
18
+ 'neutral',
19
+ ]);
20
+
21
+ const LABEL_ALIAS_KEY = 'label';
22
+
23
+ function setEntry(object, key, value) {
24
+ Object.defineProperty(object, key, {
25
+ value,
26
+ enumerable: true,
27
+ writable: true,
28
+ configurable: true,
29
+ });
30
+ }
31
+
32
+ function unescapeValue(value, quote = '"') {
33
+ let result = '';
34
+ for (let index = 0; index < value.length; index += 1) {
35
+ const char = value[index];
36
+ if (char !== '\\') {
37
+ result += char;
38
+ continue;
39
+ }
40
+ index += 1;
41
+ const next = value[index];
42
+ if (next === undefined || next === '\\') {
43
+ result += '\\';
44
+ } else if (next === 'n') {
45
+ result += '\n';
46
+ } else if (next === 'r') {
47
+ result += '\r';
48
+ } else if (next === 't') {
49
+ result += '\t';
50
+ } else if (next === quote) {
51
+ result += quote;
52
+ } else {
53
+ result += `\\${next}`;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function escapeValue(value) {
60
+ return value
61
+ .split('\\')
62
+ .join('\\\\')
63
+ .split('"')
64
+ .join('\\"')
65
+ .split('\r')
66
+ .join('\\r')
67
+ .split('\t')
68
+ .join('\\t');
69
+ }
70
+
71
+ function formatKey(key) {
72
+ return !key || /[\s"'\\:#]/.test(key)
73
+ ? `"${escapeValue(key).replaceAll('\n', '\\n')}"`
74
+ : key;
75
+ }
76
+
77
+ function splitKey(line) {
78
+ if (line.startsWith('"') || line.startsWith("'")) {
79
+ const quote = line[0];
80
+ const closing = findClosingQuote(line, quote);
81
+ if (
82
+ closing === -1 ||
83
+ (line[closing + 1] && !/\s/.test(line[closing + 1]))
84
+ ) {
85
+ throw new Error('Malformed quoted catalog key');
86
+ }
87
+ return [
88
+ unescapeValue(line.slice(1, closing), quote),
89
+ line.slice(closing + 1).trimStart() || undefined,
90
+ ];
91
+ }
92
+ const match = /^(\S+)(?:\s+(.*))?$/.exec(line);
93
+ const key =
94
+ match[2] === undefined && match[1].endsWith(':')
95
+ ? match[1].slice(0, -1)
96
+ : match[1];
97
+ return [key, match[2]?.trimStart()];
98
+ }
99
+
100
+ function countIndent(line) {
101
+ let count = 0;
102
+ for (const char of line) {
103
+ if (char === ' ') {
104
+ count += 1;
105
+ } else if (char === '\t') {
106
+ count += 2;
107
+ } else {
108
+ break;
109
+ }
110
+ }
111
+ return count;
112
+ }
113
+
114
+ function stripContentIndent(line, indent) {
115
+ let remaining = indent;
116
+ let index = 0;
117
+ while (index < line.length && remaining > 0) {
118
+ const char = line[index];
119
+ if (char === ' ') {
120
+ remaining -= 1;
121
+ index += 1;
122
+ } else if (char === '\t') {
123
+ remaining -= 2;
124
+ index += 1;
125
+ } else {
126
+ break;
127
+ }
128
+ }
129
+ return line.slice(index);
130
+ }
131
+
132
+ function findClosingQuote(value, quote, start = 1) {
133
+ let escaped = false;
134
+ for (let index = start; index < value.length; index += 1) {
135
+ const char = value[index];
136
+ if (escaped) {
137
+ escaped = false;
138
+ continue;
139
+ }
140
+ if (char === '\\') {
141
+ escaped = true;
142
+ continue;
143
+ }
144
+ if (char === quote) {
145
+ return index;
146
+ }
147
+ }
148
+ return -1;
149
+ }
150
+
151
+ function parseTripleQuotedValue(lines, index, rest, indent) {
152
+ const sameLine = rest.slice(3);
153
+ if (sameLine.trim()) {
154
+ const closing = sameLine.lastIndexOf('"""');
155
+ if (closing !== -1) {
156
+ return {
157
+ value: sameLine.slice(0, closing),
158
+ nextIndex: index + 1,
159
+ };
160
+ }
161
+ }
162
+
163
+ const valueLines = [];
164
+ const contentIndent = indent + 2;
165
+ for (let current = index + 1; current < lines.length; current += 1) {
166
+ const raw = lines[current].replace(/\r$/, '');
167
+ if (raw.trim() === '"""') {
168
+ return {
169
+ value: valueLines.join('\n'),
170
+ nextIndex: current + 1,
171
+ };
172
+ }
173
+ valueLines.push(stripContentIndent(raw, contentIndent));
174
+ }
175
+ throw new Error('unterminated multiline string');
176
+ }
177
+
178
+ function parseQuotedValue(lines, index, rest, indent) {
179
+ if (rest.startsWith('"""')) {
180
+ return parseTripleQuotedValue(lines, index, rest, indent);
181
+ }
182
+
183
+ const quote = rest[0];
184
+ const closing = findClosingQuote(rest, quote);
185
+ if (closing !== -1) {
186
+ return {
187
+ value: unescapeValue(rest.slice(1, closing), quote),
188
+ nextIndex: index + 1,
189
+ };
190
+ }
191
+
192
+ const chunks = [rest.slice(1)];
193
+ for (let current = index + 1; current < lines.length; current += 1) {
194
+ const raw = lines[current].replace(/\r$/, '');
195
+ const lineClosing = findClosingQuote(raw, quote, 0);
196
+ if (lineClosing !== -1) {
197
+ chunks.push(raw.slice(0, lineClosing));
198
+ return {
199
+ value: unescapeValue(chunks.join('\n'), quote),
200
+ nextIndex: current + 1,
201
+ };
202
+ }
203
+ chunks.push(raw);
204
+ }
205
+ throw new Error('unterminated quoted string');
206
+ }
207
+
208
+ function parseLogicalLines(text) {
209
+ const lines = String(text || '')
210
+ .replace(/\r\n/g, '\n')
211
+ .split('\n');
212
+ const entries = [];
213
+ for (let index = 0; index < lines.length;) {
214
+ const raw = lines[index].replace(/\r$/, '');
215
+ const content = raw.trimEnd();
216
+ if (!content.trim() || content.trimStart().startsWith('#')) {
217
+ index += 1;
218
+ continue;
219
+ }
220
+
221
+ const indent = countIndent(content);
222
+ const trimmed = content.trimStart();
223
+ const [key, rest] = splitKey(trimmed);
224
+
225
+ if (rest === undefined) {
226
+ entries.push({ indent, key, value: null });
227
+ index += 1;
228
+ continue;
229
+ }
230
+
231
+ if (rest.startsWith('"') || rest.startsWith("'")) {
232
+ const parsed = parseQuotedValue(lines, index, rest, indent);
233
+ entries.push({ indent, key, value: parsed.value });
234
+ index = parsed.nextIndex;
235
+ continue;
236
+ }
237
+
238
+ entries.push({ indent, key, value: unescapeValue(rest.trim()) });
239
+ index += 1;
240
+ }
241
+ return entries;
242
+ }
243
+
244
+ function parseEntriesAt(lines, start, indent) {
245
+ const tree = {};
246
+ let index = start;
247
+
248
+ while (index < lines.length) {
249
+ const line = lines[index];
250
+ if (line.indent < indent) {
251
+ break;
252
+ }
253
+ if (line.indent > indent) {
254
+ throw new Error(`unexpected indentation before ${line.key}`);
255
+ }
256
+
257
+ index += 1;
258
+ if (line.value !== null) {
259
+ setEntry(tree, line.key, line.value);
260
+ continue;
261
+ }
262
+
263
+ if (index < lines.length && lines[index].indent > line.indent) {
264
+ const parsed = parseEntriesAt(lines, index, lines[index].indent);
265
+ setEntry(tree, line.key, parsed.tree);
266
+ index = parsed.index;
267
+ } else {
268
+ setEntry(tree, line.key, {});
269
+ }
270
+ }
271
+
272
+ return { tree, index };
273
+ }
274
+
275
+ function parseLocaleTrees(text) {
276
+ const lines = parseLogicalLines(text);
277
+ const catalogues = [];
278
+ let index = 0;
279
+
280
+ while (index < lines.length) {
281
+ const root = lines[index];
282
+ if (root.indent !== 0) {
283
+ throw new Error(`expected a locale root before ${root.key}`);
284
+ }
285
+ if (root.value !== null) {
286
+ throw new Error(`locale root ${root.key} cannot have a direct value`);
287
+ }
288
+
289
+ index += 1;
290
+ let tree = {};
291
+ if (index < lines.length && lines[index].indent > root.indent) {
292
+ const parsed = parseEntriesAt(lines, index, lines[index].indent);
293
+ tree = parsed.tree;
294
+ index = parsed.index;
295
+ }
296
+ catalogues.push({ locale: root.key, tree });
297
+ }
298
+
299
+ return catalogues;
300
+ }
301
+
302
+ function isPlainObject(value) {
303
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
304
+ }
305
+
306
+ function isSelectorGroup(value) {
307
+ const entries = Object.entries(value).filter(
308
+ ([key]) => key !== LABEL_ALIAS_KEY
309
+ );
310
+ return (
311
+ entries.length > 0 &&
312
+ entries.every(
313
+ ([key, child]) => SELECTOR_SUFFIXES.has(key) && typeof child === 'string'
314
+ )
315
+ );
316
+ }
317
+
318
+ function labelAliasValue(value) {
319
+ return typeof value[LABEL_ALIAS_KEY] === 'string'
320
+ ? value[LABEL_ALIAS_KEY]
321
+ : undefined;
322
+ }
323
+
324
+ function addLabelAlias(out, base, value) {
325
+ if (!Object.prototype.hasOwnProperty.call(out, base)) {
326
+ setEntry(out, base, value);
327
+ }
328
+ }
329
+
330
+ function flattenTree(tree, pathParts = [], out = {}) {
331
+ for (const [key, value] of Object.entries(tree)) {
332
+ if (typeof value === 'string') {
333
+ setEntry(out, [...pathParts, key].join('.'), value);
334
+ continue;
335
+ }
336
+ if (!isPlainObject(value)) {
337
+ continue;
338
+ }
339
+
340
+ const nextPath = [...pathParts, key];
341
+ const base = nextPath.join('.');
342
+ const labelValue = labelAliasValue(value);
343
+ if (isSelectorGroup(value)) {
344
+ if (labelValue !== undefined) {
345
+ setEntry(out, `${base}.${LABEL_ALIAS_KEY}`, labelValue);
346
+ addLabelAlias(out, base, labelValue);
347
+ }
348
+ for (const [suffix, child] of Object.entries(value)) {
349
+ if (suffix === LABEL_ALIAS_KEY) {
350
+ continue;
351
+ }
352
+ setEntry(out, `${base}_${suffix}`, child);
353
+ }
354
+ continue;
355
+ }
356
+ flattenTree(value, nextPath, out);
357
+ if (labelValue !== undefined) {
358
+ addLabelAlias(out, base, labelValue);
359
+ }
360
+ }
361
+ return out;
362
+ }
363
+
364
+ function splitSelectorSuffix(key) {
365
+ const index = key.lastIndexOf('_');
366
+ if (index <= 0) {
367
+ return null;
368
+ }
369
+ const suffix = key.slice(index + 1);
370
+ if (!SELECTOR_SUFFIXES.has(suffix)) {
371
+ return null;
372
+ }
373
+ return { base: key.slice(0, index), suffix };
374
+ }
375
+
376
+ function setNestedValue(tree, parts, value) {
377
+ let node = tree;
378
+ for (const part of parts.slice(0, -1)) {
379
+ const current = Object.hasOwn(node, part) ? node[part] : undefined;
380
+ if (!isPlainObject(current)) {
381
+ setEntry(node, part, {});
382
+ if (typeof current === 'string') {
383
+ node[part][LABEL_ALIAS_KEY] = current;
384
+ }
385
+ }
386
+ node = node[part];
387
+ }
388
+ const leaf = parts[parts.length - 1];
389
+ if (Object.hasOwn(node, leaf) && isPlainObject(node[leaf])) {
390
+ if (!Object.prototype.hasOwnProperty.call(node[leaf], LABEL_ALIAS_KEY)) {
391
+ node[leaf][LABEL_ALIAS_KEY] = value;
392
+ }
393
+ return;
394
+ }
395
+ setEntry(node, leaf, value);
396
+ }
397
+
398
+ function translationsToTree(translations) {
399
+ const tree = {};
400
+ for (const [rawKey, rawValue] of Object.entries(translations || {})) {
401
+ const key = String(rawKey);
402
+ const value = typeof rawValue === 'string' ? rawValue : String(rawValue);
403
+ const selector = splitSelectorSuffix(key);
404
+ if (selector) {
405
+ setNestedValue(
406
+ tree,
407
+ [...selector.base.split('.'), selector.suffix],
408
+ value
409
+ );
410
+ } else {
411
+ setNestedValue(tree, key.split('.'), value);
412
+ }
413
+ }
414
+ return tree;
415
+ }
416
+
417
+ function formatValue(value, indent) {
418
+ if (value.includes('\n')) {
419
+ const contentIndent = `${indent} `;
420
+ const lines = value.split('\n').map((line) => `${contentIndent}${line}`);
421
+ return `"""\n${lines.join('\n')}\n${indent}"""`;
422
+ }
423
+ return `"${escapeValue(value)}"`;
424
+ }
425
+
426
+ function formatTreeLines(tree, indent = ' ') {
427
+ const lines = [];
428
+ const entries = Object.entries(tree);
429
+ const labelEntry = entries.find(([key]) => key === LABEL_ALIAS_KEY);
430
+ const orderedEntries = labelEntry
431
+ ? [labelEntry, ...entries.filter(([key]) => key !== LABEL_ALIAS_KEY)]
432
+ : entries;
433
+ for (const [key, value] of orderedEntries) {
434
+ if (typeof value === 'string') {
435
+ lines.push(`${indent}${formatKey(key)} ${formatValue(value, indent)}`);
436
+ continue;
437
+ }
438
+ if (!isPlainObject(value)) {
439
+ continue;
440
+ }
441
+ lines.push(`${indent}${formatKey(key)}`);
442
+ lines.push(...formatTreeLines(value, `${indent} `));
443
+ }
444
+ return lines;
445
+ }
446
+
447
+ function formatFlatCatalog(locale, translations) {
448
+ const lines = [String(locale)];
449
+ for (const [key, value] of Object.entries(translations || {})) {
450
+ const safe = typeof value === 'string' ? value : String(value);
451
+ lines.push(` ${formatKey(key)} ${formatValue(safe, ' ')}`);
452
+ }
453
+ return lines.join('\n');
454
+ }
455
+
456
+ // Parse the contents of one `.lino` catalogue. Returns the first
457
+ // `{ locale, translations }` pair when the file contains multiple locale
458
+ // roots. Use `parseLinoCatalogs` to keep every root.
459
+ export function parseLinoCatalog(text, options = {}) {
460
+ const catalogues = parseLinoCatalogs(text, options);
461
+ return catalogues[0] || { locale: null, translations: {} };
462
+ }
463
+
464
+ // Parse every top-level locale block in a `.lino` string.
465
+ export function parseLinoCatalogs(text, options = {}) {
466
+ return parseLocaleTrees(text).map(({ locale, tree }) => ({
467
+ locale: locale || null,
468
+ translations: expandCompatibilityAliases(flattenTree(tree), options),
469
+ }));
470
+ }
471
+
472
+ // Format a flat translation table as indented Links Notation. The default
473
+ // output is nested for readability; pass `{ style: 'flat' }` to emit one
474
+ // key/value line per translation.
475
+ export function formatLinoCatalog(locale, translations, options = {}) {
476
+ if (!locale) {
477
+ throw new Error('formatLinoCatalog requires a locale name');
478
+ }
479
+ if (options.style === 'flat') {
480
+ return formatFlatCatalog(locale, translations);
481
+ }
482
+ const lines = [String(locale)];
483
+ lines.push(...formatTreeLines(translationsToTree(translations)));
484
+ return lines.join('\n');
485
+ }
486
+
487
+ export function formatLinoCatalogs(catalogues, options = {}) {
488
+ const entries = Array.isArray(catalogues)
489
+ ? catalogues.map(({ locale, translations }) => [locale, translations])
490
+ : Object.entries(catalogues || {}).sort(([left], [right]) =>
491
+ left.localeCompare(right)
492
+ );
493
+ return entries
494
+ .map(([locale, translations]) =>
495
+ formatLinoCatalog(locale, translations, options)
496
+ )
497
+ .join('\n\n');
498
+ }
499
+
500
+ // The public async API converts parse errors to Promise rejections.
501
+ // eslint-disable-next-line require-await
502
+ export async function loadLocaleFromString(locale, text, options = {}) {
503
+ const parsedCatalogues = parseLinoCatalogs(text, options);
504
+ const parsed = parsedCatalogues.find(
505
+ (catalogue) => catalogue.locale === locale
506
+ ) ||
507
+ parsedCatalogues[0] || { locale: null, translations: {} };
508
+ return {
509
+ locale: locale || parsed.locale,
510
+ translations: parsed.translations,
511
+ };
512
+ }
@@ -0,0 +1,53 @@
1
+ export interface JSXTransformOptions {
2
+ file?: string;
3
+ autoText?: boolean;
4
+ attributes?: string[];
5
+ components?: Record<string, string[]>;
6
+ attributeTranslator?: string;
7
+ serverTranslator?: string;
8
+ }
9
+ export interface TransformedSource {
10
+ code: string;
11
+ map: {
12
+ version: number;
13
+ sources: string[];
14
+ sourcesContent?: Array<string | null>;
15
+ names: string[];
16
+ mappings: string;
17
+ } | null;
18
+ }
19
+ export declare function transformJSX(
20
+ code: string,
21
+ options?: JSXTransformOptions
22
+ ): TransformedSource;
23
+ export declare function createExtractionPlugin(options?: {
24
+ locale?: string;
25
+ catalogFile?: string;
26
+ manifestFile?: string;
27
+ transform?: JSXTransformOptions;
28
+ }): {
29
+ name: string;
30
+ enforce: 'pre';
31
+ buildStart(): void;
32
+ transform(
33
+ this: { error(message: string): never },
34
+ code: string,
35
+ id: string
36
+ ): TransformedSource | null;
37
+ generateBundle(this: {
38
+ error(message: string): never;
39
+ emitFile(asset: {
40
+ type: 'asset';
41
+ fileName: string;
42
+ source: string;
43
+ }): unknown;
44
+ resolve?(
45
+ source: string,
46
+ importer: string,
47
+ options: { skipSelf: true }
48
+ ): Promise<{
49
+ id: string;
50
+ external?: boolean | 'absolute' | 'relative';
51
+ } | null>;
52
+ }): Promise<void>;
53
+ };