@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,1461 @@
1
+ // node_modules/@wads.dev/i18n-ts/dist/bundle/index.js
2
+ var BUNDLE_FORMAT = "wads-i18n-bundle";
3
+ var BUNDLE_VERSION = 2;
4
+ function isRecord(value) {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ function assertBundle(value) {
8
+ if (!isRecord(value))
9
+ throw new Error("The bundle must be a JSON object.");
10
+ if (value.format !== BUNDLE_FORMAT) {
11
+ throw new Error(`Invalid bundle format. Expected: ${BUNDLE_FORMAT}.`);
12
+ }
13
+ if (value.version !== BUNDLE_VERSION) {
14
+ throw new Error(`Unsupported bundle version: ${String(value.version)}.`);
15
+ }
16
+ if (!Number.isSafeInteger(value.updatedAt) || Number(value.updatedAt) < 0) {
17
+ throw new Error("The bundle must contain a valid numeric updatedAt value.");
18
+ }
19
+ if (!isRecord(value.languages)) {
20
+ throw new Error("The bundle must contain a languages object.");
21
+ }
22
+ for (const [key, language] of Object.entries(value.languages)) {
23
+ if (!isRecord(language) || !isRecord(language.translations)) {
24
+ throw new Error(`Language "${key}" must contain a translations object.`);
25
+ }
26
+ }
27
+ return value;
28
+ }
29
+ function withUpdatedAt(bundle, updatedAt = Date.now()) {
30
+ const validBundle = assertBundle(bundle);
31
+ return {
32
+ ...validBundle,
33
+ updatedAt: Math.max(updatedAt, validBundle.updatedAt + 1)
34
+ };
35
+ }
36
+
37
+ // src/web/bundle-storage.ts
38
+ var DATABASE_NAME = "wads-i18n-editor";
39
+ var DATABASE_VERSION = 1;
40
+ var STORE_NAME = "bundles";
41
+ var CURRENT_BUNDLE_KEY = "current";
42
+ var PROJECT_CONFIG_KEY = "project-config";
43
+ var LEGACY_VISUALIZATION_CONFIG_KEY = "visualization-config";
44
+ function requestResult(request) {
45
+ return new Promise((resolve, reject) => {
46
+ request.onsuccess = () => resolve(request.result);
47
+ request.onerror = () => reject(request.error);
48
+ });
49
+ }
50
+ function transactionComplete(transaction) {
51
+ return new Promise((resolve, reject) => {
52
+ transaction.oncomplete = () => resolve();
53
+ transaction.onerror = () => reject(transaction.error);
54
+ transaction.onabort = () => reject(transaction.error);
55
+ });
56
+ }
57
+ function openDatabase() {
58
+ return new Promise((resolve, reject) => {
59
+ const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
60
+ request.onupgradeneeded = () => {
61
+ if (!request.result.objectStoreNames.contains(STORE_NAME)) {
62
+ request.result.createObjectStore(STORE_NAME, { keyPath: "id" });
63
+ }
64
+ };
65
+ request.onsuccess = () => resolve(request.result);
66
+ request.onerror = () => reject(request.error);
67
+ });
68
+ }
69
+ async function loadStoredBundle() {
70
+ const database = await openDatabase();
71
+ try {
72
+ const transaction = database.transaction(STORE_NAME, "readonly");
73
+ const record = await requestResult(
74
+ transaction.objectStore(STORE_NAME).get(CURRENT_BUNDLE_KEY)
75
+ );
76
+ await transactionComplete(transaction);
77
+ return record ? assertBundle(record.bundle) : null;
78
+ } finally {
79
+ database.close();
80
+ }
81
+ }
82
+ async function saveStoredBundle(bundle) {
83
+ const database = await openDatabase();
84
+ try {
85
+ const transaction = database.transaction(STORE_NAME, "readwrite");
86
+ await requestResult(transaction.objectStore(STORE_NAME).put({ id: CURRENT_BUNDLE_KEY, bundle }));
87
+ await transactionComplete(transaction);
88
+ } finally {
89
+ database.close();
90
+ }
91
+ }
92
+ async function loadStoredProjectConfig() {
93
+ const database = await openDatabase();
94
+ try {
95
+ const transaction = database.transaction(STORE_NAME, "readonly");
96
+ const store = transaction.objectStore(STORE_NAME);
97
+ const record = await requestResult(store.get(PROJECT_CONFIG_KEY));
98
+ const legacyRecord = record ? void 0 : await requestResult(store.get(LEGACY_VISUALIZATION_CONFIG_KEY));
99
+ await transactionComplete(transaction);
100
+ return record?.config ?? legacyRecord?.config ?? null;
101
+ } finally {
102
+ database.close();
103
+ }
104
+ }
105
+ async function saveStoredProjectConfig(config) {
106
+ const database = await openDatabase();
107
+ try {
108
+ const transaction = database.transaction(STORE_NAME, "readwrite");
109
+ await requestResult(transaction.objectStore(STORE_NAME).put({ id: PROJECT_CONFIG_KEY, config }));
110
+ await transactionComplete(transaction);
111
+ } finally {
112
+ database.close();
113
+ }
114
+ }
115
+
116
+ // src/core/keyPath.ts
117
+ function isContainer(value) {
118
+ return value !== null && typeof value === "object";
119
+ }
120
+ function asRecord(value) {
121
+ return value;
122
+ }
123
+ function parseKeyPath(key, parameterName) {
124
+ if (key.trim() === "") {
125
+ throw new Error(`${parameterName} must be a non-empty key.`);
126
+ }
127
+ const segments = [];
128
+ const matcher = /(?:^|\.)([^.[\]]+)|\[(\d+)\]/g;
129
+ let match;
130
+ let matchedCharacters = "";
131
+ while ((match = matcher.exec(key)) !== null) {
132
+ segments.push(match[2] === void 0 ? match[1] : Number(match[2]));
133
+ matchedCharacters += match[0];
134
+ }
135
+ if (segments.length === 0 || matchedCharacters !== key) {
136
+ throw new Error(`${parameterName} contains an invalid path: ${key}`);
137
+ }
138
+ return segments;
139
+ }
140
+ function getKeyPathValue(root, segments) {
141
+ let current = root;
142
+ for (const segment of segments) {
143
+ if (!isContainer(current) || !Object.hasOwn(current, segment)) return { found: false };
144
+ current = asRecord(current)[segment];
145
+ }
146
+ return { found: true, value: current };
147
+ }
148
+ function deleteKeyPathValue(root, segments) {
149
+ const parentPath = segments.slice(0, -1);
150
+ const property = segments.at(-1);
151
+ let parent = root;
152
+ for (const segment of parentPath) {
153
+ const nested = asRecord(parent)[segment];
154
+ if (!isContainer(nested)) throw new Error(`The key path is invalid at ${String(segment)}.`);
155
+ parent = nested;
156
+ }
157
+ delete asRecord(parent)[property];
158
+ }
159
+ function setKeyPathValue(root, segments, value) {
160
+ const property = segments.at(-1);
161
+ let current = root;
162
+ segments.slice(0, -1).forEach((segment, index) => {
163
+ if (!Object.hasOwn(current, segment)) {
164
+ asRecord(current)[segment] = typeof segments[index + 1] === "number" ? [] : {};
165
+ }
166
+ const nested = asRecord(current)[segment];
167
+ if (!isContainer(nested)) {
168
+ throw new Error(`The destination key cannot be created inside ${String(segment)}.`);
169
+ }
170
+ current = nested;
171
+ });
172
+ asRecord(current)[property] = value;
173
+ }
174
+
175
+ // src/core/moveKey.ts
176
+ function cloneBundle(bundle) {
177
+ return structuredClone(bundle);
178
+ }
179
+ function moveKey(bundle, { sourceKey, targetKey }) {
180
+ const validBundle = assertBundle(bundle);
181
+ const sourceSegments = parseKeyPath(sourceKey, "sourceKey");
182
+ const targetSegments = parseKeyPath(targetKey, "targetKey");
183
+ if (sourceKey === targetKey) return cloneBundle(validBundle);
184
+ if (targetKey.startsWith(`${sourceKey}.`) || targetKey.startsWith(`${sourceKey}[`)) {
185
+ throw new Error("The destination key cannot be nested inside the source key.");
186
+ }
187
+ const valuesByLanguage = Object.entries(validBundle.languages).map(([languageKey, language]) => {
188
+ const source = getKeyPathValue(language.translations, sourceSegments);
189
+ if (!source.found) {
190
+ throw new Error(`The source key does not exist in language "${languageKey}".`);
191
+ }
192
+ if (getKeyPathValue(language.translations, targetSegments).found) {
193
+ throw new Error(`The destination key already exists in language "${languageKey}".`);
194
+ }
195
+ return [languageKey, source.value];
196
+ });
197
+ const nextBundle = cloneBundle(validBundle);
198
+ valuesByLanguage.forEach(([languageKey, value]) => {
199
+ const translations = nextBundle.languages[languageKey].translations;
200
+ deleteKeyPathValue(translations, sourceSegments);
201
+ setKeyPathValue(translations, targetSegments, value);
202
+ });
203
+ return nextBundle;
204
+ }
205
+
206
+ // node_modules/@wads.dev/i18n-ts/dist/config/index.js
207
+ var PROJECT_CONFIG_FORMAT = "wads-i18n-project-config";
208
+ var PROJECT_CONFIG_VERSION = 1;
209
+ function getDefaultLevelImport(levelIndex) {
210
+ if (levelIndex === 0)
211
+ return { path: "@/shared" };
212
+ if (levelIndex === 1)
213
+ return { path: "@/modules/{value}" };
214
+ if (levelIndex === 2)
215
+ return { path: "features/{value}" };
216
+ return { path: `level-${levelIndex}/{value}` };
217
+ }
218
+ function createDefaultProjectConfig() {
219
+ return {
220
+ format: PROJECT_CONFIG_FORMAT,
221
+ version: PROJECT_CONFIG_VERSION,
222
+ catalogFile: "src/shared/i18n/index.ts",
223
+ levelCount: 2,
224
+ levelNames: "Module, Feature",
225
+ levelImports: [
226
+ getDefaultLevelImport(0),
227
+ getDefaultLevelImport(1),
228
+ getDefaultLevelImport(2)
229
+ ],
230
+ exportConfig: {
231
+ importAliases: { "@/": "src/" },
232
+ codeFormat: {
233
+ useDoubleQuotes: false,
234
+ useSemicolons: true,
235
+ useShorthandProperties: true,
236
+ useTrailingCommas: true,
237
+ printWidth: 120,
238
+ maxObjectInlineItems: 4,
239
+ maxArrayInlineItems: 8,
240
+ objectLayout: "fit",
241
+ arrayLayout: "fit",
242
+ indentation: {
243
+ character: "space",
244
+ size: 2
245
+ }
246
+ }
247
+ },
248
+ translationsDirectory: "i18n",
249
+ languageFileTemplate: "{language}.ts",
250
+ languageReplacer: {},
251
+ deletion: {
252
+ ignoredExtensions: [],
253
+ autoDelete: false
254
+ }
255
+ };
256
+ }
257
+ function isRecord2(value) {
258
+ return value !== null && typeof value === "object" && !Array.isArray(value);
259
+ }
260
+ function normalizeReplacer(value) {
261
+ if (!isRecord2(value))
262
+ return {};
263
+ const result = {};
264
+ Object.entries(value).forEach(([key, replacement]) => {
265
+ if (replacement === null || typeof replacement === "string")
266
+ result[key] = replacement;
267
+ else if (isRecord2(replacement))
268
+ result[key] = normalizeReplacer(replacement);
269
+ });
270
+ return result;
271
+ }
272
+ function normalizeStringRecord(value) {
273
+ if (!isRecord2(value))
274
+ return {};
275
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string").map(([key, item]) => [key, item.trim()]));
276
+ }
277
+ function normalizeDeletionConfig(value) {
278
+ if (value === false)
279
+ return false;
280
+ const input = isRecord2(value) ? value : {};
281
+ const ignoredExtensions = Array.isArray(input.ignoredExtensions) ? [...new Set(input.ignoredExtensions.filter((extension) => typeof extension === "string").map((extension) => extension.trim().toLowerCase()).filter(Boolean).map((extension) => extension.startsWith(".") ? extension : `.${extension}`))] : [];
282
+ return {
283
+ ignoredExtensions,
284
+ autoDelete: input.autoDelete === true
285
+ };
286
+ }
287
+ function normalizeCodeFormat(value, legacyExportConfig) {
288
+ const defaults = createDefaultProjectConfig().exportConfig.codeFormat;
289
+ const input = isRecord2(value) ? value : {};
290
+ const indentation = isRecord2(input.indentation) ? input.indentation : {};
291
+ const rawIndentationSize = Number.parseInt(String(indentation.size ?? defaults.indentation.size), 10);
292
+ const rawPrintWidth = Number.parseInt(String(input.printWidth ?? defaults.printWidth), 10);
293
+ const rawMaxObjectInlineItems = Number.parseInt(String(input.maxObjectInlineItems ?? defaults.maxObjectInlineItems), 10);
294
+ const rawMaxArrayInlineItems = Number.parseInt(String(input.maxArrayInlineItems ?? defaults.maxArrayInlineItems), 10);
295
+ return {
296
+ useDoubleQuotes: (input.useDoubleQuotes ?? legacyExportConfig.useDoubleQuotes) === true,
297
+ useSemicolons: input.useSemicolons !== false,
298
+ useShorthandProperties: input.useShorthandProperties !== false,
299
+ useTrailingCommas: input.useTrailingCommas !== false,
300
+ printWidth: Number.isFinite(rawPrintWidth) ? Math.min(400, Math.max(40, rawPrintWidth)) : defaults.printWidth,
301
+ maxObjectInlineItems: Number.isFinite(rawMaxObjectInlineItems) ? Math.min(100, Math.max(0, rawMaxObjectInlineItems)) : defaults.maxObjectInlineItems,
302
+ maxArrayInlineItems: Number.isFinite(rawMaxArrayInlineItems) ? Math.min(100, Math.max(0, rawMaxArrayInlineItems)) : defaults.maxArrayInlineItems,
303
+ objectLayout: input.objectLayout === "multiline" ? "multiline" : "fit",
304
+ arrayLayout: input.arrayLayout === "multiline" ? "multiline" : "fit",
305
+ indentation: {
306
+ character: indentation.character === "tab" ? "tab" : "space",
307
+ size: Number.isFinite(rawIndentationSize) ? Math.min(8, Math.max(1, rawIndentationSize)) : defaults.indentation.size
308
+ }
309
+ };
310
+ }
311
+ function normalizeProjectConfig(value = {}) {
312
+ const defaults = createDefaultProjectConfig();
313
+ const levelCount = Math.max(0, Number.parseInt(String(value.levelCount ?? defaults.levelCount), 10) || 0);
314
+ const inputImports = Array.isArray(value.levelImports) ? value.levelImports : [];
315
+ const inputRecord = value;
316
+ const exportConfig = isRecord2(value.exportConfig) ? value.exportConfig : {};
317
+ return {
318
+ format: PROJECT_CONFIG_FORMAT,
319
+ version: PROJECT_CONFIG_VERSION,
320
+ catalogFile: typeof value.catalogFile === "string" && value.catalogFile.trim() ? value.catalogFile.trim() : defaults.catalogFile,
321
+ levelCount,
322
+ levelNames: typeof value.levelNames === "string" ? value.levelNames : defaults.levelNames,
323
+ levelImports: Array.from({ length: levelCount + 1 }, (_, index) => {
324
+ const input = isRecord2(inputImports[index]) ? inputImports[index] : {};
325
+ const fallback = getDefaultLevelImport(index);
326
+ return {
327
+ path: typeof input.path === "string" && input.path.trim() ? input.path.trim() : fallback.path,
328
+ valueReplacer: normalizeReplacer(input.valueReplacer),
329
+ fullReplacer: normalizeReplacer(input.fullReplacer)
330
+ };
331
+ }),
332
+ exportConfig: {
333
+ importAliases: {
334
+ ...defaults.exportConfig.importAliases,
335
+ ...normalizeStringRecord(exportConfig.importAliases ?? inputRecord.importAliases)
336
+ },
337
+ codeFormat: normalizeCodeFormat(exportConfig.codeFormat, exportConfig)
338
+ },
339
+ translationsDirectory: typeof value.translationsDirectory === "string" && value.translationsDirectory.trim() ? value.translationsDirectory.trim() : defaults.translationsDirectory,
340
+ languageFileTemplate: typeof value.languageFileTemplate === "string" && value.languageFileTemplate.trim() ? value.languageFileTemplate.trim() : defaults.languageFileTemplate,
341
+ languageReplacer: normalizeStringRecord(value.languageReplacer),
342
+ deletion: normalizeDeletionConfig(value.deletion)
343
+ };
344
+ }
345
+ function flattenPathReplacer(replacer, parentPath = "") {
346
+ const flattened = {};
347
+ Object.entries(replacer || {}).forEach(([key, value]) => {
348
+ const fullKey = parentPath ? `${parentPath}.${key}` : key;
349
+ if (value === null || typeof value === "string")
350
+ flattened[fullKey] = value;
351
+ else
352
+ Object.assign(flattened, flattenPathReplacer(value, fullKey));
353
+ });
354
+ return flattened;
355
+ }
356
+ function resolveImportAlias(path, aliases) {
357
+ const alias = Object.keys(aliases).filter((candidate) => path.startsWith(candidate)).sort((left, right) => right.length - left.length)[0];
358
+ if (!alias)
359
+ return path;
360
+ return `${aliases[alias]}${path.slice(alias.length)}`;
361
+ }
362
+
363
+ // src/core/projectConfig.ts
364
+ function isRecord3(value) {
365
+ return value !== null && typeof value === "object" && !Array.isArray(value);
366
+ }
367
+ function normalizeStringRecord2(value) {
368
+ if (!isRecord3(value)) return {};
369
+ return Object.fromEntries(
370
+ Object.entries(value).filter((entry) => typeof entry[1] === "string")
371
+ );
372
+ }
373
+ function normalizeCodeFormat2(value, legacyExportConfig) {
374
+ const input = isRecord3(value) ? value : {};
375
+ const indentation = isRecord3(input.indentation) ? input.indentation : {};
376
+ const indentationSize = Number.parseInt(String(indentation.size ?? 2), 10);
377
+ const printWidth = Number.parseInt(String(input.printWidth ?? 120), 10);
378
+ const maxObjectInlineItems = Number.parseInt(String(input.maxObjectInlineItems ?? 4), 10);
379
+ const maxArrayInlineItems = Number.parseInt(String(input.maxArrayInlineItems ?? 8), 10);
380
+ return {
381
+ useDoubleQuotes: (input.useDoubleQuotes ?? legacyExportConfig.useDoubleQuotes) === true,
382
+ useSemicolons: input.useSemicolons !== false,
383
+ useShorthandProperties: input.useShorthandProperties !== false,
384
+ useTrailingCommas: input.useTrailingCommas !== false,
385
+ printWidth: Number.isFinite(printWidth) ? Math.min(400, Math.max(40, printWidth)) : 120,
386
+ maxObjectInlineItems: Number.isFinite(maxObjectInlineItems) ? Math.min(100, Math.max(0, maxObjectInlineItems)) : 4,
387
+ maxArrayInlineItems: Number.isFinite(maxArrayInlineItems) ? Math.min(100, Math.max(0, maxArrayInlineItems)) : 8,
388
+ objectLayout: input.objectLayout === "multiline" ? "multiline" : "fit",
389
+ arrayLayout: input.arrayLayout === "multiline" ? "multiline" : "fit",
390
+ indentation: {
391
+ character: indentation.character === "tab" ? "tab" : "space",
392
+ size: Number.isFinite(indentationSize) ? Math.min(8, Math.max(1, indentationSize)) : 2
393
+ }
394
+ };
395
+ }
396
+ function normalizeDeletionConfig2(value) {
397
+ if (value === false) return false;
398
+ const input = isRecord3(value) ? value : {};
399
+ const ignoredExtensions = Array.isArray(input.ignoredExtensions) ? [...new Set(input.ignoredExtensions.filter((extension) => typeof extension === "string").map((extension) => extension.trim().toLowerCase()).filter(Boolean).map((extension) => extension.startsWith(".") ? extension : `.${extension}`))] : [];
400
+ return {
401
+ ignoredExtensions,
402
+ autoDelete: input.autoDelete === true
403
+ };
404
+ }
405
+ function normalizeEditorProjectConfig(value) {
406
+ const input = isRecord3(value) ? value : {};
407
+ const exportInput = isRecord3(input.exportConfig) ? input.exportConfig : {};
408
+ const importAliases = normalizeStringRecord2(exportInput.importAliases ?? input.importAliases);
409
+ const normalized = normalizeProjectConfig({
410
+ ...input,
411
+ importAliases
412
+ });
413
+ const normalizedRecord = normalized;
414
+ const normalizedExport = isRecord3(normalizedRecord.exportConfig) ? normalizedRecord.exportConfig : {};
415
+ const normalizedAliases = normalizeStringRecord2(normalizedExport.importAliases ?? normalizedRecord.importAliases);
416
+ const { importAliases: _legacyAliases, exportConfig: _normalizedExport, ...config } = normalizedRecord;
417
+ return {
418
+ ...config,
419
+ catalogFile: typeof input.catalogFile === "string" && input.catalogFile.trim() ? input.catalogFile.trim() : "src/shared/i18n/index.ts",
420
+ deletion: normalizeDeletionConfig2(input.deletion),
421
+ exportConfig: {
422
+ importAliases: normalizedAliases,
423
+ codeFormat: normalizeCodeFormat2(exportInput.codeFormat, exportInput)
424
+ }
425
+ };
426
+ }
427
+ function createDefaultEditorProjectConfig() {
428
+ const defaults = createDefaultProjectConfig();
429
+ const defaultsExport = isRecord3(defaults.exportConfig) ? defaults.exportConfig : {};
430
+ const importAliases = normalizeStringRecord2(defaultsExport.importAliases ?? defaults.importAliases);
431
+ const { importAliases: _legacyAliases, exportConfig: _defaultsExport, ...config } = defaults;
432
+ return {
433
+ ...config,
434
+ catalogFile: "src/shared/i18n/index.ts",
435
+ deletion: normalizeDeletionConfig2(void 0),
436
+ exportConfig: {
437
+ importAliases,
438
+ codeFormat: normalizeCodeFormat2(void 0, {})
439
+ }
440
+ };
441
+ }
442
+ function getEditorLevelName(config, level) {
443
+ if (level === 0) return "Root";
444
+ const names = config.levelNames.split(",").map((name) => name.trim()).filter(Boolean);
445
+ return names[level - 1] || `Level ${level}`;
446
+ }
447
+
448
+ // src/core/setStringValue.ts
449
+ function cloneBundle2(bundle) {
450
+ return structuredClone(bundle);
451
+ }
452
+ function setStringValue(bundle, { languageKey, key, value }) {
453
+ const validBundle = assertBundle(bundle);
454
+ const language = validBundle.languages[languageKey];
455
+ if (!language) {
456
+ throw new Error(`Language "${languageKey}" does not exist in the bundle.`);
457
+ }
458
+ const segments = parseKeyPath(key, "key");
459
+ const currentValue = getKeyPathValue(language.translations, segments);
460
+ if (!currentValue.found) {
461
+ throw new Error(`Key "${key}" does not exist in language "${languageKey}".`);
462
+ }
463
+ if (typeof currentValue.value !== "string") {
464
+ throw new Error("Editing functions and non-string values is not supported yet.");
465
+ }
466
+ const nextBundle = cloneBundle2(validBundle);
467
+ setKeyPathValue(nextBundle.languages[languageKey].translations, segments, value);
468
+ return nextBundle;
469
+ }
470
+
471
+ // src/core/exportPlan.ts
472
+ function getConfiguredLevelImport(config, level) {
473
+ return config.levelImports[level] || getDefaultLevelImport(level);
474
+ }
475
+ function appendPath(basePath, nextPath) {
476
+ return [basePath, nextPath].filter(Boolean).join("/").replaceAll(/\/+/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
477
+ }
478
+ function expandTemplate(template, value) {
479
+ return template.replaceAll("{value}", value);
480
+ }
481
+ function hasAlias(path, aliases) {
482
+ return Object.keys(aliases).some((alias) => path.startsWith(alias));
483
+ }
484
+ function getReplacement(replacer, fullKey, segment) {
485
+ const flattened = flattenPathReplacer(replacer);
486
+ if (Object.hasOwn(flattened, fullKey)) return { found: true, value: flattened[fullKey] };
487
+ if (Object.hasOwn(flattened, segment)) return { found: true, value: flattened[segment] };
488
+ return { found: false, value: void 0 };
489
+ }
490
+ function getTranslationOwnerChain(fullKey, config) {
491
+ const segments = fullKey.split(".");
492
+ const groupCount = Math.min(config.levelCount, Math.max(segments.length - 1, 0));
493
+ const rootImport = getConfiguredLevelImport(config, 0);
494
+ let directory = resolveImportAlias(expandTemplate(rootImport.path, ""), config.exportConfig.importAliases);
495
+ let consumedSegments = 0;
496
+ const owners = [{ directory, keyPath: "", importPathStyle: "relative" }];
497
+ for (let level = 1; level <= groupCount; level += 1) {
498
+ const segment = segments[level - 1];
499
+ const objectPath = segments.slice(0, level).join(".");
500
+ const levelImport = getConfiguredLevelImport(config, level);
501
+ const fullReplacement = getReplacement(levelImport.fullReplacer, objectPath, segment);
502
+ if (fullReplacement.found) {
503
+ if (fullReplacement.value !== null) {
504
+ directory = resolveImportAlias(fullReplacement.value, config.exportConfig.importAliases);
505
+ consumedSegments = level;
506
+ owners.push({
507
+ directory,
508
+ keyPath: objectPath,
509
+ importPathStyle: hasAlias(fullReplacement.value, config.exportConfig.importAliases) ? "alias" : "relative"
510
+ });
511
+ }
512
+ break;
513
+ }
514
+ const valueReplacement = getReplacement(levelImport.valueReplacer, objectPath, segment);
515
+ const value = typeof valueReplacement.value === "string" ? valueReplacement.value : segment;
516
+ const template = expandTemplate(levelImport.path, value);
517
+ const resolved = resolveImportAlias(template, config.exportConfig.importAliases);
518
+ directory = hasAlias(template, config.exportConfig.importAliases) || template.startsWith("/") ? resolved : appendPath(directory, resolved);
519
+ consumedSegments = level;
520
+ owners.push({
521
+ directory,
522
+ keyPath: objectPath,
523
+ importPathStyle: hasAlias(template, config.exportConfig.importAliases) ? "alias" : "relative"
524
+ });
525
+ }
526
+ void consumedSegments;
527
+ return owners;
528
+ }
529
+ function isFunctionDescriptor(value) {
530
+ return value !== null && typeof value === "object" && "$type" in value && value.$type === "function";
531
+ }
532
+ function collectTranslationKeys(value, path = "", keys = []) {
533
+ if (isFunctionDescriptor(value)) {
534
+ keys.push(path);
535
+ return keys;
536
+ }
537
+ if (Array.isArray(value)) {
538
+ keys.push(path);
539
+ return keys;
540
+ }
541
+ if (value && typeof value === "object") {
542
+ Object.entries(value).forEach(([key, nestedValue]) => {
543
+ collectTranslationKeys(nestedValue, path ? `${path}.${key}` : key, keys);
544
+ });
545
+ return keys;
546
+ }
547
+ keys.push(path);
548
+ return keys;
549
+ }
550
+ function buildTranslationOwners(bundle, config) {
551
+ const validBundle = assertBundle(bundle);
552
+ const validConfig = normalizeEditorProjectConfig(config);
553
+ const owners = /* @__PURE__ */ new Map();
554
+ const referenceLanguage = Object.values(validBundle.languages)[0];
555
+ if (referenceLanguage) {
556
+ collectTranslationKeys(referenceLanguage.translations).forEach((key) => {
557
+ getTranslationOwnerChain(key, validConfig).forEach((owner) => {
558
+ const existing = owners.get(owner.directory);
559
+ if (existing && existing.keyPath !== owner.keyPath) {
560
+ throw new Error(`The export path "${owner.directory}" resolves multiple translation owners.`);
561
+ }
562
+ owners.set(owner.directory, owner);
563
+ });
564
+ });
565
+ }
566
+ return [...owners.values()].sort((left, right) => left.keyPath.localeCompare(right.keyPath));
567
+ }
568
+ function buildExportPlan(bundle, config) {
569
+ const validBundle = assertBundle(bundle);
570
+ const validConfig = normalizeEditorProjectConfig(config);
571
+ const owners = buildTranslationOwners(validBundle, validConfig);
572
+ return owners.flatMap(({ directory, keyPath }) => {
573
+ const translationsDirectory = appendPath(directory, validConfig.translationsDirectory);
574
+ const structuralFiles = [
575
+ { kind: "base", path: appendPath(translationsDirectory, "base.ts") }
576
+ ];
577
+ if (keyPath === "") {
578
+ structuralFiles.push({
579
+ kind: "index",
580
+ path: validConfig.catalogFile || appendPath(translationsDirectory, "index.ts")
581
+ });
582
+ }
583
+ const languageFiles = Object.keys(validBundle.languages).map((languageKey) => {
584
+ const language = validConfig.languageReplacer[languageKey] || languageKey;
585
+ return {
586
+ kind: "language",
587
+ languageKey,
588
+ path: appendPath(
589
+ translationsDirectory,
590
+ validConfig.languageFileTemplate.replaceAll("{language}", language)
591
+ )
592
+ };
593
+ });
594
+ return [...structuralFiles, ...languageFiles];
595
+ }).sort((left, right) => left.path.localeCompare(right.path));
596
+ }
597
+
598
+ // src/web/export-preview.ts
599
+ var statusLabels = {
600
+ create: "new",
601
+ modify: "modified",
602
+ unchanged: "unchanged",
603
+ delete: "delete"
604
+ };
605
+ function createDiff(document2, diff) {
606
+ const pre = document2.createElement("pre");
607
+ pre.className = "export-file-diff";
608
+ diff.trimEnd().split("\n").forEach((line) => {
609
+ const row = document2.createElement("span");
610
+ row.className = line.startsWith("+++") || line.startsWith("---") ? "diff-file-header" : line.startsWith("@@") ? "diff-range" : line.startsWith("+") ? "diff-addition" : line.startsWith("-") ? "diff-removal" : "diff-context";
611
+ row.textContent = line;
612
+ pre.append(row);
613
+ });
614
+ return pre;
615
+ }
616
+ function renderExportPreview(container, bundle, config, checkedChanges = null) {
617
+ const document2 = container.ownerDocument;
618
+ if (!bundle) {
619
+ container.textContent = "Carregue um bundle para visualizar os arquivos de sa\xEDda.";
620
+ return;
621
+ }
622
+ const plan = checkedChanges ?? buildExportPlan(bundle, config);
623
+ if (plan.length === 0) {
624
+ container.textContent = "N\xE3o h\xE1 tradu\xE7\xF5es para exportar.";
625
+ return;
626
+ }
627
+ const list = document2.createElement("ul");
628
+ list.className = "export-preview-list";
629
+ plan.forEach(({ path, status, diff }) => {
630
+ const item = document2.createElement("li");
631
+ item.className = status ? `export-preview-item export-preview-${status}` : "export-preview-item";
632
+ const line = document2.createElement("div");
633
+ line.className = "export-preview-path";
634
+ if (status) {
635
+ const badge = document2.createElement("span");
636
+ badge.className = "export-preview-status";
637
+ badge.textContent = statusLabels[status];
638
+ line.append(badge);
639
+ }
640
+ const code = document2.createElement("code");
641
+ code.textContent = path;
642
+ line.append(code);
643
+ item.append(line);
644
+ if (diff && status !== "delete") {
645
+ const details = document2.createElement("details");
646
+ details.className = "export-diff-details";
647
+ const summary = document2.createElement("summary");
648
+ summary.textContent = "Mostrar diff";
649
+ details.append(summary, createDiff(document2, diff));
650
+ item.append(details);
651
+ }
652
+ list.append(item);
653
+ });
654
+ container.replaceChildren(list);
655
+ }
656
+
657
+ // src/web/project-settings.ts
658
+ function createJsonField(document2, { labelText, value, placeholder, onInput }) {
659
+ const label = document2.createElement("label");
660
+ label.className = "level-field level-json-field";
661
+ const title = document2.createElement("span");
662
+ title.textContent = labelText;
663
+ const textarea = document2.createElement("textarea");
664
+ textarea.spellcheck = false;
665
+ textarea.value = JSON.stringify(value || {}, null, 2);
666
+ textarea.placeholder = placeholder;
667
+ textarea.addEventListener("input", () => onInput(textarea.value));
668
+ label.append(title, textarea);
669
+ return label;
670
+ }
671
+ function renderLevelImportFields(container, config, onChange) {
672
+ const document2 = container.ownerDocument;
673
+ const fragment = document2.createDocumentFragment();
674
+ for (let index = 0; index <= config.levelCount; index += 1) {
675
+ const levelImport = config.levelImports[index] || getDefaultLevelImport(index);
676
+ const card = document2.createElement("section");
677
+ card.className = "level-import-card";
678
+ const heading = document2.createElement("h4");
679
+ heading.textContent = index === 0 ? "Root (n\xEDvel 0)" : `${getEditorLevelName(config, index)} (n\xEDvel ${index})`;
680
+ const pathLabel = document2.createElement("label");
681
+ pathLabel.className = "level-field level-path-field";
682
+ const pathTitle = document2.createElement("span");
683
+ pathTitle.textContent = "Template do caminho";
684
+ const pathInput = document2.createElement("input");
685
+ pathInput.type = "text";
686
+ pathInput.value = levelImport.path;
687
+ pathInput.placeholder = getDefaultLevelImport(index).path;
688
+ pathInput.addEventListener("input", () => onChange(index, "path", pathInput.value));
689
+ pathLabel.append(pathTitle, pathInput);
690
+ card.append(heading, pathLabel);
691
+ if (index > 0) {
692
+ const replacers = document2.createElement("div");
693
+ replacers.className = "level-replacers";
694
+ replacers.append(
695
+ createJsonField(document2, {
696
+ labelText: "Value replacer (JSON)",
697
+ value: levelImport.valueReplacer,
698
+ placeholder: '{ "module.feature": "folder-name" }',
699
+ onInput: (value) => onChange(index, "valueReplacer", value)
700
+ }),
701
+ createJsonField(document2, {
702
+ labelText: "Full replacer (JSON)",
703
+ value: levelImport.fullReplacer,
704
+ placeholder: '{ "module.feature": "@/custom/path" }',
705
+ onInput: (value) => onChange(index, "fullReplacer", value)
706
+ })
707
+ );
708
+ card.append(replacers);
709
+ }
710
+ fragment.append(card);
711
+ }
712
+ container.replaceChildren(fragment);
713
+ }
714
+
715
+ // src/web/project-api.ts
716
+ async function readResponse(response) {
717
+ const value = await response.json();
718
+ if (!response.ok) {
719
+ throw new Error("error" in value ? value.error : `Request failed with status ${response.status}.`);
720
+ }
721
+ return value;
722
+ }
723
+ async function getProjectInfo() {
724
+ const info = await readResponse(await fetch("/api/project", { cache: "no-store" }));
725
+ return {
726
+ ...info,
727
+ config: info.config ? normalizeEditorProjectConfig(info.config) : null,
728
+ bundle: info.bundle ? assertBundle(info.bundle) : null
729
+ };
730
+ }
731
+ async function generateProjectBundle() {
732
+ const result = await readResponse(await fetch("/api/bundle", { method: "POST" }));
733
+ return { ...result, bundle: assertBundle(result.bundle) };
734
+ }
735
+ async function checkProjectExport(bundle, config) {
736
+ return readResponse(await fetch("/api/export-preview", {
737
+ method: "POST",
738
+ headers: { "content-type": "application/json" },
739
+ body: JSON.stringify({ bundle, config })
740
+ }));
741
+ }
742
+
743
+ // src/web/state.ts
744
+ function createEditorState() {
745
+ let bundle = null;
746
+ const listeners = /* @__PURE__ */ new Set();
747
+ function notify(reason) {
748
+ listeners.forEach((listener) => listener(bundle, { reason }));
749
+ }
750
+ return {
751
+ getBundle: () => bundle,
752
+ replaceBundle(nextBundle) {
753
+ bundle = nextBundle;
754
+ notify("replace");
755
+ },
756
+ update(updater) {
757
+ if (!bundle) throw new Error("N\xE3o existe um bundle carregado para editar.");
758
+ bundle = withUpdatedAt(updater(bundle));
759
+ notify("edit");
760
+ },
761
+ subscribe(listener) {
762
+ listeners.add(listener);
763
+ return () => listeners.delete(listener);
764
+ }
765
+ };
766
+ }
767
+
768
+ // src/web/table.ts
769
+ function isFunctionDescriptor2(value) {
770
+ return value && typeof value === "object" && value.$type === "function";
771
+ }
772
+ function splitFunctionSource(source) {
773
+ const arrowIndex = source.indexOf("=>");
774
+ if (arrowIndex === -1) return { signature: "\u0192 function (\u2026) => \u2026", body: source };
775
+ return {
776
+ signature: `\u0192 ${source.slice(0, arrowIndex).trim()} => \u2026`,
777
+ body: source.slice(arrowIndex + 2).trim()
778
+ };
779
+ }
780
+ function describeBase(entries) {
781
+ const presentEntries = entries.filter(Boolean);
782
+ if (presentEntries.length === 0) return { label: "ausente", type: "missing" };
783
+ if (presentEntries.every((entry) => entry.type === "string")) return { label: "string", type: "string" };
784
+ if (presentEntries.every((entry) => entry.type === "function")) {
785
+ return { label: presentEntries[0].signature, type: "function" };
786
+ }
787
+ return { label: "inconsistente", type: "inconsistent" };
788
+ }
789
+ function appendSegment(path, segment) {
790
+ return segment.startsWith("[") ? `${path}${segment}` : path ? `${path}.${segment}` : segment;
791
+ }
792
+ function flatten(value, path = "", entries = /* @__PURE__ */ new Map()) {
793
+ if (isFunctionDescriptor2(value)) {
794
+ const { signature, body } = splitFunctionSource(value.source);
795
+ entries.set(path, { text: body, type: "function", signature });
796
+ return entries;
797
+ }
798
+ if (Array.isArray(value)) {
799
+ value.forEach((item, index) => flatten(item, appendSegment(path, `[${index}]`), entries));
800
+ return entries;
801
+ }
802
+ if (value && typeof value === "object") {
803
+ Object.entries(value).forEach(([key, nestedValue]) => flatten(nestedValue, appendSegment(path, key), entries));
804
+ return entries;
805
+ }
806
+ entries.set(path, { text: value == null ? "" : String(value), type: "string" });
807
+ return entries;
808
+ }
809
+ function createCell(document2, value, className) {
810
+ const cell = document2.createElement("td");
811
+ if (className) cell.className = className;
812
+ cell.textContent = value;
813
+ return cell;
814
+ }
815
+ function beginTextEdit(cell, originalValue, onCommit) {
816
+ let completed = false;
817
+ const input = cell.ownerDocument.createElement("input");
818
+ input.className = "inline-value-input";
819
+ input.type = "text";
820
+ input.value = originalValue;
821
+ input.setAttribute("aria-label", "Editar tradu\xE7\xE3o");
822
+ function finish(save) {
823
+ if (completed) return;
824
+ completed = true;
825
+ if (!save || input.value === originalValue) {
826
+ cell.textContent = originalValue;
827
+ return;
828
+ }
829
+ if (onCommit(input.value) === false) cell.textContent = originalValue;
830
+ }
831
+ input.addEventListener("keydown", (event) => {
832
+ if (event.key === "Enter") {
833
+ event.preventDefault();
834
+ finish(true);
835
+ }
836
+ if (event.key === "Escape") {
837
+ event.preventDefault();
838
+ finish(false);
839
+ }
840
+ });
841
+ input.addEventListener("blur", () => finish(true));
842
+ cell.replaceChildren(input);
843
+ input.focus();
844
+ input.select();
845
+ }
846
+ function createRows(bundle) {
847
+ const languages = Object.entries(bundle.languages);
848
+ const flattenedTranslations = Object.fromEntries(
849
+ languages.map(([key, language]) => [key, flatten(language.translations)])
850
+ );
851
+ const keys = [...new Set(Object.values(flattenedTranslations).flatMap((entries) => [...entries.keys()]))].sort((left, right) => left.localeCompare(right));
852
+ return {
853
+ languages,
854
+ rows: keys.map((fullKey) => ({
855
+ fullKey,
856
+ languageEntries: languages.map(([languageKey]) => flattenedTranslations[languageKey].get(fullKey))
857
+ }))
858
+ };
859
+ }
860
+ function createGroupTree(rows, levelCount) {
861
+ const root = { depth: 0, name: "Raiz", rows: [], children: /* @__PURE__ */ new Map() };
862
+ rows.forEach((row) => {
863
+ const segments = row.fullKey.split(".");
864
+ const groupCount = Math.min(levelCount, Math.max(segments.length - 1, 0));
865
+ let node = root;
866
+ for (let index = 0; index < groupCount; index += 1) {
867
+ const name = segments[index];
868
+ if (!node.children.has(name)) {
869
+ node.children.set(name, { depth: index + 1, name, rows: [], children: /* @__PURE__ */ new Map() });
870
+ }
871
+ node = node.children.get(name);
872
+ }
873
+ node.rows.push(row);
874
+ });
875
+ return root;
876
+ }
877
+ function getNodeRowCount(node) {
878
+ return node.rows.length + [...node.children.values()].reduce(
879
+ (count, child) => count + getNodeRowCount(child),
880
+ 0
881
+ );
882
+ }
883
+ function getRelativeKey(fullKey, depth) {
884
+ return fullKey.split(".").slice(depth).join(".");
885
+ }
886
+ function createTranslationTable(document2, languages, rows, depth, { onMoveKey, onEditValue }) {
887
+ const table = document2.createElement("table");
888
+ const header = table.createTHead().insertRow();
889
+ const keyHeader = document2.createElement("th");
890
+ keyHeader.textContent = "Chave";
891
+ header.append(keyHeader);
892
+ languages.forEach(([key, language]) => {
893
+ const cell = document2.createElement("th");
894
+ cell.className = "language-header";
895
+ cell.textContent = `${key} \xB7 ${language.short}`;
896
+ header.append(cell);
897
+ });
898
+ const baseHeader = document2.createElement("th");
899
+ baseHeader.textContent = "Base";
900
+ header.append(baseHeader);
901
+ const body = table.createTBody();
902
+ rows.forEach((rowData) => {
903
+ const row = body.insertRow();
904
+ const { fullKey, languageEntries } = rowData;
905
+ row.dataset.search = [
906
+ fullKey,
907
+ ...languageEntries.map((entry) => entry?.text ?? "")
908
+ ].join(" ").toLocaleLowerCase();
909
+ const keyCell = createCell(document2, getRelativeKey(fullKey, depth), "key");
910
+ if (onMoveKey) {
911
+ keyCell.title = "Clique duas vezes para mover esta chave";
912
+ keyCell.addEventListener("dblclick", () => onMoveKey(fullKey));
913
+ }
914
+ row.append(keyCell);
915
+ languages.forEach(([languageKey], index) => {
916
+ const entry = languageEntries[index];
917
+ const className = ["language-value", entry?.type === "function" ? "function-value" : ""].filter(Boolean).join(" ");
918
+ const valueCell = createCell(document2, entry?.text ?? "Ausente", className);
919
+ if (entry?.type === "string" && onEditValue) {
920
+ valueCell.title = "Clique duas vezes para editar esta tradu\xE7\xE3o";
921
+ valueCell.addEventListener("dblclick", () => {
922
+ beginTextEdit(valueCell, entry.text, (value) => onEditValue({
923
+ languageKey,
924
+ key: fullKey,
925
+ value
926
+ }));
927
+ });
928
+ }
929
+ row.append(valueCell);
930
+ });
931
+ const base = describeBase(languageEntries);
932
+ row.append(createCell(document2, base.label, `base-value base-${base.type}`));
933
+ });
934
+ return table;
935
+ }
936
+ function appendTableGroup(document2, container, node, languages, callbacks, isRoot = false) {
937
+ if (isRoot) {
938
+ if (node.rows.length > 0) {
939
+ const rootGroup = document2.createElement("section");
940
+ rootGroup.className = "table-group";
941
+ const title = document2.createElement("h3");
942
+ title.className = "table-group-title";
943
+ title.textContent = "Raiz";
944
+ rootGroup.append(title, createTranslationTable(document2, languages, node.rows, node.depth, callbacks));
945
+ container.append(rootGroup);
946
+ }
947
+ } else {
948
+ const details = document2.createElement("details");
949
+ details.className = "translation-group";
950
+ const summary = document2.createElement("summary");
951
+ const label = document2.createElement("span");
952
+ label.textContent = `${getEditorLevelName(callbacks.projectConfig, node.depth)} \xB7 ${node.name}`;
953
+ const count = document2.createElement("span");
954
+ count.className = "group-count";
955
+ count.textContent = `${getNodeRowCount(node)} chaves`;
956
+ summary.append(label, count);
957
+ details.append(summary);
958
+ const content = document2.createElement("div");
959
+ content.className = "translation-group-content";
960
+ if (node.rows.length > 0) {
961
+ const tableGroup = document2.createElement("section");
962
+ tableGroup.className = "table-group";
963
+ if (node.children.size > 0) {
964
+ const title = document2.createElement("h3");
965
+ title.className = "table-group-title";
966
+ title.textContent = "Raiz";
967
+ tableGroup.append(title);
968
+ }
969
+ tableGroup.append(createTranslationTable(document2, languages, node.rows, node.depth, callbacks));
970
+ content.append(tableGroup);
971
+ }
972
+ details.append(content);
973
+ container.append(details);
974
+ container = content;
975
+ }
976
+ [...node.children.values()].sort((left, right) => left.name.localeCompare(right.name)).forEach((child) => appendTableGroup(document2, container, child, languages, callbacks));
977
+ }
978
+ function renderTranslationTables(container, bundle, { projectConfig: projectConfig2, onMoveKey, onEditValue }) {
979
+ const document2 = container.ownerDocument;
980
+ const { languages, rows } = createRows(bundle);
981
+ const normalizedLevelCount = Math.max(0, Number.isInteger(projectConfig2?.levelCount) ? projectConfig2.levelCount : 0);
982
+ const tree = createGroupTree(rows, normalizedLevelCount);
983
+ const fragment = document2.createDocumentFragment();
984
+ appendTableGroup(document2, fragment, tree, languages, { projectConfig: projectConfig2, onMoveKey, onEditValue }, true);
985
+ container.replaceChildren(fragment);
986
+ return rows.length;
987
+ }
988
+
989
+ // src/web/view.ts
990
+ function createEditorView({ emptyState, tableWrap, tableContainer, summary, search, onMoveKey, onEditValue }) {
991
+ let currentRows = [];
992
+ let currentBundle = null;
993
+ let projectConfig2 = createDefaultEditorProjectConfig();
994
+ search.addEventListener("input", () => {
995
+ const query = search.value.trim().toLocaleLowerCase();
996
+ currentRows.forEach((row) => {
997
+ row.hidden = query !== "" && !row.dataset.search.includes(query);
998
+ });
999
+ });
1000
+ return {
1001
+ render(bundle) {
1002
+ currentBundle = bundle;
1003
+ if (!bundle) {
1004
+ emptyState.hidden = false;
1005
+ tableWrap.hidden = true;
1006
+ summary.textContent = "Carregue um bundle para visualizar as tradu\xE7\xF5es.";
1007
+ search.value = "";
1008
+ currentRows = [];
1009
+ return;
1010
+ }
1011
+ const keyCount = renderTranslationTables(tableContainer, bundle, {
1012
+ projectConfig: projectConfig2,
1013
+ onMoveKey,
1014
+ onEditValue
1015
+ });
1016
+ currentRows = [...tableContainer.querySelectorAll("tbody tr")];
1017
+ emptyState.hidden = true;
1018
+ tableWrap.hidden = false;
1019
+ search.value = "";
1020
+ summary.textContent = `${keyCount} chaves em ${Object.keys(bundle.languages).length} idiomas.`;
1021
+ },
1022
+ setProjectConfig(nextProjectConfig) {
1023
+ projectConfig2 = nextProjectConfig;
1024
+ if (currentBundle) this.render(currentBundle);
1025
+ }
1026
+ };
1027
+ }
1028
+
1029
+ // src/web/main.ts
1030
+ var state = createEditorState();
1031
+ function getElement(selector) {
1032
+ const element = document.querySelector(selector);
1033
+ if (!element) throw new Error(`Required editor element not found: ${selector}`);
1034
+ return element;
1035
+ }
1036
+ var elements = {
1037
+ emptyState: getElement("#empty-state"),
1038
+ feedback: getElement("#editor-feedback"),
1039
+ levelCount: getElement("#level-count"),
1040
+ levelNames: getElement("#level-names"),
1041
+ levelImports: getElement("#level-imports"),
1042
+ importAliases: getElement("#import-aliases"),
1043
+ translationsDirectory: getElement("#translations-directory"),
1044
+ languageFileTemplate: getElement("#language-file-template"),
1045
+ languageReplacer: getElement("#language-replacer"),
1046
+ useDoubleQuotes: getElement("#use-double-quotes"),
1047
+ useSemicolons: getElement("#use-semicolons"),
1048
+ useShorthandProperties: getElement("#use-shorthand-properties"),
1049
+ useTrailingCommas: getElement("#use-trailing-commas"),
1050
+ indentationCharacter: getElement("#indentation-character"),
1051
+ indentationSize: getElement("#indentation-size"),
1052
+ printWidth: getElement("#print-width"),
1053
+ maxObjectInlineItems: getElement("#max-object-inline-items"),
1054
+ maxArrayInlineItems: getElement("#max-array-inline-items"),
1055
+ objectLayout: getElement("#object-layout"),
1056
+ arrayLayout: getElement("#array-layout"),
1057
+ deletionEnabled: getElement("#deletion-enabled"),
1058
+ ignoredDeletionExtensions: getElement("#ignored-deletion-extensions"),
1059
+ autoDelete: getElement("#auto-delete"),
1060
+ exportPreview: getElement("#export-preview"),
1061
+ exportPreviewFeedback: getElement("#export-preview-feedback"),
1062
+ checkExportDiffs: getElement("#check-export-diffs"),
1063
+ search: getElement("#translation-search"),
1064
+ summary: getElement("#bundle-summary"),
1065
+ settingsPanel: getElement("#settings-panel"),
1066
+ tableWrap: getElement("#table-wrap"),
1067
+ toggleSettingsPanel: getElement("#toggle-settings-panel"),
1068
+ projectStatus: getElement("#project-status"),
1069
+ projectStatusText: getElement("#project-status-text"),
1070
+ retryProjectLoad: getElement("#retry-project-load")
1071
+ };
1072
+ var settingsPanelVisible = false;
1073
+ var projectConfig = createDefaultEditorProjectConfig();
1074
+ var checkedExportChanges = null;
1075
+ var exportPreviewRequestVersion = 0;
1076
+ function renderCurrentExportPreview() {
1077
+ renderExportPreview(elements.exportPreview, state.getBundle(), projectConfig, checkedExportChanges);
1078
+ }
1079
+ function invalidateExportPreview() {
1080
+ exportPreviewRequestVersion += 1;
1081
+ checkedExportChanges = null;
1082
+ elements.exportPreviewFeedback.textContent = "";
1083
+ elements.exportPreviewFeedback.classList.remove("error", "warning");
1084
+ elements.checkExportDiffs.disabled = !state.getBundle();
1085
+ elements.checkExportDiffs.textContent = "Verificar diffs";
1086
+ renderCurrentExportPreview();
1087
+ }
1088
+ var view = createEditorView({
1089
+ emptyState: elements.emptyState,
1090
+ tableWrap: elements.tableWrap,
1091
+ tableContainer: elements.tableWrap,
1092
+ summary: elements.summary,
1093
+ search: elements.search,
1094
+ onMoveKey(sourceKey) {
1095
+ const targetKey = window.prompt("Mover chave para:", sourceKey);
1096
+ if (targetKey === null || targetKey.trim() === "" || targetKey === sourceKey) return;
1097
+ try {
1098
+ state.update((bundle) => moveKey(bundle, { sourceKey, targetKey: targetKey.trim() }));
1099
+ setFeedback(`Chave movida de ${sourceKey} para ${targetKey.trim()}.`);
1100
+ } catch (error) {
1101
+ setFeedback(error instanceof Error ? error.message : String(error), true);
1102
+ }
1103
+ },
1104
+ onEditValue({ languageKey, key, value }) {
1105
+ try {
1106
+ state.update((bundle) => setStringValue(bundle, { languageKey, key, value }));
1107
+ setFeedback(`Tradu\xE7\xE3o ${languageKey}.${key} atualizada.`);
1108
+ return true;
1109
+ } catch (error) {
1110
+ setFeedback(error instanceof Error ? error.message : String(error), true);
1111
+ return false;
1112
+ }
1113
+ }
1114
+ });
1115
+ function setFeedback(message, isError = false) {
1116
+ elements.feedback.textContent = message;
1117
+ elements.feedback.classList.toggle("error", isError);
1118
+ }
1119
+ function setSettingsPanelVisibility(visible) {
1120
+ settingsPanelVisible = visible;
1121
+ elements.settingsPanel.hidden = !visible;
1122
+ elements.toggleSettingsPanel.setAttribute("aria-expanded", String(visible));
1123
+ elements.toggleSettingsPanel.setAttribute(
1124
+ "aria-label",
1125
+ visible ? "Ocultar configura\xE7\xF5es" : "Mostrar configura\xE7\xF5es"
1126
+ );
1127
+ elements.toggleSettingsPanel.title = elements.toggleSettingsPanel.getAttribute("aria-label");
1128
+ }
1129
+ var projectConfigQueue = Promise.resolve();
1130
+ function parseJsonObject(value, fieldName) {
1131
+ const parsed = JSON.parse(value);
1132
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1133
+ throw new Error(`${fieldName} deve ser um objeto JSON.`);
1134
+ }
1135
+ return parsed;
1136
+ }
1137
+ function applyProjectConfig(nextConfig, persist = true, renderLevelImports = true, syncJsonFields = true) {
1138
+ projectConfig = normalizeEditorProjectConfig(nextConfig);
1139
+ elements.levelCount.value = String(projectConfig.levelCount);
1140
+ elements.levelNames.value = projectConfig.levelNames;
1141
+ elements.translationsDirectory.value = projectConfig.translationsDirectory;
1142
+ elements.languageFileTemplate.value = projectConfig.languageFileTemplate;
1143
+ elements.deletionEnabled.checked = projectConfig.deletion !== false;
1144
+ elements.ignoredDeletionExtensions.value = projectConfig.deletion === false ? "" : projectConfig.deletion.ignoredExtensions.join(", ");
1145
+ elements.ignoredDeletionExtensions.disabled = projectConfig.deletion === false;
1146
+ elements.autoDelete.checked = projectConfig.deletion !== false && projectConfig.deletion.autoDelete;
1147
+ elements.autoDelete.disabled = projectConfig.deletion === false;
1148
+ const codeFormat = projectConfig.exportConfig.codeFormat;
1149
+ elements.useDoubleQuotes.checked = codeFormat.useDoubleQuotes;
1150
+ elements.useSemicolons.checked = codeFormat.useSemicolons;
1151
+ elements.useShorthandProperties.checked = codeFormat.useShorthandProperties;
1152
+ elements.useTrailingCommas.checked = codeFormat.useTrailingCommas;
1153
+ elements.indentationCharacter.value = codeFormat.indentation.character;
1154
+ elements.indentationSize.value = String(codeFormat.indentation.size);
1155
+ elements.printWidth.value = String(codeFormat.printWidth);
1156
+ elements.maxObjectInlineItems.value = String(codeFormat.maxObjectInlineItems);
1157
+ elements.maxArrayInlineItems.value = String(codeFormat.maxArrayInlineItems);
1158
+ elements.objectLayout.value = codeFormat.objectLayout;
1159
+ elements.arrayLayout.value = codeFormat.arrayLayout;
1160
+ if (syncJsonFields) {
1161
+ elements.importAliases.value = JSON.stringify(projectConfig.exportConfig.importAliases, null, 2);
1162
+ elements.languageReplacer.value = JSON.stringify(projectConfig.languageReplacer, null, 2);
1163
+ }
1164
+ if (renderLevelImports) {
1165
+ renderLevelImportFields(elements.levelImports, projectConfig, (index, field, value) => {
1166
+ try {
1167
+ const levelImports = projectConfig.levelImports.map((item) => ({ ...item }));
1168
+ levelImports[index][field] = field === "path" ? value : parseJsonObject(value, field === "valueReplacer" ? "Value replacer" : "Full replacer");
1169
+ applyProjectConfig({ ...projectConfig, levelImports }, true, false);
1170
+ setFeedback("Configura\xE7\xE3o atualizada.");
1171
+ } catch (error) {
1172
+ setFeedback(error instanceof Error ? error.message : String(error), true);
1173
+ }
1174
+ });
1175
+ }
1176
+ view.setProjectConfig(projectConfig);
1177
+ invalidateExportPreview();
1178
+ if (!persist) return;
1179
+ projectConfigQueue = projectConfigQueue.catch(() => void 0).then(() => saveStoredProjectConfig(projectConfig)).catch((error) => {
1180
+ setFeedback(`N\xE3o foi poss\xEDvel salvar as configura\xE7\xF5es: ${error instanceof Error ? error.message : String(error)}`, true);
1181
+ });
1182
+ }
1183
+ var persistenceQueue = Promise.resolve();
1184
+ function persistBundle(bundle) {
1185
+ persistenceQueue = persistenceQueue.catch(() => void 0).then(() => saveStoredBundle(bundle)).catch((error) => {
1186
+ setFeedback(`N\xE3o foi poss\xEDvel salvar a c\xF3pia local: ${error instanceof Error ? error.message : String(error)}`, true);
1187
+ });
1188
+ }
1189
+ state.subscribe((bundle) => {
1190
+ view.render(bundle);
1191
+ invalidateExportPreview();
1192
+ if (bundle) persistBundle(bundle);
1193
+ });
1194
+ view.render(state.getBundle());
1195
+ elements.toggleSettingsPanel.addEventListener("click", () => {
1196
+ setSettingsPanelVisibility(!settingsPanelVisible);
1197
+ });
1198
+ elements.checkExportDiffs.addEventListener("click", async () => {
1199
+ const bundle = state.getBundle();
1200
+ if (!bundle) return;
1201
+ const requestVersion = ++exportPreviewRequestVersion;
1202
+ elements.checkExportDiffs.disabled = true;
1203
+ elements.checkExportDiffs.textContent = "Verificando\u2026";
1204
+ elements.exportPreviewFeedback.textContent = "Comparando com os arquivos atuais do projeto\u2026";
1205
+ elements.exportPreviewFeedback.classList.remove("error", "warning");
1206
+ try {
1207
+ const result = await checkProjectExport(bundle, projectConfig);
1208
+ if (requestVersion !== exportPreviewRequestVersion) return;
1209
+ checkedExportChanges = result.changes;
1210
+ renderCurrentExportPreview();
1211
+ const changed = result.changes.filter(({ status }) => status !== "unchanged").length;
1212
+ const deletionCount = result.changes.filter(({ status }) => status === "delete").length;
1213
+ if (deletionCount > 0) {
1214
+ const deletionMessage = projectConfig.deletion !== false && projectConfig.deletion.autoDelete ? `${deletionCount} arquivo${deletionCount === 1 ? "" : "s"} divergente${deletionCount === 1 ? "" : "s"} ${deletionCount === 1 ? "ser\xE1 exclu\xEDdo" : "ser\xE3o exclu\xEDdos"} durante o export porque autoDelete est\xE1 ativo.` : `${deletionCount} candidato${deletionCount === 1 ? "" : "s"} \xE0 exclus\xE3o ${deletionCount === 1 ? "ser\xE1 preservado" : "ser\xE3o preservados"} sem i18n-edit export --delete.`;
1215
+ elements.exportPreviewFeedback.textContent = `${changed} arquivo${changed === 1 ? "" : "s"} com altera\xE7\xF5es planejadas. Aten\xE7\xE3o: ${deletionMessage}`;
1216
+ elements.exportPreviewFeedback.classList.add("warning");
1217
+ } else {
1218
+ elements.exportPreviewFeedback.textContent = changed === 0 ? "Os arquivos do projeto j\xE1 correspondem \xE0 pr\xE9via." : `${changed} arquivo${changed === 1 ? "" : "s"} com altera\xE7\xF5es planejadas.`;
1219
+ }
1220
+ } catch (error) {
1221
+ if (requestVersion !== exportPreviewRequestVersion) return;
1222
+ elements.exportPreviewFeedback.textContent = `N\xE3o foi poss\xEDvel verificar os diffs: ${error instanceof Error ? error.message : String(error)}`;
1223
+ elements.exportPreviewFeedback.classList.add("error");
1224
+ } finally {
1225
+ if (requestVersion === exportPreviewRequestVersion) {
1226
+ elements.checkExportDiffs.disabled = false;
1227
+ elements.checkExportDiffs.textContent = "Verificar novamente";
1228
+ }
1229
+ }
1230
+ });
1231
+ elements.levelCount.addEventListener("input", () => {
1232
+ applyProjectConfig({
1233
+ ...projectConfig,
1234
+ levelCount: elements.levelCount.value
1235
+ });
1236
+ });
1237
+ elements.levelNames.addEventListener("input", () => {
1238
+ applyProjectConfig({
1239
+ ...projectConfig,
1240
+ levelNames: elements.levelNames.value
1241
+ });
1242
+ });
1243
+ elements.translationsDirectory.addEventListener("input", () => {
1244
+ applyProjectConfig({ ...projectConfig, translationsDirectory: elements.translationsDirectory.value });
1245
+ });
1246
+ elements.languageFileTemplate.addEventListener("input", () => {
1247
+ applyProjectConfig({ ...projectConfig, languageFileTemplate: elements.languageFileTemplate.value });
1248
+ });
1249
+ elements.importAliases.addEventListener("input", () => {
1250
+ try {
1251
+ const importAliases = parseJsonObject(elements.importAliases.value, "Aliases de import");
1252
+ applyProjectConfig({
1253
+ ...projectConfig,
1254
+ exportConfig: { ...projectConfig.exportConfig, importAliases }
1255
+ }, true, false, false);
1256
+ setFeedback("Aliases atualizados.");
1257
+ } catch (error) {
1258
+ setFeedback(error instanceof Error ? error.message : String(error), true);
1259
+ }
1260
+ });
1261
+ elements.useDoubleQuotes.addEventListener("change", () => {
1262
+ applyProjectConfig({
1263
+ ...projectConfig,
1264
+ exportConfig: {
1265
+ ...projectConfig.exportConfig,
1266
+ codeFormat: {
1267
+ ...projectConfig.exportConfig.codeFormat,
1268
+ useDoubleQuotes: elements.useDoubleQuotes.checked
1269
+ }
1270
+ }
1271
+ });
1272
+ setFeedback(elements.useDoubleQuotes.checked ? "A exporta\xE7\xE3o usar\xE1 aspas duplas." : "A exporta\xE7\xE3o usar\xE1 aspas simples.");
1273
+ });
1274
+ elements.useSemicolons.addEventListener("change", () => {
1275
+ applyProjectConfig({
1276
+ ...projectConfig,
1277
+ exportConfig: {
1278
+ ...projectConfig.exportConfig,
1279
+ codeFormat: {
1280
+ ...projectConfig.exportConfig.codeFormat,
1281
+ useSemicolons: elements.useSemicolons.checked
1282
+ }
1283
+ }
1284
+ });
1285
+ setFeedback(elements.useSemicolons.checked ? "A exporta\xE7\xE3o usar\xE1 ponto e v\xEDrgula." : "A exporta\xE7\xE3o n\xE3o usar\xE1 ponto e v\xEDrgula.");
1286
+ });
1287
+ elements.useShorthandProperties.addEventListener("change", () => {
1288
+ applyProjectConfig({
1289
+ ...projectConfig,
1290
+ exportConfig: {
1291
+ ...projectConfig.exportConfig,
1292
+ codeFormat: {
1293
+ ...projectConfig.exportConfig.codeFormat,
1294
+ useShorthandProperties: elements.useShorthandProperties.checked
1295
+ }
1296
+ }
1297
+ });
1298
+ setFeedback("Prefer\xEAncia de propriedades shorthand atualizada.");
1299
+ });
1300
+ elements.useTrailingCommas.addEventListener("change", () => {
1301
+ applyProjectConfig({
1302
+ ...projectConfig,
1303
+ exportConfig: {
1304
+ ...projectConfig.exportConfig,
1305
+ codeFormat: {
1306
+ ...projectConfig.exportConfig.codeFormat,
1307
+ useTrailingCommas: elements.useTrailingCommas.checked
1308
+ }
1309
+ }
1310
+ });
1311
+ setFeedback("Prefer\xEAncia de v\xEDrgula final atualizada.");
1312
+ });
1313
+ function updateIndentation() {
1314
+ applyProjectConfig({
1315
+ ...projectConfig,
1316
+ exportConfig: {
1317
+ ...projectConfig.exportConfig,
1318
+ codeFormat: {
1319
+ ...projectConfig.exportConfig.codeFormat,
1320
+ indentation: {
1321
+ character: elements.indentationCharacter.value,
1322
+ size: elements.indentationSize.value
1323
+ }
1324
+ }
1325
+ }
1326
+ });
1327
+ setFeedback("Indenta\xE7\xE3o da exporta\xE7\xE3o atualizada.");
1328
+ }
1329
+ elements.indentationCharacter.addEventListener("change", updateIndentation);
1330
+ elements.indentationSize.addEventListener("input", updateIndentation);
1331
+ elements.printWidth.addEventListener("input", () => {
1332
+ applyProjectConfig({
1333
+ ...projectConfig,
1334
+ exportConfig: {
1335
+ ...projectConfig.exportConfig,
1336
+ codeFormat: {
1337
+ ...projectConfig.exportConfig.codeFormat,
1338
+ printWidth: elements.printWidth.value
1339
+ }
1340
+ }
1341
+ });
1342
+ setFeedback("Largura m\xE1xima da exporta\xE7\xE3o atualizada.");
1343
+ });
1344
+ function updateInlineItemLimits() {
1345
+ applyProjectConfig({
1346
+ ...projectConfig,
1347
+ exportConfig: {
1348
+ ...projectConfig.exportConfig,
1349
+ codeFormat: {
1350
+ ...projectConfig.exportConfig.codeFormat,
1351
+ maxObjectInlineItems: elements.maxObjectInlineItems.value,
1352
+ maxArrayInlineItems: elements.maxArrayInlineItems.value
1353
+ }
1354
+ }
1355
+ });
1356
+ setFeedback("Limites de cole\xE7\xF5es inline atualizados.");
1357
+ }
1358
+ elements.maxObjectInlineItems.addEventListener("input", updateInlineItemLimits);
1359
+ elements.maxArrayInlineItems.addEventListener("input", updateInlineItemLimits);
1360
+ function updateCollectionLayouts() {
1361
+ applyProjectConfig({
1362
+ ...projectConfig,
1363
+ exportConfig: {
1364
+ ...projectConfig.exportConfig,
1365
+ codeFormat: {
1366
+ ...projectConfig.exportConfig.codeFormat,
1367
+ objectLayout: elements.objectLayout.value,
1368
+ arrayLayout: elements.arrayLayout.value
1369
+ }
1370
+ }
1371
+ });
1372
+ setFeedback("Layout de objetos e arrays atualizado.");
1373
+ }
1374
+ elements.objectLayout.addEventListener("change", updateCollectionLayouts);
1375
+ elements.arrayLayout.addEventListener("change", updateCollectionLayouts);
1376
+ elements.languageReplacer.addEventListener("input", () => {
1377
+ try {
1378
+ const languageReplacer = parseJsonObject(elements.languageReplacer.value, "Replacer dos idiomas");
1379
+ applyProjectConfig({ ...projectConfig, languageReplacer }, true, false, false);
1380
+ setFeedback("Replacer dos idiomas atualizado.");
1381
+ } catch (error) {
1382
+ setFeedback(error instanceof Error ? error.message : String(error), true);
1383
+ }
1384
+ });
1385
+ elements.deletionEnabled.addEventListener("change", () => {
1386
+ applyProjectConfig({
1387
+ ...projectConfig,
1388
+ deletion: elements.deletionEnabled.checked ? { ignoredExtensions: [], autoDelete: false } : false
1389
+ });
1390
+ setFeedback(elements.deletionEnabled.checked ? "Detec\xE7\xE3o de arquivos divergentes ativada." : "Detec\xE7\xE3o e avisos de exclus\xE3o desativados.");
1391
+ });
1392
+ elements.ignoredDeletionExtensions.addEventListener("input", () => {
1393
+ if (projectConfig.deletion === false) return;
1394
+ applyProjectConfig({
1395
+ ...projectConfig,
1396
+ deletion: {
1397
+ ...projectConfig.deletion,
1398
+ ignoredExtensions: elements.ignoredDeletionExtensions.value.split(",")
1399
+ }
1400
+ });
1401
+ setFeedback("Extens\xF5es ignoradas atualizadas.");
1402
+ });
1403
+ elements.autoDelete.addEventListener("change", () => {
1404
+ if (projectConfig.deletion === false) return;
1405
+ applyProjectConfig({
1406
+ ...projectConfig,
1407
+ deletion: { ...projectConfig.deletion, autoDelete: elements.autoDelete.checked }
1408
+ });
1409
+ setFeedback(elements.autoDelete.checked ? "Exclus\xE3o autom\xE1tica ativada para o projeto." : "Exclus\xF5es voltar\xE3o a exigir a op\xE7\xE3o --delete.");
1410
+ });
1411
+ function setProjectStatus(message, isError = false) {
1412
+ elements.projectStatus.hidden = false;
1413
+ elements.projectStatus.classList.toggle("error", isError);
1414
+ elements.projectStatus.classList.toggle("loading", !isError);
1415
+ elements.projectStatusText.textContent = message;
1416
+ elements.retryProjectLoad.hidden = !isError;
1417
+ }
1418
+ function hideProjectStatus() {
1419
+ elements.projectStatus.hidden = true;
1420
+ }
1421
+ async function loadProject() {
1422
+ setProjectStatus("Conectando ao projeto\u2026");
1423
+ try {
1424
+ const [info, storedBundle, storedConfig] = await Promise.all([
1425
+ getProjectInfo(),
1426
+ loadStoredBundle(),
1427
+ loadStoredProjectConfig()
1428
+ ]);
1429
+ if (info.config) {
1430
+ applyProjectConfig(info.config, false);
1431
+ } else if (storedConfig && Array.isArray(storedConfig.levelImports)) {
1432
+ applyProjectConfig(storedConfig, false);
1433
+ }
1434
+ let projectBundle = info.bundle;
1435
+ let generated = false;
1436
+ if (!projectBundle) {
1437
+ if (!info.canGenerateBundle) {
1438
+ throw new Error("O projeto n\xE3o possui uma bundle e o cat\xE1logo TypeScript n\xE3o foi encontrado.");
1439
+ }
1440
+ setProjectStatus("Gerando a bundle do projeto\u2026");
1441
+ projectBundle = (await generateProjectBundle()).bundle;
1442
+ generated = true;
1443
+ }
1444
+ const projectIsNewer = storedBundle && projectBundle.updatedAt > storedBundle.updatedAt;
1445
+ const selectedBundle = !storedBundle || projectIsNewer ? projectBundle : storedBundle;
1446
+ if (projectIsNewer && !generated) {
1447
+ window.alert("Existe uma bundle mais recente no projeto. Ela foi carregada e substituir\xE1 a c\xF3pia local do navegador.");
1448
+ }
1449
+ state.replaceBundle(selectedBundle);
1450
+ setFeedback(generated ? "Bundle gerada automaticamente a partir do cat\xE1logo do projeto." : selectedBundle === storedBundle ? "Vers\xE3o mais recente restaurada do navegador." : "Bundle do projeto carregada automaticamente.");
1451
+ hideProjectStatus();
1452
+ } catch (error) {
1453
+ setProjectStatus(
1454
+ `N\xE3o foi poss\xEDvel carregar o projeto: ${error instanceof Error ? error.message : String(error)}`,
1455
+ true
1456
+ );
1457
+ }
1458
+ }
1459
+ applyProjectConfig(projectConfig, false);
1460
+ elements.retryProjectLoad.addEventListener("click", () => void loadProject());
1461
+ void loadProject();