@wads.dev/i18n-editor 0.0.1-alpha.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.
@@ -0,0 +1,33 @@
1
+ import { type I18nProjectConfig } from '@wads.dev/i18n-ts/config';
2
+ export type I18nDeletionConfig = {
3
+ ignoredExtensions: string[];
4
+ autoDelete: boolean;
5
+ };
6
+ export type I18nExportConfig = {
7
+ importAliases: Record<string, string>;
8
+ codeFormat: I18nCodeFormatConfig;
9
+ };
10
+ export type I18nCodeFormatConfig = {
11
+ useDoubleQuotes: boolean;
12
+ useSemicolons: boolean;
13
+ useShorthandProperties: boolean;
14
+ useTrailingCommas: boolean;
15
+ printWidth: number;
16
+ maxObjectInlineItems: number;
17
+ maxArrayInlineItems: number;
18
+ objectLayout: 'fit' | 'multiline';
19
+ arrayLayout: 'fit' | 'multiline';
20
+ indentation: {
21
+ character: 'space' | 'tab';
22
+ size: number;
23
+ };
24
+ };
25
+ export type EditorProjectConfig = Omit<I18nProjectConfig, 'deletion' | 'importAliases' | 'exportConfig'> & {
26
+ catalogFile: string;
27
+ deletion: false | I18nDeletionConfig;
28
+ exportConfig: I18nExportConfig;
29
+ };
30
+ export declare function normalizeDeletionConfig(value: unknown): false | I18nDeletionConfig;
31
+ export declare function normalizeEditorProjectConfig(value: unknown): EditorProjectConfig;
32
+ export declare function createDefaultEditorProjectConfig(): EditorProjectConfig;
33
+ export declare function getEditorLevelName(config: EditorProjectConfig, level: number): string;
@@ -0,0 +1,93 @@
1
+ import { createDefaultProjectConfig, normalizeProjectConfig, } from '@wads.dev/i18n-ts/config';
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ function normalizeStringRecord(value) {
6
+ if (!isRecord(value))
7
+ return {};
8
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
9
+ }
10
+ function normalizeCodeFormat(value, legacyExportConfig) {
11
+ const input = isRecord(value) ? value : {};
12
+ const indentation = isRecord(input.indentation) ? input.indentation : {};
13
+ const indentationSize = Number.parseInt(String(indentation.size ?? 2), 10);
14
+ const printWidth = Number.parseInt(String(input.printWidth ?? 120), 10);
15
+ const maxObjectInlineItems = Number.parseInt(String(input.maxObjectInlineItems ?? 4), 10);
16
+ const maxArrayInlineItems = Number.parseInt(String(input.maxArrayInlineItems ?? 8), 10);
17
+ return {
18
+ useDoubleQuotes: (input.useDoubleQuotes ?? legacyExportConfig.useDoubleQuotes) === true,
19
+ useSemicolons: input.useSemicolons !== false,
20
+ useShorthandProperties: input.useShorthandProperties !== false,
21
+ useTrailingCommas: input.useTrailingCommas !== false,
22
+ printWidth: Number.isFinite(printWidth) ? Math.min(400, Math.max(40, printWidth)) : 120,
23
+ maxObjectInlineItems: Number.isFinite(maxObjectInlineItems) ? Math.min(100, Math.max(0, maxObjectInlineItems)) : 4,
24
+ maxArrayInlineItems: Number.isFinite(maxArrayInlineItems) ? Math.min(100, Math.max(0, maxArrayInlineItems)) : 8,
25
+ objectLayout: input.objectLayout === 'multiline' ? 'multiline' : 'fit',
26
+ arrayLayout: input.arrayLayout === 'multiline' ? 'multiline' : 'fit',
27
+ indentation: {
28
+ character: indentation.character === 'tab' ? 'tab' : 'space',
29
+ size: Number.isFinite(indentationSize) ? Math.min(8, Math.max(1, indentationSize)) : 2,
30
+ },
31
+ };
32
+ }
33
+ export function normalizeDeletionConfig(value) {
34
+ if (value === false)
35
+ return false;
36
+ const input = isRecord(value) ? value : {};
37
+ const ignoredExtensions = Array.isArray(input.ignoredExtensions)
38
+ ? [...new Set(input.ignoredExtensions
39
+ .filter((extension) => typeof extension === 'string')
40
+ .map((extension) => extension.trim().toLowerCase())
41
+ .filter(Boolean)
42
+ .map((extension) => extension.startsWith('.') ? extension : `.${extension}`))]
43
+ : [];
44
+ return {
45
+ ignoredExtensions,
46
+ autoDelete: input.autoDelete === true,
47
+ };
48
+ }
49
+ export function normalizeEditorProjectConfig(value) {
50
+ const input = isRecord(value) ? value : {};
51
+ const exportInput = isRecord(input.exportConfig) ? input.exportConfig : {};
52
+ const importAliases = normalizeStringRecord(exportInput.importAliases ?? input.importAliases);
53
+ const normalized = normalizeProjectConfig({
54
+ ...input,
55
+ importAliases,
56
+ });
57
+ const normalizedRecord = normalized;
58
+ const normalizedExport = isRecord(normalizedRecord.exportConfig) ? normalizedRecord.exportConfig : {};
59
+ const normalizedAliases = normalizeStringRecord(normalizedExport.importAliases ?? normalizedRecord.importAliases);
60
+ const { importAliases: _legacyAliases, exportConfig: _normalizedExport, ...config } = normalizedRecord;
61
+ return {
62
+ ...config,
63
+ catalogFile: typeof input.catalogFile === 'string' && input.catalogFile.trim()
64
+ ? input.catalogFile.trim()
65
+ : 'src/shared/i18n/index.ts',
66
+ deletion: normalizeDeletionConfig(input.deletion),
67
+ exportConfig: {
68
+ importAliases: normalizedAliases,
69
+ codeFormat: normalizeCodeFormat(exportInput.codeFormat, exportInput),
70
+ },
71
+ };
72
+ }
73
+ export function createDefaultEditorProjectConfig() {
74
+ const defaults = createDefaultProjectConfig();
75
+ const defaultsExport = isRecord(defaults.exportConfig) ? defaults.exportConfig : {};
76
+ const importAliases = normalizeStringRecord(defaultsExport.importAliases ?? defaults.importAliases);
77
+ const { importAliases: _legacyAliases, exportConfig: _defaultsExport, ...config } = defaults;
78
+ return {
79
+ ...config,
80
+ catalogFile: 'src/shared/i18n/index.ts',
81
+ deletion: normalizeDeletionConfig(undefined),
82
+ exportConfig: {
83
+ importAliases,
84
+ codeFormat: normalizeCodeFormat(undefined, {}),
85
+ },
86
+ };
87
+ }
88
+ export function getEditorLevelName(config, level) {
89
+ if (level === 0)
90
+ return 'Root';
91
+ const names = config.levelNames.split(',').map((name) => name.trim()).filter(Boolean);
92
+ return names[level - 1] || `Level ${level}`;
93
+ }
@@ -0,0 +1,7 @@
1
+ import { type I18nBundle } from '@wads.dev/i18n-ts/bundle';
2
+ export type SetStringValueOptions = {
3
+ languageKey: string;
4
+ key: string;
5
+ value: string;
6
+ };
7
+ export declare function setStringValue(bundle: I18nBundle, { languageKey, key, value }: SetStringValueOptions): I18nBundle;
@@ -0,0 +1,23 @@
1
+ import { assertBundle } from '@wads.dev/i18n-ts/bundle';
2
+ import { getKeyPathValue, parseKeyPath, setKeyPathValue } from './keyPath.js';
3
+ function cloneBundle(bundle) {
4
+ return structuredClone(bundle);
5
+ }
6
+ export function setStringValue(bundle, { languageKey, key, value }) {
7
+ const validBundle = assertBundle(bundle);
8
+ const language = validBundle.languages[languageKey];
9
+ if (!language) {
10
+ throw new Error(`Language "${languageKey}" does not exist in the bundle.`);
11
+ }
12
+ const segments = parseKeyPath(key, 'key');
13
+ const currentValue = getKeyPathValue(language.translations, segments);
14
+ if (!currentValue.found) {
15
+ throw new Error(`Key "${key}" does not exist in language "${languageKey}".`);
16
+ }
17
+ if (typeof currentValue.value !== 'string') {
18
+ throw new Error('Editing functions and non-string values is not supported yet.');
19
+ }
20
+ const nextBundle = cloneBundle(validBundle);
21
+ setKeyPathValue(nextBundle.languages[languageKey].translations, segments, value);
22
+ return nextBundle;
23
+ }
@@ -0,0 +1,19 @@
1
+ import { type I18nBundle } from '@wads.dev/i18n-ts/bundle';
2
+ import { type EditorProjectConfig } from './projectConfig.js';
3
+ export type GeneratedSourceFile = {
4
+ kind: 'base' | 'index' | 'language';
5
+ path: string;
6
+ content: string;
7
+ };
8
+ export type SourceGenerationOptions = {
9
+ catalogFile?: string;
10
+ existingInterfaceNames?: Record<string, string>;
11
+ existingLanguageTypeName?: string;
12
+ existingPropertyTypes?: Record<string, string>;
13
+ existingTypeImports?: Record<string, Array<{
14
+ name: string;
15
+ path: string;
16
+ }>>;
17
+ existingValueImportOrder?: Record<string, string[]>;
18
+ };
19
+ export declare function generateSourceFiles(bundle: I18nBundle, projectConfig: EditorProjectConfig, options?: SourceGenerationOptions): GeneratedSourceFile[];
@@ -0,0 +1,406 @@
1
+ import { assertBundle } from '@wads.dev/i18n-ts/bundle';
2
+ import { buildTranslationOwners } from './exportPlan.js';
3
+ import { normalizeEditorProjectConfig, } from './projectConfig.js';
4
+ function appendPath(basePath, nextPath) {
5
+ return [basePath, nextPath].filter(Boolean).join('/').replaceAll(/\/+/g, '/').replace(/^\.\//, '');
6
+ }
7
+ function isRecord(value) {
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+ function isFunctionDescriptor(value) {
11
+ return isRecord(value) && value.$type === 'function' && typeof value.source === 'string';
12
+ }
13
+ function isTypeReference(value) {
14
+ return isRecord(value) && typeof value.$reference === 'string' && typeof value.$value === 'string';
15
+ }
16
+ function getAtPath(value, keyPath) {
17
+ if (!keyPath)
18
+ return value;
19
+ return keyPath.split('.').reduce((current, segment) => {
20
+ return isRecord(current) ? current[segment] : undefined;
21
+ }, value);
22
+ }
23
+ function setAtPath(target, segments, value) {
24
+ let current = target;
25
+ segments.forEach((segment, index) => {
26
+ if (index === segments.length - 1) {
27
+ current[segment] = value;
28
+ return;
29
+ }
30
+ const existing = current[segment];
31
+ if (!isRecord(existing) || isFunctionDescriptor(existing) || isTypeReference(existing)) {
32
+ current[segment] = {};
33
+ }
34
+ current = current[segment];
35
+ });
36
+ }
37
+ function toPascalCase(value) {
38
+ const result = value
39
+ .split(/[^A-Za-z0-9]+/)
40
+ .filter(Boolean)
41
+ .map((part) => `${part[0]?.toUpperCase() || ''}${part.slice(1)}`)
42
+ .join('');
43
+ return result || 'Project';
44
+ }
45
+ function toIdentifier(value) {
46
+ const pascal = toPascalCase(value);
47
+ const identifier = `${pascal[0]?.toLowerCase() || ''}${pascal.slice(1)}`;
48
+ return /^[$A-Z_a-z]/.test(identifier) ? identifier : `_${identifier}`;
49
+ }
50
+ function quoteString(value, codeFormat) {
51
+ if (codeFormat.useDoubleQuotes)
52
+ return JSON.stringify(value);
53
+ const escaped = [...value].map((character) => {
54
+ if (character === "'")
55
+ return "\\'";
56
+ if (character === '\\')
57
+ return '\\\\';
58
+ if (character === '\n')
59
+ return '\\n';
60
+ if (character === '\r')
61
+ return '\\r';
62
+ if (character === '\t')
63
+ return '\\t';
64
+ if (character === '\b')
65
+ return '\\b';
66
+ if (character === '\f')
67
+ return '\\f';
68
+ const codePoint = character.codePointAt(0);
69
+ if (codePoint < 0x20 || codePoint === 0x2028 || codePoint === 0x2029) {
70
+ return `\\u${codePoint.toString(16).padStart(4, '0')}`;
71
+ }
72
+ return character;
73
+ }).join('');
74
+ return `'${escaped}'`;
75
+ }
76
+ function propertyName(value, codeFormat) {
77
+ return /^[$A-Z_a-z][$\w]*$/.test(value) ? value : quoteString(value, codeFormat);
78
+ }
79
+ function indent(level, codeFormat) {
80
+ const character = codeFormat.indentation.character === 'tab' ? '\t' : ' ';
81
+ return character.repeat(codeFormat.indentation.size * level);
82
+ }
83
+ function terminateStatement(value, codeFormat) {
84
+ return `${value}${codeFormat.useSemicolons ? ';' : ''}`;
85
+ }
86
+ function addCommas(values, codeFormat) {
87
+ return values.map((value, index) => {
88
+ const needsComma = index < values.length - 1 || codeFormat.useTrailingCommas;
89
+ return `${value}${needsComma ? ',' : ''}`;
90
+ });
91
+ }
92
+ function renderCompactValue(value, codeFormat) {
93
+ if (isTypeReference(value))
94
+ return value.$value;
95
+ if (isFunctionDescriptor(value))
96
+ return value.source.includes('\n') ? null : value.source;
97
+ if (Array.isArray(value)) {
98
+ if (codeFormat.arrayLayout === 'multiline')
99
+ return null;
100
+ if (value.length > codeFormat.maxArrayInlineItems)
101
+ return null;
102
+ if (value.some((item) => Array.isArray(item) || (isRecord(item) && !isFunctionDescriptor(item) && !isTypeReference(item))))
103
+ return null;
104
+ const items = value.map((item) => renderCompactValue(item, codeFormat));
105
+ if (items.some((item) => item === null))
106
+ return null;
107
+ return `[${items.join(', ')}]`;
108
+ }
109
+ if (isRecord(value)) {
110
+ if (codeFormat.objectLayout === 'multiline')
111
+ return null;
112
+ if (Object.keys(value).length > codeFormat.maxObjectInlineItems)
113
+ return null;
114
+ if (Object.values(value).some((item) => Array.isArray(item) || (isRecord(item) && !isFunctionDescriptor(item) && !isTypeReference(item))))
115
+ return null;
116
+ const properties = Object.entries(value).map(([key, item]) => {
117
+ const renderedValue = renderCompactValue(item, codeFormat);
118
+ if (renderedValue === null)
119
+ return null;
120
+ const canUseShorthand = codeFormat.useShorthandProperties
121
+ && /^[$A-Z_a-z][$\w]*$/.test(key)
122
+ && renderedValue === key;
123
+ return canUseShorthand ? key : `${propertyName(key, codeFormat)}: ${renderedValue}`;
124
+ });
125
+ if (properties.some((property) => property === null))
126
+ return null;
127
+ return `{ ${properties.join(', ')} }`;
128
+ }
129
+ if (typeof value === 'string')
130
+ return quoteString(value, codeFormat);
131
+ return JSON.stringify(value);
132
+ }
133
+ function getTypeName(owner, basePath, names) {
134
+ return names[basePath] || `${toPascalCase(owner.keyPath)}Translation`;
135
+ }
136
+ function getFunctionType(source) {
137
+ const arrowIndex = source.indexOf('=>');
138
+ if (arrowIndex < 0)
139
+ return '(...args: any[]) => string';
140
+ const rawParameters = source.slice(0, arrowIndex).trim().replace(/^async\s+/, '');
141
+ const parameterText = rawParameters.startsWith('(')
142
+ ? rawParameters.slice(1, rawParameters.lastIndexOf(')'))
143
+ : rawParameters;
144
+ const parameters = parameterText
145
+ .split(',')
146
+ .map((parameter) => parameter.trim().replace(/=.*$/, '').replace(/^\.\.\./, '').trim())
147
+ .filter((parameter) => /^[$A-Z_a-z][$\w]*$/.test(parameter))
148
+ .map((parameter) => `${parameter}: any`);
149
+ return `(${parameters.join(', ')}) => string`;
150
+ }
151
+ function renderType(value, level, keyPath, existingPropertyTypes, codeFormat) {
152
+ if (isTypeReference(value))
153
+ return value.$reference;
154
+ if (existingPropertyTypes[keyPath])
155
+ return existingPropertyTypes[keyPath];
156
+ if (isFunctionDescriptor(value))
157
+ return getFunctionType(value.source);
158
+ if (Array.isArray(value)) {
159
+ if (value.length === 0)
160
+ return 'unknown[]';
161
+ const types = [...new Set(value.map((item) => renderType(item, level, keyPath, existingPropertyTypes, codeFormat)))];
162
+ return types.length === 1 ? `${types[0]}[]` : `Array<${types.join(' | ')}>`;
163
+ }
164
+ if (isRecord(value)) {
165
+ const entries = Object.entries(value);
166
+ if (entries.length === 0)
167
+ return 'Record<string, never>';
168
+ const pad = indent(level, codeFormat);
169
+ const childPad = indent(level + 1, codeFormat);
170
+ return `{\n${entries.map(([key, item]) => {
171
+ const childPath = keyPath ? `${keyPath}.${key}` : key;
172
+ const member = `${childPad}${propertyName(key, codeFormat)}: ${renderType(item, level + 1, childPath, existingPropertyTypes, codeFormat)}`;
173
+ return terminateStatement(member, codeFormat);
174
+ }).join('\n')}\n${pad}}`;
175
+ }
176
+ if (value === null)
177
+ return 'null';
178
+ return typeof value;
179
+ }
180
+ function renderValue(value, level, codeFormat, currentColumn = 0, allowInline = true) {
181
+ if (isTypeReference(value))
182
+ return value.$value;
183
+ if (isFunctionDescriptor(value))
184
+ return value.source;
185
+ if (Array.isArray(value)) {
186
+ if (value.length === 0)
187
+ return '[]';
188
+ const compact = allowInline ? renderCompactValue(value, codeFormat) : null;
189
+ if (compact !== null && currentColumn + compact.length <= codeFormat.printWidth)
190
+ return compact;
191
+ const pad = indent(level, codeFormat);
192
+ const childPad = indent(level + 1, codeFormat);
193
+ const items = value.map((item) => `${childPad}${renderValue(item, level + 1, codeFormat, childPad.length)}`);
194
+ return `[\n${addCommas(items, codeFormat).join('\n')}\n${pad}]`;
195
+ }
196
+ if (isRecord(value)) {
197
+ const entries = Object.entries(value);
198
+ if (entries.length === 0)
199
+ return '{}';
200
+ const compact = allowInline ? renderCompactValue(value, codeFormat) : null;
201
+ if (compact !== null && currentColumn + compact.length <= codeFormat.printWidth)
202
+ return compact;
203
+ const pad = indent(level, codeFormat);
204
+ const childPad = indent(level + 1, codeFormat);
205
+ const properties = entries.map(([key, item]) => {
206
+ const renderedKey = propertyName(key, codeFormat);
207
+ const valueColumn = childPad.length + renderedKey.length + 2;
208
+ const renderedValue = renderValue(item, level + 1, codeFormat, valueColumn);
209
+ const canUseShorthand = codeFormat.useShorthandProperties
210
+ && /^[$A-Z_a-z][$\w]*$/.test(key)
211
+ && renderedValue === key;
212
+ return `${childPad}${canUseShorthand ? key : `${renderedKey}: ${renderedValue}`}`;
213
+ });
214
+ return `{\n${addCommas(properties, codeFormat).join('\n')}\n${pad}}`;
215
+ }
216
+ if (typeof value === 'string')
217
+ return quoteString(value, codeFormat);
218
+ return JSON.stringify(value);
219
+ }
220
+ function relativeImport(fromFile, toFile) {
221
+ const fromSegments = fromFile.split('/').slice(0, -1);
222
+ const toSegments = toFile.replace(/\.ts$/, '').split('/');
223
+ while (fromSegments[0] === toSegments[0]) {
224
+ fromSegments.shift();
225
+ toSegments.shift();
226
+ }
227
+ const relative = [...fromSegments.map(() => '..'), ...toSegments].join('/');
228
+ return relative.startsWith('.') ? relative : `./${relative}`;
229
+ }
230
+ function aliasedImport(toFile, aliases) {
231
+ const normalizedTarget = toFile.replace(/\.ts$/, '');
232
+ const match = Object.entries(aliases)
233
+ .filter(([, target]) => {
234
+ const prefix = target.replace(/\/$/, '');
235
+ return normalizedTarget === prefix || normalizedTarget.startsWith(`${prefix}/`);
236
+ })
237
+ .sort((left, right) => right[1].length - left[1].length)[0];
238
+ if (!match)
239
+ return null;
240
+ const [alias, target] = match;
241
+ const normalizedAlias = alias.endsWith('/') ? alias : `${alias}/`;
242
+ const normalizedPrefix = target.replace(/\/$/, '');
243
+ const remainder = normalizedTarget.slice(normalizedPrefix.length).replace(/^\//, '');
244
+ return `${normalizedAlias}${remainder}`;
245
+ }
246
+ function generatedImport(fromFile, toFile, owner, config) {
247
+ if (owner.importPathStyle === 'alias') {
248
+ const aliased = aliasedImport(toFile, config.exportConfig.importAliases);
249
+ if (aliased)
250
+ return aliased;
251
+ }
252
+ return relativeImport(fromFile, toFile);
253
+ }
254
+ function ownerIdentifier(owner) {
255
+ return toIdentifier(owner.keyPath.split('.').at(-1) || owner.keyPath);
256
+ }
257
+ function orderChildrenByExistingImports(children, childPaths, existingOrder = []) {
258
+ const ranks = new Map(existingOrder.map((targetPath, index) => [targetPath, index]));
259
+ return [...children].sort((left, right) => {
260
+ const leftRank = ranks.get(childPaths.get(left.keyPath) || '') ?? Number.POSITIVE_INFINITY;
261
+ const rightRank = ranks.get(childPaths.get(right.keyPath) || '') ?? Number.POSITIVE_INFINITY;
262
+ return leftRank - rightRank;
263
+ });
264
+ }
265
+ function renderBaseFile(typeName, ownerKeyPath, tree, imports, existingPropertyTypes, codeFormat) {
266
+ const importLines = [
267
+ `import type { Translation } from ${quoteString('@wads.dev/i18n-ts', codeFormat)}`,
268
+ ...imports.map((item) => `import type ${item.name} from ${quoteString(item.path, codeFormat)}`),
269
+ ].map((line) => terminateStatement(line, codeFormat));
270
+ const body = Object.entries(tree)
271
+ .map(([key, value]) => {
272
+ const keyPath = ownerKeyPath ? `${ownerKeyPath}.${key}` : key;
273
+ const member = `${indent(1, codeFormat)}${propertyName(key, codeFormat)}: ${renderType(value, 1, keyPath, existingPropertyTypes, codeFormat)}`;
274
+ return terminateStatement(member, codeFormat);
275
+ })
276
+ .join('\n');
277
+ return `${importLines.join('\n')}\n\nexport default interface ${typeName} extends Translation {\n${body}\n}\n`;
278
+ }
279
+ function renderLanguageFile(variableName, typeName, tree, imports, codeFormat) {
280
+ const importLines = [
281
+ ...imports.map((item) => `import ${item.name} from ${quoteString(item.path, codeFormat)}`),
282
+ `import type ${typeName} from ${quoteString('./base', codeFormat)}`,
283
+ ].map((line) => terminateStatement(line, codeFormat));
284
+ const declaration = terminateStatement(`const ${variableName}: ${typeName} = ${renderValue(tree, 0, codeFormat, 0, false)}`, codeFormat);
285
+ const exportLine = terminateStatement(`export default ${variableName}`, codeFormat);
286
+ return `${importLines.join('\n')}\n\n${declaration}\n\n${exportLine}\n`;
287
+ }
288
+ function renderIndexFile(bundle, rootTypeName, languageTypeName, config, indexPath, rootBasePath) {
289
+ const languageKeys = Object.keys(bundle.languages);
290
+ const codeFormat = config.exportConfig.codeFormat;
291
+ const languageUnion = languageKeys.map((key) => quoteString(key, codeFormat)).join(' | ') || 'never';
292
+ const entries = languageKeys.map((languageKey) => {
293
+ const language = bundle.languages[languageKey];
294
+ const filename = config.languageReplacer[languageKey] || languageKey;
295
+ const languagePath = `${rootBasePath.slice(0, -'base.ts'.length)}${config.languageFileTemplate.replaceAll('{language}', filename)}`;
296
+ const properties = addCommas([
297
+ `${indent(2, codeFormat)}lang: () => import(${quoteString(relativeImport(indexPath, languagePath), codeFormat)})`,
298
+ `${indent(2, codeFormat)}name: ${quoteString(language.name, codeFormat)}`,
299
+ `${indent(2, codeFormat)}short: ${quoteString(language.short, codeFormat)}`,
300
+ `${indent(2, codeFormat)}locale: ${quoteString(language.locale, codeFormat)}`,
301
+ ], codeFormat);
302
+ return `${indent(1, codeFormat)}${propertyName(languageKey, codeFormat)}: {\n${properties.join('\n')}\n${indent(1, codeFormat)}}`;
303
+ });
304
+ const imports = [
305
+ terminateStatement(`import type { AvailableLangs } from ${quoteString('@wads.dev/i18n-ts', codeFormat)}`, codeFormat),
306
+ terminateStatement(`import type ${rootTypeName} from ${quoteString(relativeImport(indexPath, rootBasePath), codeFormat)}`, codeFormat),
307
+ ];
308
+ const typeDeclaration = terminateStatement(`export type ${languageTypeName} = ${languageUnion}`, codeFormat);
309
+ const langsDeclaration = terminateStatement(`export const Langs: AvailableLangs<${languageTypeName}, ${rootTypeName}> = {\n${addCommas(entries, codeFormat).join('\n')}\n}`, codeFormat);
310
+ return `${imports.join('\n')}\n\n${typeDeclaration}\n\n${langsDeclaration}\n`;
311
+ }
312
+ export function generateSourceFiles(bundle, projectConfig, options = {}) {
313
+ const validBundle = assertBundle(bundle);
314
+ const config = normalizeEditorProjectConfig(projectConfig);
315
+ const owners = buildTranslationOwners(validBundle, config);
316
+ const ownerByKey = new Map(owners.map((owner) => [owner.keyPath, owner]));
317
+ const typeNames = new Map();
318
+ const basePaths = new Map();
319
+ owners.forEach((owner) => {
320
+ const basePath = appendPath(appendPath(owner.directory, config.translationsDirectory), 'base.ts');
321
+ basePaths.set(owner.keyPath, basePath);
322
+ typeNames.set(owner.keyPath, getTypeName(owner, basePath, options.existingInterfaceNames || {}));
323
+ });
324
+ function getParent(owner) {
325
+ return owners
326
+ .filter((candidate) => candidate.keyPath !== owner.keyPath)
327
+ .filter((candidate) => !candidate.keyPath || owner.keyPath.startsWith(`${candidate.keyPath}.`))
328
+ .sort((left, right) => right.keyPath.length - left.keyPath.length)[0];
329
+ }
330
+ const children = new Map();
331
+ owners.forEach((owner) => {
332
+ const parent = getParent(owner);
333
+ if (!parent)
334
+ return;
335
+ children.set(parent.keyPath, [...(children.get(parent.keyPath) || []), owner]);
336
+ });
337
+ const files = [];
338
+ owners.forEach((owner) => {
339
+ const translationDirectory = appendPath(owner.directory, config.translationsDirectory);
340
+ const basePath = basePaths.get(owner.keyPath);
341
+ const typeName = typeNames.get(owner.keyPath);
342
+ const ownerChildren = children.get(owner.keyPath) || [];
343
+ const referenceTree = structuredClone(getAtPath(Object.values(validBundle.languages)[0]?.translations, owner.keyPath) || {});
344
+ ownerChildren.forEach((child) => {
345
+ const relativeSegments = child.keyPath.slice(owner.keyPath ? owner.keyPath.length + 1 : 0).split('.');
346
+ setAtPath(referenceTree, relativeSegments, {
347
+ $reference: typeNames.get(child.keyPath),
348
+ $value: ownerIdentifier(child),
349
+ });
350
+ });
351
+ const generatedBaseImports = ownerChildren.map((child) => ({
352
+ name: typeNames.get(child.keyPath),
353
+ path: generatedImport(basePath, basePaths.get(child.keyPath), child, config),
354
+ }));
355
+ const generatedImportNames = new Set(generatedBaseImports.map((item) => item.name));
356
+ const preservedBaseImports = (options.existingTypeImports?.[basePath] || [])
357
+ .filter((item) => !generatedImportNames.has(item.name));
358
+ const baseImports = [...generatedBaseImports, ...preservedBaseImports];
359
+ files.push({
360
+ kind: 'base',
361
+ path: basePath,
362
+ content: renderBaseFile(typeName, owner.keyPath, referenceTree, baseImports, options.existingPropertyTypes || {}, config.exportConfig.codeFormat),
363
+ });
364
+ Object.entries(validBundle.languages).forEach(([languageKey, language]) => {
365
+ const filenameValue = config.languageReplacer[languageKey] || languageKey;
366
+ const languagePath = appendPath(translationDirectory, config.languageFileTemplate.replaceAll('{language}', filenameValue));
367
+ const tree = structuredClone(getAtPath(language.translations, owner.keyPath) || {});
368
+ ownerChildren.forEach((child) => {
369
+ const relativeSegments = child.keyPath.slice(owner.keyPath ? owner.keyPath.length + 1 : 0).split('.');
370
+ setAtPath(tree, relativeSegments, {
371
+ $reference: typeNames.get(child.keyPath),
372
+ $value: ownerIdentifier(child),
373
+ });
374
+ });
375
+ const childPaths = new Map(ownerChildren.map((child) => {
376
+ const childDirectory = appendPath(child.directory, config.translationsDirectory);
377
+ const childPath = appendPath(childDirectory, config.languageFileTemplate.replaceAll('{language}', filenameValue));
378
+ return [child.keyPath, childPath];
379
+ }));
380
+ const orderedChildren = orderChildrenByExistingImports(ownerChildren, childPaths, options.existingValueImportOrder?.[languagePath]);
381
+ const languageImports = orderedChildren.map((child) => {
382
+ const childPath = childPaths.get(child.keyPath);
383
+ return {
384
+ name: ownerIdentifier(child),
385
+ path: generatedImport(languagePath, childPath, child, config),
386
+ };
387
+ });
388
+ files.push({
389
+ kind: 'language',
390
+ path: languagePath,
391
+ content: renderLanguageFile(toIdentifier(filenameValue), typeName, tree, languageImports, config.exportConfig.codeFormat),
392
+ });
393
+ });
394
+ });
395
+ const rootOwner = ownerByKey.get('');
396
+ if (rootOwner) {
397
+ const rootBasePath = basePaths.get('');
398
+ const indexPath = options.catalogFile || appendPath(appendPath(rootOwner.directory, config.translationsDirectory), 'index.ts');
399
+ files.push({
400
+ kind: 'index',
401
+ path: indexPath,
402
+ content: renderIndexFile(validBundle, typeNames.get(''), options.existingLanguageTypeName || 'I18nLanguage', config, indexPath, rootBasePath),
403
+ });
404
+ }
405
+ return files.sort((left, right) => left.path.localeCompare(right.path));
406
+ }