gt 2.20.4 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/collectUserEditDiffs.d.ts +2 -0
  3. package/dist/api/collectUserEditDiffs.js +31 -8
  4. package/dist/api/collectUserEditDiffs.js.map +1 -1
  5. package/dist/api/downloadFileBatch.js +13 -6
  6. package/dist/api/downloadFileBatch.js.map +1 -1
  7. package/dist/cli/commands/upload.js +30 -11
  8. package/dist/cli/commands/upload.js.map +1 -1
  9. package/dist/console/index.d.ts +4 -0
  10. package/dist/console/index.js +2 -1
  11. package/dist/console/index.js.map +1 -1
  12. package/dist/formats/files/aggregateFiles.js +39 -2
  13. package/dist/formats/files/aggregateFiles.js.map +1 -1
  14. package/dist/formats/files/localeContent.d.ts +18 -0
  15. package/dist/formats/files/localeContent.js +34 -0
  16. package/dist/formats/files/localeContent.js.map +1 -0
  17. package/dist/formats/files/supportedFiles.d.ts +2 -1
  18. package/dist/formats/files/supportedFiles.js +4 -2
  19. package/dist/formats/files/supportedFiles.js.map +1 -1
  20. package/dist/formats/files/transformFormat.d.ts +2 -0
  21. package/dist/formats/files/transformFormat.js +6 -3
  22. package/dist/formats/files/transformFormat.js.map +1 -1
  23. package/dist/formats/xcstrings/mergeXcstrings.d.ts +13 -0
  24. package/dist/formats/xcstrings/mergeXcstrings.js +71 -0
  25. package/dist/formats/xcstrings/mergeXcstrings.js.map +1 -0
  26. package/dist/formats/xcstrings/parseXcstrings.d.ts +43 -0
  27. package/dist/formats/xcstrings/parseXcstrings.js +123 -0
  28. package/dist/formats/xcstrings/parseXcstrings.js.map +1 -0
  29. package/dist/fs/config/parseFilesConfig.d.ts +2 -2
  30. package/dist/fs/config/parseFilesConfig.js +17 -3
  31. package/dist/fs/config/parseFilesConfig.js.map +1 -1
  32. package/dist/generated/version.d.ts +1 -1
  33. package/dist/generated/version.js +1 -1
  34. package/dist/generated/version.js.map +1 -1
  35. package/dist/state/recentDownloads.d.ts +1 -1
  36. package/dist/state/recentDownloads.js +3 -1
  37. package/dist/state/recentDownloads.js.map +1 -1
  38. package/dist/utils/persistPostprocessHashes.d.ts +1 -1
  39. package/dist/utils/persistPostprocessHashes.js +18 -14
  40. package/dist/utils/persistPostprocessHashes.js.map +1 -1
  41. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2254](https://github.com/generaltranslation/gt/pull/2254) [`8e59be2`](https://github.com/generaltranslation/gt/commit/8e59be28c129b4b9509ce8897156de3282b2a295) Thanks [@eoinest](https://github.com/eoinest)! - Add Apple `.xcstrings` catalog upload support to the CLI. An `.xcstrings` catalog holds every locale in one file; on upload the CLI extracts a source-only slice (only the catalog's `sourceLanguage` localization per entry) and uploads that as the source document. The slice is serialized with a pinned byte layout and hashed into its `versionId`, so an unchanged catalog re-slices byte-identically and does not re-upload. Configure it under `files.xcstrings` in `gt.config.json`; patterns without `[locale]` are expected because the catalog is shared across locales.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#2254](https://github.com/generaltranslation/gt/pull/2254) [`e3aae42`](https://github.com/generaltranslation/gt/commit/e3aae423660e8d9116a08afc337ff3b587786a96) Thanks [@eoinest](https://github.com/eoinest)! - Record download bookkeeping per locale for translation files that hold every locale, so every locale keeps its post-process hash and unchanged in-place files are no longer resubmitted as user edits by save-local.
12
+
13
+ - Updated dependencies [[`8e59be2`](https://github.com/generaltranslation/gt/commit/8e59be28c129b4b9509ce8897156de3282b2a295)]:
14
+ - generaltranslation@9.3.0
15
+ - @generaltranslation/python-extractor@0.2.48
16
+ - @generaltranslation/supported-locales@2.1.28
17
+ - @generaltranslation/vue-extractor@0.1.8
18
+
3
19
  ## 2.20.4
4
20
 
5
21
  ### Patch Changes
@@ -3,6 +3,8 @@ import { FileReference } from 'generaltranslation/types';
3
3
  /**
4
4
  * Collects local user edits by diffing the latest downloaded server translation version
5
5
  * against the current local translation file, and submits the diffs upstream.
6
+ * A file that holds every locale is compared one locale slice at a time, so an
7
+ * edit to one locale is submitted under that locale alone.
6
8
  *
7
9
  * Must run before enqueueing new translations so rules are available to the generator.
8
10
  */
@@ -8,6 +8,7 @@ import { recordWarning } from "../state/translateWarnings.js";
8
8
  import { readLockfile } from "../fs/config/downloadedVersions.js";
9
9
  import { createFileMapping } from "../formats/files/fileMapping.js";
10
10
  import { getGitUnifiedDiff } from "../utils/gitDiff.js";
11
+ import { emptyLocaleContent, localeContent } from "../formats/files/localeContent.js";
11
12
  import { extractYaml } from "../formats/yaml/extractYaml.js";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import * as path$1 from "node:path";
@@ -34,6 +35,8 @@ const findLatestDownloadedVersion = (entryMap, fileId, locale) => {
34
35
  /**
35
36
  * Collects local user edits by diffing the latest downloaded server translation version
36
37
  * against the current local translation file, and submits the diffs upstream.
38
+ * A file that holds every locale is compared one locale slice at a time, so an
39
+ * edit to one locale is submitted under that locale alone.
37
40
  *
38
41
  * Must run before enqueueing new translations so rules are available to the generator.
39
42
  */
@@ -53,7 +56,8 @@ async function collectAndSendUserEditDiffs(files, settings) {
53
56
  if (!latestDownloaded) continue;
54
57
  const downloadedVersion = latestDownloaded.entry;
55
58
  if (downloadedVersion.postProcessHash) try {
56
- if (hashStringSync(readFileContent(outputPath, uploadedFile.fileFormat)) === downloadedVersion.postProcessHash) continue;
59
+ const localFile = readFileContent(outputPath, uploadedFile.fileFormat);
60
+ if (hashStringSync(localeContent(localFile, uploadedFile.fileFormat, locale) ?? emptyLocaleContent(localFile, uploadedFile.fileFormat)) === downloadedVersion.postProcessHash) continue;
57
61
  } catch {}
58
62
  candidates.push({
59
63
  branchId: uploadedFile.branchId,
@@ -74,22 +78,41 @@ async function collectAndSendUserEditDiffs(files, settings) {
74
78
  branchId: c.branchId
75
79
  }));
76
80
  const translatedFiles = (await api.queryFileData({ translatedFiles: fileQueryData })).translatedFiles?.filter((t) => t.completedAt) ?? [];
77
- const serverContentByKey = /* @__PURE__ */ new Map();
81
+ const serverPayloadByKey = /* @__PURE__ */ new Map();
78
82
  try {
79
- const files = (await api.downloadFileBatch(translatedFiles.map((file) => ({
83
+ const resp = await api.downloadFileBatch(translatedFiles.map((file) => ({
80
84
  branchId: file.branchId,
81
85
  fileId: file.fileId,
82
86
  locale: file.locale,
83
87
  versionId: file.versionId
84
- }))))?.files || [];
85
- for (const f of files) serverContentByKey.set(`${f.branchId}:${f.fileId}:${f.versionId}:${f.locale}`, contentBytes(f.data, f.fileFormat));
88
+ })));
89
+ for (const f of resp?.files || []) {
90
+ if (!f.locale) continue;
91
+ serverPayloadByKey.set(`${f.branchId}:${f.fileId}:${f.versionId}:${f.locale}`, {
92
+ data: f.data,
93
+ fileFormat: f.fileFormat
94
+ });
95
+ }
86
96
  } catch {}
87
97
  for (const c of candidates) {
88
98
  const key = `${c.branchId}:${c.fileId}:${c.versionId}:${c.locale}`;
89
- const serverBytes = serverContentByKey.get(key);
90
- if (!serverBytes) continue;
99
+ const payload = serverPayloadByKey.get(key);
100
+ if (!payload) continue;
101
+ let serverContent;
91
102
  try {
92
- const localBytes = contentBytes(readFileContent(c.outputPath, c.fileFormat), c.fileFormat);
103
+ serverContent = localeContent(payload.data, payload.fileFormat, c.locale) ?? emptyLocaleContent(payload.data, payload.fileFormat);
104
+ } catch (error) {
105
+ const relativePath = getRelative(c.outputPath);
106
+ const reason = `The downloaded ${c.locale} translation could not be read (${error instanceof Error ? error.message : String(error)})`;
107
+ logger.warn(`Skipping local edits to ${relativePath}: ${reason}`);
108
+ recordWarning("skipped_file", relativePath, reason);
109
+ continue;
110
+ }
111
+ const serverBytes = contentBytes(serverContent, payload.fileFormat);
112
+ try {
113
+ const local = localeContent(readFileContent(c.outputPath, c.fileFormat), c.fileFormat, c.locale);
114
+ if (local === void 0) continue;
115
+ const localBytes = contentBytes(local, c.fileFormat);
93
116
  if (localBytes.equals(serverBytes)) continue;
94
117
  if (isBinaryFileFormat(c.fileFormat)) {
95
118
  const relativePath = getRelative(c.outputPath);
@@ -1 +1 @@
1
- {"version":3,"file":"collectUserEditDiffs.js","names":["path","fs"],"sources":["../../src/api/collectUserEditDiffs.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n readLockfile,\n EntryMap,\n DownloadedTranslation,\n} from '../fs/config/downloadedVersions.js';\nimport { Settings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport { getGitUnifiedDiff } from '../utils/gitDiff.js';\nimport { api } from '../utils/api.js';\nimport {\n FileReference,\n isBinaryFileFormat,\n SubmitUserEditDiff,\n type FileFormat,\n} from 'generaltranslation/types';\nimport { readFileContent } from '../fs/fileContent.js';\nimport os from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport { hashStringSync } from '../utils/hash.js';\nimport { extractJson } from '../formats/json/extractJson.js';\nimport { extractYaml } from '../formats/yaml/extractYaml.js';\nimport { logger } from '../console/logger.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport { getRelative } from '../fs/findFilepath.js';\n\ntype LatestDownloadedVersion = {\n versionId: string;\n entry: DownloadedTranslation;\n};\n\n/**\n * The bytes a file's pipeline content stands for: binary formats carry base64,\n * everything else carries UTF-8 text. Applied to both sides of the comparison\n * so they are measured the same way.\n */\nconst contentBytes = (content: string, fileFormat: FileFormat): Buffer =>\n isBinaryFileFormat(fileFormat)\n ? Buffer.from(content, 'base64')\n : Buffer.from(content, 'utf8');\n\nconst findLatestDownloadedVersion = (\n entryMap: EntryMap,\n fileId: string,\n locale: string\n): LatestDownloadedVersion | null => {\n const entry = entryMap.get(fileId);\n if (!entry) return null;\n\n const translation = entry.translations[locale];\n if (!translation) return null;\n\n return { versionId: entry.versionId, entry: translation };\n};\n\n/**\n * Collects local user edits by diffing the latest downloaded server translation version\n * against the current local translation file, and submits the diffs upstream.\n *\n * Must run before enqueueing new translations so rules are available to the generator.\n */\nexport async function collectAndSendUserEditDiffs(\n files: FileReference[],\n settings: Settings\n): Promise<boolean> {\n if (!settings.files) return false;\n\n const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } =\n settings.files;\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n\n const { entryMap } = readLockfile(settings);\n\n const tempDir = path.join(os.tmpdir(), randomUUID());\n if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });\n\n // Build candidates for diff and batch-fetch server contents\n type DiffCandidate = {\n branchId: string;\n fileName: string;\n fileId: string;\n versionId: string;\n locale: string; // resolved\n outputPath: string;\n fileFormat: FileFormat;\n };\n const candidates: DiffCandidate[] = [];\n\n for (const uploadedFile of files) {\n for (const locale of settings.locales) {\n const outputPath = fileMapping[locale]?.[uploadedFile.fileName] ?? null;\n if (!outputPath) continue;\n if (!fs.existsSync(outputPath)) continue;\n\n const latestDownloaded = findLatestDownloadedVersion(\n entryMap,\n uploadedFile.fileId,\n locale\n );\n\n if (!latestDownloaded) continue;\n const downloadedVersion = latestDownloaded.entry;\n\n // Skip if local file matches the last postprocessed content hash\n if (downloadedVersion.postProcessHash) {\n try {\n // Hashed from the same pipeline content the hash was recorded from,\n // so a file stored in UTF-16 still matches when it is untouched.\n const localContent = readFileContent(\n outputPath,\n uploadedFile.fileFormat\n );\n const localHash = hashStringSync(localContent);\n if (localHash === downloadedVersion.postProcessHash) {\n continue;\n }\n } catch {\n // If hash check fails, fall through to diff\n }\n }\n\n candidates.push({\n branchId: uploadedFile.branchId,\n fileName: uploadedFile.fileName,\n fileId: uploadedFile.fileId,\n versionId: latestDownloaded.versionId,\n locale: locale,\n outputPath,\n fileFormat: uploadedFile.fileFormat,\n });\n }\n }\n\n const collectedDiffs: SubmitUserEditDiff[] = [];\n\n if (candidates.length > 0) {\n const fileQueryData = candidates.map((c) => ({\n versionId: c.versionId,\n locale: c.locale,\n fileId: c.fileId,\n branchId: c.branchId,\n }));\n\n // Single batched check to obtain translation IDs\n const checkResponse = await api.queryFileData({\n translatedFiles: fileQueryData,\n });\n const translatedFiles =\n checkResponse.translatedFiles?.filter((t) => t.completedAt) ?? [];\n\n const serverContentByKey = new Map<string, Buffer>();\n try {\n const resp = await api.downloadFileBatch(\n translatedFiles.map((file) => ({\n branchId: file.branchId,\n fileId: file.fileId,\n locale: file.locale,\n versionId: file.versionId,\n }))\n );\n const files = resp?.files || [];\n for (const f of files) {\n serverContentByKey.set(\n `${f.branchId}:${f.fileId}:${f.versionId}:${f.locale}`,\n contentBytes(f.data, f.fileFormat)\n );\n }\n } catch {\n // Ignore chunk failures; proceed with what we have\n }\n\n // Compute diffs using fetched server contents\n for (const c of candidates) {\n const key = `${c.branchId}:${c.fileId}:${c.versionId}:${c.locale}`;\n const serverBytes = serverContentByKey.get(key);\n // Absent means the batch did not return this file, so there is no\n // baseline. An empty payload is a baseline of nothing, which the user\n // may well have written against.\n if (!serverBytes) continue;\n\n try {\n // Read the local file the same way the pipeline read it originally, so\n // a file stored differently on disk than the server's copy is compared\n // as content rather than as bytes. Otherwise every such file would read\n // as edited on every run.\n const localBytes = contentBytes(\n readFileContent(c.outputPath, c.fileFormat),\n c.fileFormat\n );\n\n // Nothing was edited, so there is no diff to compute or report.\n if (localBytes.equals(serverBytes)) continue;\n\n // A unified diff and localContent are both UTF-8 text, and every text\n // format's content is UTF-8 by the time it reaches here. Only a binary\n // format's bytes — a Lottie zip — have no text form at all. Say so:\n // the edit is real and will be lost on the next download.\n if (isBinaryFileFormat(c.fileFormat)) {\n const relativePath = getRelative(c.outputPath);\n const reason =\n 'Edited file is a binary format, so its changes cannot be submitted';\n logger.warn(`Skipping local edits to ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n continue;\n }\n\n const safeName = Buffer.from(\n `${c.branchId}:${c.fileId}:${c.versionId}:${c.locale}`\n )\n .toString('base64')\n .replace(/=+$/g, '');\n const tempServerFile = path.join(tempDir, `${safeName}.server`);\n await fs.promises.writeFile(tempServerFile, serverBytes);\n\n // git diff reads bytes, so both sides are written from the content\n // they represent rather than diffing the file as it sits on disk.\n const tempLocalFile = path.join(tempDir, `${safeName}.local`);\n await fs.promises.writeFile(tempLocalFile, localBytes);\n\n const diff = await getGitUnifiedDiff(tempServerFile, tempLocalFile);\n for (const tempFile of [tempServerFile, tempLocalFile]) {\n try {\n await fs.promises.unlink(tempFile);\n } catch {\n // Ignore cleanup errors for temporary comparison files.\n }\n }\n\n if (diff && diff.trim().length > 0) {\n const rawLocalContent = localBytes.toString('utf8');\n\n // For JSON files with jsonSchema config, extract to composite format\n let localContent = rawLocalContent;\n if (\n c.fileName.endsWith('.json') &&\n settings.options?.jsonSchema &&\n c.locale !== settings.defaultLocale\n ) {\n const extractedContent = extractJson(\n rawLocalContent,\n c.fileName,\n settings.options,\n c.locale,\n settings.defaultLocale\n );\n if (extractedContent) {\n localContent = extractedContent;\n }\n } else if (\n (c.fileName.endsWith('.yaml') || c.fileName.endsWith('.yml')) &&\n settings.options?.yamlSchema &&\n c.locale !== settings.defaultLocale\n ) {\n const extractedContent = extractYaml(\n rawLocalContent,\n c.fileName,\n settings.options\n );\n if (extractedContent) {\n localContent = extractedContent;\n }\n }\n\n collectedDiffs.push({\n fileName: c.fileName,\n locale: c.locale,\n diff,\n branchId: c.branchId,\n versionId: c.versionId,\n fileId: c.fileId,\n localContent,\n } satisfies SubmitUserEditDiff);\n }\n } catch {\n // Ignore failures for this file\n }\n }\n }\n\n if (collectedDiffs.length > 0) {\n await api.submitUserEditDiffs({ diffs: collectedDiffs });\n }\n\n return collectedDiffs.length > 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"collectUserEditDiffs.js","names":["path","fs"],"sources":["../../src/api/collectUserEditDiffs.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n readLockfile,\n EntryMap,\n DownloadedTranslation,\n} from '../fs/config/downloadedVersions.js';\nimport { Settings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport { getGitUnifiedDiff } from '../utils/gitDiff.js';\nimport { api } from '../utils/api.js';\nimport {\n FileReference,\n isBinaryFileFormat,\n SubmitUserEditDiff,\n type FileFormat,\n} from 'generaltranslation/types';\nimport { readFileContent } from '../fs/fileContent.js';\nimport os from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport { hashStringSync } from '../utils/hash.js';\nimport { extractJson } from '../formats/json/extractJson.js';\nimport {\n emptyLocaleContent,\n localeContent,\n} from '../formats/files/localeContent.js';\nimport { extractYaml } from '../formats/yaml/extractYaml.js';\nimport { logger } from '../console/logger.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport { getRelative } from '../fs/findFilepath.js';\n\ntype LatestDownloadedVersion = {\n versionId: string;\n entry: DownloadedTranslation;\n};\n\n/**\n * The bytes a file's pipeline content stands for: binary formats carry base64,\n * everything else carries UTF-8 text. Applied to both sides of the comparison\n * so they are measured the same way.\n */\nconst contentBytes = (content: string, fileFormat: FileFormat): Buffer =>\n isBinaryFileFormat(fileFormat)\n ? Buffer.from(content, 'base64')\n : Buffer.from(content, 'utf8');\n\nconst findLatestDownloadedVersion = (\n entryMap: EntryMap,\n fileId: string,\n locale: string\n): LatestDownloadedVersion | null => {\n const entry = entryMap.get(fileId);\n if (!entry) return null;\n\n const translation = entry.translations[locale];\n if (!translation) return null;\n\n return { versionId: entry.versionId, entry: translation };\n};\n\n/**\n * Collects local user edits by diffing the latest downloaded server translation version\n * against the current local translation file, and submits the diffs upstream.\n * A file that holds every locale is compared one locale slice at a time, so an\n * edit to one locale is submitted under that locale alone.\n *\n * Must run before enqueueing new translations so rules are available to the generator.\n */\nexport async function collectAndSendUserEditDiffs(\n files: FileReference[],\n settings: Settings\n): Promise<boolean> {\n if (!settings.files) return false;\n\n const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } =\n settings.files;\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n\n const { entryMap } = readLockfile(settings);\n\n const tempDir = path.join(os.tmpdir(), randomUUID());\n if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });\n\n // Build candidates for diff and batch-fetch server contents\n type DiffCandidate = {\n branchId: string;\n fileName: string;\n fileId: string;\n versionId: string;\n locale: string; // resolved\n outputPath: string;\n fileFormat: FileFormat;\n };\n const candidates: DiffCandidate[] = [];\n\n for (const uploadedFile of files) {\n for (const locale of settings.locales) {\n const outputPath = fileMapping[locale]?.[uploadedFile.fileName] ?? null;\n if (!outputPath) continue;\n if (!fs.existsSync(outputPath)) continue;\n\n const latestDownloaded = findLatestDownloadedVersion(\n entryMap,\n uploadedFile.fileId,\n locale\n );\n\n if (!latestDownloaded) continue;\n const downloadedVersion = latestDownloaded.entry;\n\n // Skip if the locale's local content matches the last recorded hash\n if (downloadedVersion.postProcessHash) {\n try {\n // Hashed from the locale's share of the pipeline content, which is\n // what was recorded, so an untouched locale matches regardless of\n // the file's encoding or of edits to its other locales.\n const localFile = readFileContent(\n outputPath,\n uploadedFile.fileFormat\n );\n const localHash = hashStringSync(\n localeContent(localFile, uploadedFile.fileFormat, locale) ??\n emptyLocaleContent(localFile, uploadedFile.fileFormat)\n );\n if (localHash === downloadedVersion.postProcessHash) {\n continue;\n }\n } catch {\n // If hash check fails, fall through to diff\n }\n }\n\n candidates.push({\n branchId: uploadedFile.branchId,\n fileName: uploadedFile.fileName,\n fileId: uploadedFile.fileId,\n versionId: latestDownloaded.versionId,\n locale: locale,\n outputPath,\n fileFormat: uploadedFile.fileFormat,\n });\n }\n }\n\n const collectedDiffs: SubmitUserEditDiff[] = [];\n\n if (candidates.length > 0) {\n const fileQueryData = candidates.map((c) => ({\n versionId: c.versionId,\n locale: c.locale,\n fileId: c.fileId,\n branchId: c.branchId,\n }));\n\n // Single batched check to obtain translation IDs\n const checkResponse = await api.queryFileData({\n translatedFiles: fileQueryData,\n });\n const translatedFiles =\n checkResponse.translatedFiles?.filter((t) => t.completedAt) ?? [];\n\n // The server's copy of each candidate, keyed like the candidates\n type ServerPayload = { data: string; fileFormat: FileFormat };\n const serverPayloadByKey = new Map<string, ServerPayload>();\n try {\n const resp = await api.downloadFileBatch(\n translatedFiles.map((file) => ({\n branchId: file.branchId,\n fileId: file.fileId,\n locale: file.locale,\n versionId: file.versionId,\n }))\n );\n for (const f of resp?.files || []) {\n if (!f.locale) continue;\n serverPayloadByKey.set(\n `${f.branchId}:${f.fileId}:${f.versionId}:${f.locale}`,\n { data: f.data, fileFormat: f.fileFormat }\n );\n }\n } catch {\n // Ignore chunk failures; proceed with what we have\n }\n\n // Compute diffs using fetched server contents\n for (const c of candidates) {\n const key = `${c.branchId}:${c.fileId}:${c.versionId}:${c.locale}`;\n const payload = serverPayloadByKey.get(key);\n // Absent means the batch did not return this file, so there is no\n // baseline. An empty payload is a baseline of nothing, which the user\n // may well have written against.\n if (!payload) continue;\n\n // The locale's share of the payload, read on its own so a payload the\n // server sent malformed costs only its own locale. A catalog payload\n // that carries nothing for the locale is that file's empty payload, and\n // a baseline of nothing like any other.\n let serverContent: string;\n try {\n serverContent =\n localeContent(payload.data, payload.fileFormat, c.locale) ??\n emptyLocaleContent(payload.data, payload.fileFormat);\n } catch (error) {\n const relativePath = getRelative(c.outputPath);\n const reason = `The downloaded ${c.locale} translation could not be read (${\n error instanceof Error ? error.message : String(error)\n })`;\n logger.warn(`Skipping local edits to ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n continue;\n }\n const serverBytes = contentBytes(serverContent, payload.fileFormat);\n\n try {\n // Read the local file the same way the pipeline read it originally, so\n // a file stored differently on disk than the server's copy is compared\n // as content rather than as bytes. Otherwise every such file would read\n // as edited on every run. Then cut to this locale's share, so a file\n // that holds every locale is compared slice to slice.\n const local = localeContent(\n readFileContent(c.outputPath, c.fileFormat),\n c.fileFormat,\n c.locale\n );\n // A catalog that no longer carries the locale has nothing to submit\n if (local === undefined) continue;\n const localBytes = contentBytes(local, c.fileFormat);\n\n // Nothing was edited, so there is no diff to compute or report.\n if (localBytes.equals(serverBytes)) continue;\n\n // A unified diff and localContent are both UTF-8 text, and every text\n // format's content is UTF-8 by the time it reaches here. Only a binary\n // format's bytes — a Lottie zip — have no text form at all. Say so:\n // the edit is real and will be lost on the next download.\n if (isBinaryFileFormat(c.fileFormat)) {\n const relativePath = getRelative(c.outputPath);\n const reason =\n 'Edited file is a binary format, so its changes cannot be submitted';\n logger.warn(`Skipping local edits to ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n continue;\n }\n\n const safeName = Buffer.from(\n `${c.branchId}:${c.fileId}:${c.versionId}:${c.locale}`\n )\n .toString('base64')\n .replace(/=+$/g, '');\n const tempServerFile = path.join(tempDir, `${safeName}.server`);\n await fs.promises.writeFile(tempServerFile, serverBytes);\n\n // git diff reads bytes, so both sides are written from the content\n // they represent rather than diffing the file as it sits on disk.\n const tempLocalFile = path.join(tempDir, `${safeName}.local`);\n await fs.promises.writeFile(tempLocalFile, localBytes);\n\n const diff = await getGitUnifiedDiff(tempServerFile, tempLocalFile);\n for (const tempFile of [tempServerFile, tempLocalFile]) {\n try {\n await fs.promises.unlink(tempFile);\n } catch {\n // Ignore cleanup errors for temporary comparison files.\n }\n }\n\n if (diff && diff.trim().length > 0) {\n const rawLocalContent = localBytes.toString('utf8');\n\n // For JSON files with jsonSchema config, extract to composite format\n let localContent = rawLocalContent;\n if (\n c.fileName.endsWith('.json') &&\n settings.options?.jsonSchema &&\n c.locale !== settings.defaultLocale\n ) {\n const extractedContent = extractJson(\n rawLocalContent,\n c.fileName,\n settings.options,\n c.locale,\n settings.defaultLocale\n );\n if (extractedContent) {\n localContent = extractedContent;\n }\n } else if (\n (c.fileName.endsWith('.yaml') || c.fileName.endsWith('.yml')) &&\n settings.options?.yamlSchema &&\n c.locale !== settings.defaultLocale\n ) {\n const extractedContent = extractYaml(\n rawLocalContent,\n c.fileName,\n settings.options\n );\n if (extractedContent) {\n localContent = extractedContent;\n }\n }\n\n collectedDiffs.push({\n fileName: c.fileName,\n locale: c.locale,\n diff,\n branchId: c.branchId,\n versionId: c.versionId,\n fileId: c.fileId,\n localContent,\n } satisfies SubmitUserEditDiff);\n }\n } catch {\n // Ignore failures for this file\n }\n }\n }\n\n if (collectedDiffs.length > 0) {\n await api.submitUserEditDiffs({ diffs: collectedDiffs });\n }\n\n return collectedDiffs.length > 0;\n}\n"],"mappings":"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"}
@@ -13,6 +13,7 @@ import { resolveMintlifyRefs, shouldResolveRefs } from "../utils/resolveMintlify
13
13
  import { validateYamlSchema } from "../formats/yaml/utils.js";
14
14
  import { extractYaml } from "../formats/yaml/extractYaml.js";
15
15
  import { mergeJson } from "../formats/json/mergeJson.js";
16
+ import { mergeXcstringsLocale } from "../formats/xcstrings/mergeXcstrings.js";
16
17
  import mergeYaml from "../formats/yaml/mergeYaml.js";
17
18
  import { recordDownloaded, recordRemerged } from "../state/recentDownloads.js";
18
19
  import * as fs$1 from "fs";
@@ -162,8 +163,9 @@ async function downloadFileBatch(fileTracker, files, options, forceDownload = fa
162
163
  }
163
164
  continue;
164
165
  }
166
+ const isXcstringsCatalog = file.fileFormat === "XCSTRINGS";
165
167
  const isInPlaceComposite = options.options?.jsonSchema ? !!validateJsonSchema(options.options, inputPath)?.composite : false;
166
- if (!forceDownload && fileExists && downloadedTranslation && !isInPlaceComposite) {
168
+ if (!forceDownload && fileExists && downloadedTranslation && !(isXcstringsCatalog || isInPlaceComposite)) {
167
169
  try {
168
170
  let existingContent = fs$1.readFileSync(outputPath, "utf8");
169
171
  if (shouldResolveRefs(outputPath, options.options)) try {
@@ -186,11 +188,15 @@ async function downloadFileBatch(fileTracker, files, options, forceDownload = fa
186
188
  result.skipped.push(requestedFile);
187
189
  continue;
188
190
  }
189
- let data = mergeWithSource(file.data, locale, inputPath, options);
190
- if (file.fileFormat === "GTJSON" || outputPath.endsWith(".json")) try {
191
- data = sortJsonString(data);
192
- } catch (error) {
193
- logger.warn(`Failed to sort JSON file: ${file.id}: ` + error);
191
+ let data;
192
+ if (isXcstringsCatalog) data = mergeXcstringsLocale(file.data, locale, inputPath);
193
+ else {
194
+ data = mergeWithSource(file.data, locale, inputPath, options);
195
+ if (file.fileFormat === "GTJSON" || outputPath.endsWith(".json")) try {
196
+ data = sortJsonString(data);
197
+ } catch (error) {
198
+ logger.warn(`Failed to sort JSON file: ${file.id}: ` + error);
199
+ }
194
200
  }
195
201
  await fs$1.promises.writeFile(outputPath, encodeFileContent(data, file.fileFormat, inputPath, outputPath));
196
202
  recordDownloaded(outputPath, {
@@ -207,6 +213,7 @@ async function downloadFileBatch(fileTracker, files, options, forceDownload = fa
207
213
  const entry = findOrCreateEntry(entryMap, downloadedVersions.entries, fileId, versionId);
208
214
  entry.fileName = inputPath;
209
215
  entry.translations[locale] = {
216
+ ...entry.translations[locale],
210
217
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
211
218
  fileName: getRelative(outputPath)
212
219
  };
@@ -1 +1 @@
1
- {"version":3,"file":"downloadFileBatch.js","names":["fs","path"],"sources":["../../src/api/downloadFileBatch.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { isBinaryFileFormat } from 'generaltranslation/types';\nimport { logger } from '../console/logger.js';\nimport { api } from '../utils/api.js';\nimport { Settings } from '../types/index.js';\nimport { validateJsonSchema } from '../formats/json/utils.js';\nimport { validateYamlSchema } from '../formats/yaml/utils.js';\nimport { mergeJson } from '../formats/json/mergeJson.js';\nimport { extractJson } from '../formats/json/extractJson.js';\nimport mergeYaml from '../formats/yaml/mergeYaml.js';\nimport { extractYaml } from '../formats/yaml/extractYaml.js';\nimport {\n resolveMintlifyRefs,\n shouldResolveRefs,\n} from '../utils/resolveMintlifyRefs.js';\nimport {\n readLockfile,\n writeLockfile,\n findOrCreateEntry,\n} from '../fs/config/downloadedVersions.js';\nimport { recordDownloaded, recordRemerged } from '../state/recentDownloads.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport stringify from 'fast-json-stable-stringify';\nimport type { FileStatusTracker } from '../workflows/steps/PollJobsStep.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../formats/files/supportedFiles.js';\nimport { hasNonIdentityFileFormatTransformForType } from '../formats/files/transformFormat.js';\nimport { getRelative } from '../fs/findFilepath.js';\nimport { encodeFileContent } from '../fs/fileContent.js';\nimport {\n getInlineElementsLabel,\n type InlineLibrary,\n} from '../types/libraries.js';\n\n/**\n * The platform withholds unapproved review-gated GTJSON components from\n * served output. Reports the delta between the source component count and\n * the served content so partially-served files don't read as fully\n * translated — for both fresh downloads and already-downloaded skips.\n */\nfunction reportWithheldGtJsonComponents(\n fileFormat: string | undefined,\n servedContent: string,\n componentCount: number | undefined,\n locale: string,\n inlineLibrary?: InlineLibrary\n): void {\n if (fileFormat !== 'GTJSON' || componentCount == null) return;\n const received = countGtJsonEntries(servedContent);\n if (received == null) return;\n const withheld = componentCount - received;\n if (withheld > 0) {\n recordWarning(\n 'pending_review',\n `<${getInlineElementsLabel(inlineLibrary)}>`,\n `${withheld} component translation(s) for locale ${locale} require review and are not approved yet`\n );\n }\n}\n\n/**\n * Counts entries in downloaded GTJSON output, tolerating both the flat\n * public shape and a wrapped { type, data } shape.\n */\nfunction countGtJsonEntries(content: string): number | undefined {\n try {\n const parsed: unknown = JSON.parse(content);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined;\n }\n const entries =\n 'type' in parsed &&\n parsed.type === 'GTJSON' &&\n 'data' in parsed &&\n parsed.data &&\n typeof parsed.data === 'object' &&\n !Array.isArray(parsed.data)\n ? parsed.data\n : parsed;\n return Object.keys(entries).length;\n } catch {\n return undefined;\n }\n}\n\nfunction sortJsonString(data: string): string {\n const sortedData = stringify(JSON.parse(data));\n return JSON.stringify(JSON.parse(sortedData), null, 2);\n}\n\n/**\n * Merges translated content with the current source file for schema-based formats.\n */\nfunction mergeWithSource(\n translatedContent: string,\n locale: string,\n inputPath: string,\n options: Settings\n): string {\n if (shouldSkipSourceFormatMerge(inputPath, options)) {\n return translatedContent;\n }\n if (!options.options) return translatedContent;\n\n const jsonSchema = options.options.jsonSchema\n ? validateJsonSchema(options.options, inputPath)\n : null;\n const yamlSchema =\n !jsonSchema && options.options.yamlSchema\n ? validateYamlSchema(options.options, inputPath)\n : null;\n\n if (!jsonSchema && !yamlSchema) return translatedContent;\n\n const sourceContent = fs.readFileSync(inputPath, 'utf8');\n if (!sourceContent) return translatedContent;\n\n if (jsonSchema) {\n // Resolve $ref before merging if configured\n let resolvedSourceContent = sourceContent;\n if (shouldResolveRefs(inputPath, options.options)) {\n try {\n const json = JSON.parse(sourceContent);\n const { resolved } = resolveMintlifyRefs(json, inputPath);\n resolvedSourceContent = JSON.stringify(resolved, null, 2);\n } catch {\n // Fall through with original content\n }\n }\n return mergeJson(\n resolvedSourceContent,\n inputPath,\n options.options,\n [{ translatedContent, targetLocale: locale }],\n options.defaultLocale,\n options.locales\n )[0];\n } else {\n return mergeYaml(\n sourceContent,\n inputPath,\n options.options,\n [{ translatedContent, targetLocale: locale }],\n options.defaultLocale\n )[0];\n }\n}\n\n/**\n * Determines whether a source file should be skipped for schema re-merging.\n * @param inputPath - The path of the source file\n * @param options - The settings for the project\n * @returns True if the source file should be skipped for schema re-merging, false otherwise\n */\nfunction shouldSkipSourceFormatMerge(\n inputPath: string,\n options: Settings\n): boolean {\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (!hasNonIdentityFileFormatTransformForType(options, fileType)) continue;\n\n const transformedSourcePaths = options.files.resolvedPaths[fileType] || [];\n if (\n transformedSourcePaths.some(\n (sourcePath) => getRelative(sourcePath) === inputPath\n )\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nexport type BatchedFiles = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n outputPath: string;\n inputPath: string;\n}[];\n\nexport type DownloadFileBatchResult = {\n successful: BatchedFiles;\n failed: BatchedFiles;\n skipped: BatchedFiles;\n};\n/**\n * Downloads multiple translation files in a single batch request\n * @param fileTracker - Current translation status keyed by file identity\n * @param files - Array of files to download with their output paths\n * @param options - Resolved CLI settings used for merge and lockfile behavior\n * @param forceDownload - Whether to bypass an up-to-date local translation\n * @param inlineLibrary - Inline runtime used to label the shared GTJSON file\n * @returns Object containing successful and failed file IDs\n */\nexport async function downloadFileBatch(\n fileTracker: FileStatusTracker,\n files: BatchedFiles,\n options: Settings,\n forceDownload: boolean = false,\n inlineLibrary?: InlineLibrary\n): Promise<DownloadFileBatchResult> {\n // Local record of what version was last downloaded for each fileName:locale\n const {\n data: downloadedVersions,\n entryMap,\n originalV1,\n } = readLockfile(options);\n let didUpdateDownloadedLock = false;\n\n // Create a map of requested file keys to the file object\n const requestedFileMap = new Map(\n files.map((file) => [\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file,\n ])\n );\n const result: DownloadFileBatchResult = {\n successful: [],\n failed: [],\n skipped: [],\n };\n\n // Create a map of translationId to outputPath for easier lookup\n const outputPathMap = new Map(\n files.map((file) => [\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file.outputPath,\n ])\n );\n\n try {\n // Download the files\n const responseData = await api.downloadFileBatch(\n files.map((file) => ({\n fileId: file.fileId,\n branchId: file.branchId,\n versionId: file.versionId,\n locale: file.locale,\n }))\n );\n const downloadedFiles = responseData.files || [];\n\n // Process each file in the response\n for (const file of downloadedFiles) {\n const fileKey = `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`;\n const requestedFile = requestedFileMap.get(fileKey);\n if (!requestedFile) {\n continue;\n }\n try {\n const outputPath = outputPathMap.get(fileKey);\n const fileProperties = fileTracker.completed.get(fileKey);\n\n if (!outputPath || !fileProperties) {\n logger.warn(`No input/output path found for file: ${fileKey}`);\n recordWarning(\n 'failed_download',\n fileKey,\n 'No input/output path found'\n );\n result.failed.push(requestedFile);\n continue;\n }\n\n const {\n fileId,\n versionId,\n locale,\n branchId,\n fileName: inputPath,\n } = fileProperties;\n\n // Ensure the directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n // If a local translation already exists for the same source version, skip overwrite\n const existingEntry = entryMap.get(fileId);\n const downloadedTranslation =\n existingEntry?.versionId === versionId\n ? existingEntry.translations[locale]\n : undefined;\n const fileExists = fs.existsSync(outputPath);\n\n // Binary formats (e.g. LOTTIE zip bundles) carry base64 content that must\n // not go through the text merge/sort path — decode straight to bytes and\n // write. Skip only when an unchanged local translation already exists.\n if (isBinaryFileFormat(file.fileFormat)) {\n if (!forceDownload && fileExists && downloadedTranslation) {\n result.skipped.push(requestedFile);\n continue;\n }\n await fs.promises.writeFile(\n outputPath,\n Buffer.from(file.data, 'base64')\n );\n recordDownloaded(outputPath, {\n branchId,\n fileId,\n versionId,\n locale,\n fileFormat: file.fileFormat,\n inputPath,\n });\n result.successful.push(requestedFile);\n if (fileId && versionId && locale) {\n const entry = findOrCreateEntry(\n entryMap,\n downloadedVersions.entries,\n fileId,\n versionId\n );\n entry.fileName = inputPath;\n entry.translations[locale] = {\n updatedAt: new Date().toISOString(),\n // Store a relative path (matches the text path) so gt-lock.json\n // stays portable and the skip-on-re-download check matches.\n fileName: getRelative(outputPath),\n };\n // A staged entry tracks the whole file, not one locale. Keep it\n // staged until every configured locale has downloaded, so a partial\n // batch (locales still translating) doesn't abandon the rest.\n if (entry.staged) {\n entry.staged = !options.locales.every(\n (l) => entry.translations[l]\n );\n }\n didUpdateDownloadedLock = true;\n }\n continue;\n }\n\n // Composite schema files merge translations into the source file itself,\n // so outputPath always exists and the lock can't tell whether derived\n // split outputs (e.g. {locale}/docs.json) are still on disk. Always\n // merge fresh API data so derived files are regenerated every run;\n // local edits to translated output are preserved via `gt save-local`.\n const isInPlaceComposite = options.options?.jsonSchema\n ? !!validateJsonSchema(options.options, inputPath)?.composite\n : false;\n\n if (\n !forceDownload &&\n fileExists &&\n downloadedTranslation &&\n !isInPlaceComposite\n ) {\n // For schema-based files, re-merge with current source in case\n // non-translatable fields changed (skip the API download, not the merge)\n try {\n let existingContent = fs.readFileSync(outputPath, 'utf8');\n // Resolve $ref before extraction if configured\n if (shouldResolveRefs(outputPath, options.options)) {\n try {\n const json = JSON.parse(existingContent);\n const { resolved } = resolveMintlifyRefs(json, outputPath);\n existingContent = JSON.stringify(resolved, null, 2);\n } catch {\n // Fall through with original content\n }\n }\n const jsonExtracted = options.options?.jsonSchema\n ? extractJson(\n existingContent,\n inputPath,\n options.options,\n locale,\n options.defaultLocale\n )\n : null;\n const extracted =\n jsonExtracted ??\n (options.options?.yamlSchema\n ? extractYaml(existingContent, inputPath, options.options)\n : null);\n if (extracted) {\n const remerged = mergeWithSource(\n extracted,\n locale,\n inputPath,\n options\n );\n let remergedData = remerged;\n if (outputPath.endsWith('.json')) {\n try {\n remergedData = sortJsonString(remergedData);\n } catch {\n // Fall through with unsorted content\n }\n }\n if (remergedData !== existingContent) {\n await fs.promises.writeFile(outputPath, remergedData);\n }\n // Track for postprocessing (e.g. openapi path localization)\n // even when the API download was skipped\n recordRemerged(outputPath);\n }\n } catch (error) {\n // If re-merge fails, still count as skipped — not worth failing\n // the download, but surface it so missing output is diagnosable\n logger.warn(\n `Failed to re-merge existing translation for ${outputPath} (${locale}): ${error}`\n );\n }\n // Even when the local copy is current, the served content may\n // still be missing review-withheld components — keep reporting\n reportWithheldGtJsonComponents(\n file.fileFormat,\n file.data,\n fileProperties.componentCount,\n locale,\n inlineLibrary\n );\n result.skipped.push(requestedFile);\n continue;\n }\n let data = mergeWithSource(file.data, locale, inputPath, options);\n\n // Stable sort JSON keys for deterministic output\n if (file.fileFormat === 'GTJSON' || outputPath.endsWith('.json')) {\n try {\n data = sortJsonString(data);\n } catch (error) {\n logger.warn(`Failed to sort JSON file: ${file.id}: ` + error);\n }\n }\n\n // Write the file to disk\n await fs.promises.writeFile(\n outputPath,\n encodeFileContent(data, file.fileFormat, inputPath, outputPath)\n );\n // Track as downloaded with metadata for downstream postprocessing\n recordDownloaded(outputPath, {\n branchId,\n fileId,\n versionId,\n locale,\n fileFormat: file.fileFormat,\n inputPath,\n });\n\n reportWithheldGtJsonComponents(\n file.fileFormat,\n data,\n fileProperties.componentCount,\n locale,\n inlineLibrary\n );\n\n result.successful.push(requestedFile);\n if (fileId && versionId && locale) {\n const entry = findOrCreateEntry(\n entryMap,\n downloadedVersions.entries,\n fileId,\n versionId\n );\n entry.fileName = inputPath;\n entry.translations[locale] = {\n updatedAt: new Date().toISOString(),\n fileName: getRelative(outputPath),\n };\n // A staged entry tracks the whole file, not one locale. Keep it\n // staged until every configured locale has downloaded, so a partial\n // batch (locales still translating) doesn't abandon the rest.\n if (entry.staged) {\n entry.staged = !options.locales.every((l) => entry.translations[l]);\n }\n didUpdateDownloadedLock = true;\n }\n } catch (error) {\n logger.error(`Error saving file ${fileKey}: ` + error);\n recordWarning(\n 'failed_download',\n fileKey,\n `Error saving file: ${error}`\n );\n result.failed.push(requestedFile);\n }\n }\n\n // Add any files that weren't in the response to the failed list\n const downloadedFileKeys = new Set(\n downloadedFiles.map(\n (file) =>\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`\n )\n );\n for (const [fileKey, requestedFile] of requestedFileMap.entries()) {\n if (!downloadedFileKeys.has(fileKey)) {\n result.failed.push(requestedFile);\n }\n }\n\n // Persist any updates to the downloaded map at the end of a successful cycle\n if (didUpdateDownloadedLock) {\n writeLockfile(downloadedVersions, originalV1);\n didUpdateDownloadedLock = false;\n }\n return result;\n } catch (error) {\n logger.error(\n `An unexpected error occurred while downloading files: ` + error\n );\n }\n\n // Mark all files as failed if we get here\n result.failed = [...requestedFileMap.values()];\n if (didUpdateDownloadedLock) {\n writeLockfile(downloadedVersions, originalV1);\n }\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"downloadFileBatch.js","names":["fs","path"],"sources":["../../src/api/downloadFileBatch.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { isBinaryFileFormat } from 'generaltranslation/types';\nimport { logger } from '../console/logger.js';\nimport { api } from '../utils/api.js';\nimport { Settings } from '../types/index.js';\nimport { validateJsonSchema } from '../formats/json/utils.js';\nimport { validateYamlSchema } from '../formats/yaml/utils.js';\nimport { mergeJson } from '../formats/json/mergeJson.js';\nimport { extractJson } from '../formats/json/extractJson.js';\nimport { mergeXcstringsLocale } from '../formats/xcstrings/mergeXcstrings.js';\nimport mergeYaml from '../formats/yaml/mergeYaml.js';\nimport { extractYaml } from '../formats/yaml/extractYaml.js';\nimport {\n resolveMintlifyRefs,\n shouldResolveRefs,\n} from '../utils/resolveMintlifyRefs.js';\nimport {\n readLockfile,\n writeLockfile,\n findOrCreateEntry,\n} from '../fs/config/downloadedVersions.js';\nimport { recordDownloaded, recordRemerged } from '../state/recentDownloads.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport stringify from 'fast-json-stable-stringify';\nimport type { FileStatusTracker } from '../workflows/steps/PollJobsStep.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../formats/files/supportedFiles.js';\nimport { hasNonIdentityFileFormatTransformForType } from '../formats/files/transformFormat.js';\nimport { getRelative } from '../fs/findFilepath.js';\nimport { encodeFileContent } from '../fs/fileContent.js';\nimport {\n getInlineElementsLabel,\n type InlineLibrary,\n} from '../types/libraries.js';\n\n/**\n * The platform withholds unapproved review-gated GTJSON components from\n * served output. Reports the delta between the source component count and\n * the served content so partially-served files don't read as fully\n * translated — for both fresh downloads and already-downloaded skips.\n */\nfunction reportWithheldGtJsonComponents(\n fileFormat: string | undefined,\n servedContent: string,\n componentCount: number | undefined,\n locale: string,\n inlineLibrary?: InlineLibrary\n): void {\n if (fileFormat !== 'GTJSON' || componentCount == null) return;\n const received = countGtJsonEntries(servedContent);\n if (received == null) return;\n const withheld = componentCount - received;\n if (withheld > 0) {\n recordWarning(\n 'pending_review',\n `<${getInlineElementsLabel(inlineLibrary)}>`,\n `${withheld} component translation(s) for locale ${locale} require review and are not approved yet`\n );\n }\n}\n\n/**\n * Counts entries in downloaded GTJSON output, tolerating both the flat\n * public shape and a wrapped { type, data } shape.\n */\nfunction countGtJsonEntries(content: string): number | undefined {\n try {\n const parsed: unknown = JSON.parse(content);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return undefined;\n }\n const entries =\n 'type' in parsed &&\n parsed.type === 'GTJSON' &&\n 'data' in parsed &&\n parsed.data &&\n typeof parsed.data === 'object' &&\n !Array.isArray(parsed.data)\n ? parsed.data\n : parsed;\n return Object.keys(entries).length;\n } catch {\n return undefined;\n }\n}\n\nfunction sortJsonString(data: string): string {\n const sortedData = stringify(JSON.parse(data));\n return JSON.stringify(JSON.parse(sortedData), null, 2);\n}\n\n/**\n * Merges translated content with the current source file for schema-based formats.\n */\nfunction mergeWithSource(\n translatedContent: string,\n locale: string,\n inputPath: string,\n options: Settings\n): string {\n if (shouldSkipSourceFormatMerge(inputPath, options)) {\n return translatedContent;\n }\n if (!options.options) return translatedContent;\n\n const jsonSchema = options.options.jsonSchema\n ? validateJsonSchema(options.options, inputPath)\n : null;\n const yamlSchema =\n !jsonSchema && options.options.yamlSchema\n ? validateYamlSchema(options.options, inputPath)\n : null;\n\n if (!jsonSchema && !yamlSchema) return translatedContent;\n\n const sourceContent = fs.readFileSync(inputPath, 'utf8');\n if (!sourceContent) return translatedContent;\n\n if (jsonSchema) {\n // Resolve $ref before merging if configured\n let resolvedSourceContent = sourceContent;\n if (shouldResolveRefs(inputPath, options.options)) {\n try {\n const json = JSON.parse(sourceContent);\n const { resolved } = resolveMintlifyRefs(json, inputPath);\n resolvedSourceContent = JSON.stringify(resolved, null, 2);\n } catch {\n // Fall through with original content\n }\n }\n return mergeJson(\n resolvedSourceContent,\n inputPath,\n options.options,\n [{ translatedContent, targetLocale: locale }],\n options.defaultLocale,\n options.locales\n )[0];\n } else {\n return mergeYaml(\n sourceContent,\n inputPath,\n options.options,\n [{ translatedContent, targetLocale: locale }],\n options.defaultLocale\n )[0];\n }\n}\n\n/**\n * Determines whether a source file should be skipped for schema re-merging.\n * @param inputPath - The path of the source file\n * @param options - The settings for the project\n * @returns True if the source file should be skipped for schema re-merging, false otherwise\n */\nfunction shouldSkipSourceFormatMerge(\n inputPath: string,\n options: Settings\n): boolean {\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (!hasNonIdentityFileFormatTransformForType(options, fileType)) continue;\n\n const transformedSourcePaths = options.files.resolvedPaths[fileType] || [];\n if (\n transformedSourcePaths.some(\n (sourcePath) => getRelative(sourcePath) === inputPath\n )\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nexport type BatchedFiles = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n outputPath: string;\n inputPath: string;\n}[];\n\nexport type DownloadFileBatchResult = {\n successful: BatchedFiles;\n failed: BatchedFiles;\n skipped: BatchedFiles;\n};\n/**\n * Downloads multiple translation files in a single batch request\n * @param fileTracker - Current translation status keyed by file identity\n * @param files - Array of files to download with their output paths\n * @param options - Resolved CLI settings used for merge and lockfile behavior\n * @param forceDownload - Whether to bypass an up-to-date local translation\n * @param inlineLibrary - Inline runtime used to label the shared GTJSON file\n * @returns Object containing successful and failed file IDs\n */\nexport async function downloadFileBatch(\n fileTracker: FileStatusTracker,\n files: BatchedFiles,\n options: Settings,\n forceDownload: boolean = false,\n inlineLibrary?: InlineLibrary\n): Promise<DownloadFileBatchResult> {\n // Local record of what version was last downloaded for each fileName:locale\n const {\n data: downloadedVersions,\n entryMap,\n originalV1,\n } = readLockfile(options);\n let didUpdateDownloadedLock = false;\n\n // Create a map of requested file keys to the file object\n const requestedFileMap = new Map(\n files.map((file) => [\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file,\n ])\n );\n const result: DownloadFileBatchResult = {\n successful: [],\n failed: [],\n skipped: [],\n };\n\n // Create a map of translationId to outputPath for easier lookup\n const outputPathMap = new Map(\n files.map((file) => [\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file.outputPath,\n ])\n );\n\n try {\n // Download the files\n const responseData = await api.downloadFileBatch(\n files.map((file) => ({\n fileId: file.fileId,\n branchId: file.branchId,\n versionId: file.versionId,\n locale: file.locale,\n }))\n );\n const downloadedFiles = responseData.files || [];\n\n // Process each file in the response\n for (const file of downloadedFiles) {\n const fileKey = `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`;\n const requestedFile = requestedFileMap.get(fileKey);\n if (!requestedFile) {\n continue;\n }\n try {\n const outputPath = outputPathMap.get(fileKey);\n const fileProperties = fileTracker.completed.get(fileKey);\n\n if (!outputPath || !fileProperties) {\n logger.warn(`No input/output path found for file: ${fileKey}`);\n recordWarning(\n 'failed_download',\n fileKey,\n 'No input/output path found'\n );\n result.failed.push(requestedFile);\n continue;\n }\n\n const {\n fileId,\n versionId,\n locale,\n branchId,\n fileName: inputPath,\n } = fileProperties;\n\n // Ensure the directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n // If a local translation already exists for the same source version, skip overwrite\n const existingEntry = entryMap.get(fileId);\n const downloadedTranslation =\n existingEntry?.versionId === versionId\n ? existingEntry.translations[locale]\n : undefined;\n const fileExists = fs.existsSync(outputPath);\n\n // Binary formats (e.g. LOTTIE zip bundles) carry base64 content that must\n // not go through the text merge/sort path — decode straight to bytes and\n // write. Skip only when an unchanged local translation already exists.\n if (isBinaryFileFormat(file.fileFormat)) {\n if (!forceDownload && fileExists && downloadedTranslation) {\n result.skipped.push(requestedFile);\n continue;\n }\n await fs.promises.writeFile(\n outputPath,\n Buffer.from(file.data, 'base64')\n );\n recordDownloaded(outputPath, {\n branchId,\n fileId,\n versionId,\n locale,\n fileFormat: file.fileFormat,\n inputPath,\n });\n result.successful.push(requestedFile);\n if (fileId && versionId && locale) {\n const entry = findOrCreateEntry(\n entryMap,\n downloadedVersions.entries,\n fileId,\n versionId\n );\n entry.fileName = inputPath;\n entry.translations[locale] = {\n updatedAt: new Date().toISOString(),\n // Store a relative path (matches the text path) so gt-lock.json\n // stays portable and the skip-on-re-download check matches.\n fileName: getRelative(outputPath),\n };\n // A staged entry tracks the whole file, not one locale. Keep it\n // staged until every configured locale has downloaded, so a partial\n // batch (locales still translating) doesn't abandon the rest.\n if (entry.staged) {\n entry.staged = !options.locales.every(\n (l) => entry.translations[l]\n );\n }\n didUpdateDownloadedLock = true;\n }\n continue;\n }\n\n // In-place translation files (composite schema JSON, .xcstrings\n // catalogs) merge translations into the source file itself, so\n // outputPath always exists and the lock can't tell whether this\n // locale's content or derived split outputs (e.g. {locale}/docs.json)\n // are still on disk. Always merge fresh API data so they are\n // regenerated every run; local edits to translated output are\n // preserved via `gt save-local`.\n const isXcstringsCatalog = file.fileFormat === 'XCSTRINGS';\n const isInPlaceComposite = options.options?.jsonSchema\n ? !!validateJsonSchema(options.options, inputPath)?.composite\n : false;\n const isInPlaceTranslationFile =\n isXcstringsCatalog || isInPlaceComposite;\n\n if (\n !forceDownload &&\n fileExists &&\n downloadedTranslation &&\n !isInPlaceTranslationFile\n ) {\n // For schema-based files, re-merge with current source in case\n // non-translatable fields changed (skip the API download, not the merge)\n try {\n let existingContent = fs.readFileSync(outputPath, 'utf8');\n // Resolve $ref before extraction if configured\n if (shouldResolveRefs(outputPath, options.options)) {\n try {\n const json = JSON.parse(existingContent);\n const { resolved } = resolveMintlifyRefs(json, outputPath);\n existingContent = JSON.stringify(resolved, null, 2);\n } catch {\n // Fall through with original content\n }\n }\n const jsonExtracted = options.options?.jsonSchema\n ? extractJson(\n existingContent,\n inputPath,\n options.options,\n locale,\n options.defaultLocale\n )\n : null;\n const extracted =\n jsonExtracted ??\n (options.options?.yamlSchema\n ? extractYaml(existingContent, inputPath, options.options)\n : null);\n if (extracted) {\n const remerged = mergeWithSource(\n extracted,\n locale,\n inputPath,\n options\n );\n let remergedData = remerged;\n if (outputPath.endsWith('.json')) {\n try {\n remergedData = sortJsonString(remergedData);\n } catch {\n // Fall through with unsorted content\n }\n }\n if (remergedData !== existingContent) {\n await fs.promises.writeFile(outputPath, remergedData);\n }\n // Track for postprocessing (e.g. openapi path localization)\n // even when the API download was skipped\n recordRemerged(outputPath);\n }\n } catch (error) {\n // If re-merge fails, still count as skipped — not worth failing\n // the download, but surface it so missing output is diagnosable\n logger.warn(\n `Failed to re-merge existing translation for ${outputPath} (${locale}): ${error}`\n );\n }\n // Even when the local copy is current, the served content may\n // still be missing review-withheld components — keep reporting\n reportWithheldGtJsonComponents(\n file.fileFormat,\n file.data,\n fileProperties.componentCount,\n locale,\n inlineLibrary\n );\n result.skipped.push(requestedFile);\n continue;\n }\n let data: string;\n if (isXcstringsCatalog) {\n // The pinned serializer owns the bytes and the JSON key sorter below\n // must never see them. JSON.parse hoists integer-like keys (\"404\")\n // first; versionId is unaffected (slices come from the parsed object)\n // and Xcode re-sorts on its next save, so the only effect is a\n // one-time reorder.\n data = mergeXcstringsLocale(file.data, locale, inputPath);\n } else {\n data = mergeWithSource(file.data, locale, inputPath, options);\n\n // Stable sort JSON keys for deterministic output\n if (file.fileFormat === 'GTJSON' || outputPath.endsWith('.json')) {\n try {\n data = sortJsonString(data);\n } catch (error) {\n logger.warn(`Failed to sort JSON file: ${file.id}: ` + error);\n }\n }\n }\n\n // Write the file to disk\n await fs.promises.writeFile(\n outputPath,\n encodeFileContent(data, file.fileFormat, inputPath, outputPath)\n );\n // Track as downloaded with metadata for downstream postprocessing\n recordDownloaded(outputPath, {\n branchId,\n fileId,\n versionId,\n locale,\n fileFormat: file.fileFormat,\n inputPath,\n });\n\n reportWithheldGtJsonComponents(\n file.fileFormat,\n data,\n fileProperties.componentCount,\n locale,\n inlineLibrary\n );\n\n result.successful.push(requestedFile);\n if (fileId && versionId && locale) {\n const entry = findOrCreateEntry(\n entryMap,\n downloadedVersions.entries,\n fileId,\n versionId\n );\n entry.fileName = inputPath;\n // Keep the hash upload recorded until postprocessing re-hashes the file\n entry.translations[locale] = {\n ...entry.translations[locale],\n updatedAt: new Date().toISOString(),\n fileName: getRelative(outputPath),\n };\n // A staged entry tracks the whole file, not one locale. Keep it\n // staged until every configured locale has downloaded, so a partial\n // batch (locales still translating) doesn't abandon the rest.\n if (entry.staged) {\n entry.staged = !options.locales.every((l) => entry.translations[l]);\n }\n didUpdateDownloadedLock = true;\n }\n } catch (error) {\n logger.error(`Error saving file ${fileKey}: ` + error);\n recordWarning(\n 'failed_download',\n fileKey,\n `Error saving file: ${error}`\n );\n result.failed.push(requestedFile);\n }\n }\n\n // Add any files that weren't in the response to the failed list\n const downloadedFileKeys = new Set(\n downloadedFiles.map(\n (file) =>\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`\n )\n );\n for (const [fileKey, requestedFile] of requestedFileMap.entries()) {\n if (!downloadedFileKeys.has(fileKey)) {\n result.failed.push(requestedFile);\n }\n }\n\n // Persist any updates to the downloaded map at the end of a successful cycle\n if (didUpdateDownloadedLock) {\n writeLockfile(downloadedVersions, originalV1);\n didUpdateDownloadedLock = false;\n }\n return result;\n } catch (error) {\n logger.error(\n `An unexpected error occurred while downloading files: ` + error\n );\n }\n\n // Mark all files as failed if we get here\n result.failed = [...requestedFileMap.values()];\n if (didUpdateDownloadedLock) {\n writeLockfile(downloadedVersions, originalV1);\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { logger } from "../../console/logger.js";
2
+ import { gt } from "../../utils/gt.js";
2
3
  import { exitSync, logErrorAndExit } from "../../console/logging.js";
3
4
  import { getRelative, readFile } from "../../fs/findFilepath.js";
4
5
  import { fileEncodingSkipReason, noDefaultLocaleError } from "../../console/index.js";
@@ -10,6 +11,7 @@ import { runUploadFilesWorkflow } from "../../workflows/upload.js";
10
11
  import { createFileMapping } from "../../formats/files/fileMapping.js";
11
12
  import { hasValidCredentials } from "./utils/validation.js";
12
13
  import { runPublishWorkflow } from "../../workflows/publish.js";
14
+ import { parseXcstringsCatalog, serializeXcstringsSlice, sliceTranslationCatalog } from "../../formats/xcstrings/parseXcstrings.js";
13
15
  import { aggregateFiles } from "../../formats/files/aggregateFiles.js";
14
16
  import { existsSync } from "node:fs";
15
17
  //#region src/cli/commands/upload.ts
@@ -23,17 +25,21 @@ async function upload(settings) {
23
25
  const additionalOptions = settings.options || {};
24
26
  const { resolvedPaths: filePaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
25
27
  const { files: allFiles, publishMap } = await aggregateFiles(settings);
28
+ const sourceFileNames = new Set(allFiles.map((file) => file.fileName));
26
29
  const compositeJsonFiles = /* @__PURE__ */ new Map();
27
- if (filePaths.json) {
28
- const sourceFileNames = new Set(allFiles.map((file) => file.fileName));
29
- for (const filePath of filePaths.json) {
30
- const relativePath = getRelative(filePath);
31
- if (!sourceFileNames.has(relativePath)) continue;
32
- if (validateJsonSchema(additionalOptions, filePath)?.composite) compositeJsonFiles.set(relativePath, {
33
- filePath,
34
- content: readFile(filePath)
35
- });
36
- }
30
+ if (filePaths.json) for (const filePath of filePaths.json) {
31
+ const relativePath = getRelative(filePath);
32
+ if (!sourceFileNames.has(relativePath)) continue;
33
+ if (validateJsonSchema(additionalOptions, filePath)?.composite) compositeJsonFiles.set(relativePath, {
34
+ filePath,
35
+ content: readFile(filePath)
36
+ });
37
+ }
38
+ const xcstringsCatalogs = /* @__PURE__ */ new Map();
39
+ if (filePaths.xcstrings) for (const filePath of filePaths.xcstrings) {
40
+ const relativePath = getRelative(filePath);
41
+ if (!sourceFileNames.has(relativePath)) continue;
42
+ xcstringsCatalogs.set(relativePath, parseXcstringsCatalog(readFile(filePath)));
37
43
  }
38
44
  if (allFiles.length === 0) {
39
45
  logger.error("No files to upload were found. Check your configuration and try again.");
@@ -47,7 +53,20 @@ async function upload(settings) {
47
53
  const sourceFile = { ...file };
48
54
  const translations = [];
49
55
  const compositeInfo = compositeJsonFiles.get(file.fileName);
50
- for (const locale of locales) if (compositeInfo) {
56
+ const xcstringsCatalog = xcstringsCatalogs.get(file.fileName);
57
+ for (const locale of locales) if (xcstringsCatalog) {
58
+ const slice = sliceTranslationCatalog(xcstringsCatalog, gt.resolveCanonicalLocale(locale));
59
+ if (!slice) continue;
60
+ translations.push({
61
+ content: serializeXcstringsSlice(slice),
62
+ fileName: file.fileName,
63
+ fileFormat: file.transformFormat ?? file.fileFormat,
64
+ dataFormat: file.dataFormat,
65
+ locale,
66
+ fileId: file.fileId,
67
+ versionId: file.versionId
68
+ });
69
+ } else if (compositeInfo) {
51
70
  const extracted = extractJson(compositeInfo.content, compositeInfo.filePath, additionalOptions, locale, settings.defaultLocale);
52
71
  if (extracted) translations.push({
53
72
  content: extracted,
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","names":[],"sources":["../../../src/cli/commands/upload.ts"],"sourcesContent":["import {\n fileEncodingSkipReason,\n noDefaultLocaleError,\n} from '../../console/index.js';\nimport { exitSync, logErrorAndExit } from '../../console/logging.js';\nimport { logger } from '../../console/logger.js';\nimport { getRelative, readFile } from '../../fs/findFilepath.js';\nimport { readFileContent } from '../../fs/fileContent.js';\nimport { Settings } from '../../types/index.js';\nimport { UploadOptions } from '../base.js';\nimport { extractJson } from '../../formats/json/extractJson.js';\nimport { validateJsonSchema } from '../../formats/json/utils.js';\nimport { runUploadFilesWorkflow } from '../../workflows/upload.js';\nimport { existsSync } from 'node:fs';\nimport { createFileMapping } from '../../formats/files/fileMapping.js';\nimport type { FileToUpload } from 'generaltranslation/types';\nimport { hasValidCredentials } from './utils/validation.js';\nimport { runPublishWorkflow } from '../../workflows/publish.js';\nimport { aggregateFiles } from '../../formats/files/aggregateFiles.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\n\n/**\n * Sends multiple files to the API for translation\n * @param settings - Translation options including API settings\n * @returns Promise that resolves when translation is complete\n */\nexport async function upload(\n settings: Settings & UploadOptions\n): Promise<void> {\n if (!settings.files) {\n return;\n }\n\n const additionalOptions = settings.options || {};\n const {\n resolvedPaths: filePaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n } = settings.files;\n\n // Reuse the same source aggregation path as translate/stage so source\n // parsing behavior stays consistent across commands.\n const { files: allFiles, publishMap } = await aggregateFiles(settings);\n const compositeJsonFiles = new Map<\n string,\n { filePath: string; content: string }\n >();\n\n if (filePaths.json) {\n const sourceFileNames = new Set(allFiles.map((file) => file.fileName));\n for (const filePath of filePaths.json) {\n const relativePath = getRelative(filePath);\n if (!sourceFileNames.has(relativePath)) continue;\n\n const jsonSchema = validateJsonSchema(additionalOptions, filePath);\n if (jsonSchema?.composite) {\n compositeJsonFiles.set(relativePath, {\n filePath,\n content: readFile(filePath),\n });\n }\n }\n }\n\n if (allFiles.length === 0) {\n logger.error(\n 'No files to upload were found. Check your configuration and try again.'\n );\n return;\n }\n\n if (!settings.defaultLocale) {\n return logErrorAndExit(noDefaultLocaleError);\n }\n if (!hasValidCredentials(settings)) return exitSync(1);\n\n const locales = settings.locales || [];\n // Create file mapping for all file types\n const fileMapping = createFileMapping(\n filePaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n locales,\n settings.defaultLocale\n );\n\n // construct object\n const uploadData = allFiles.map((file) => {\n const sourceFile: FileToUpload = { ...file };\n\n const translations: FileToUpload[] = [];\n const compositeInfo = compositeJsonFiles.get(file.fileName);\n\n for (const locale of locales) {\n if (compositeInfo) {\n // Composite JSON: extract translations from the same source file\n const extracted = extractJson(\n compositeInfo.content,\n compositeInfo.filePath,\n additionalOptions,\n locale,\n settings.defaultLocale\n );\n if (extracted) {\n translations.push({\n content: extracted,\n fileName: file.fileName,\n fileFormat: file.transformFormat ?? file.fileFormat,\n dataFormat: file.dataFormat,\n locale,\n fileId: file.fileId,\n versionId: file.versionId,\n });\n }\n } else {\n // Non-composite: look for separate translation files\n const translatedFileName = fileMapping[locale]?.[file.fileName];\n if (translatedFileName && existsSync(translatedFileName)) {\n const translationFormat = file.transformFormat ?? file.fileFormat;\n let translatedContent: string;\n try {\n translatedContent = readFileContent(\n translatedFileName,\n translationFormat\n );\n } catch (error) {\n // One undecodable locale should not block the others: skip it and\n // report it in the summary.\n const relativePath = getRelative(translatedFileName);\n const reason = fileEncodingSkipReason(error);\n logger.warn(`Skipping ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n continue;\n }\n translations.push({\n content: translatedContent,\n fileName: translatedFileName,\n fileFormat: translationFormat,\n dataFormat: file.dataFormat,\n locale,\n fileId: file.fileId,\n versionId: file.versionId,\n });\n }\n }\n }\n return {\n source: sourceFile,\n translations,\n };\n });\n\n try {\n // Send all files in a single API call\n const { branchData } = await runUploadFilesWorkflow({\n files: uploadData,\n options: settings,\n });\n\n // Publish files to CDN if publish config exists\n await runPublishWorkflow(\n allFiles,\n publishMap,\n branchData.currentBranch.id,\n settings\n );\n } catch (error) {\n logErrorAndExit(`Error uploading files: ${error}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,OACpB,UACe;AACf,KAAI,CAAC,SAAS,MACZ;CAGF,MAAM,oBAAoB,SAAS,WAAW,EAAE;CAChD,MAAM,EACJ,eAAe,WACf,kBACA,gBACA,qBACE,SAAS;CAIb,MAAM,EAAE,OAAO,UAAU,eAAe,MAAM,eAAe,SAAS;CACtE,MAAM,qCAAqB,IAAI,KAG5B;AAEH,KAAI,UAAU,MAAM;EAClB,MAAM,kBAAkB,IAAI,IAAI,SAAS,KAAK,SAAS,KAAK,SAAS,CAAC;AACtE,OAAK,MAAM,YAAY,UAAU,MAAM;GACrC,MAAM,eAAe,YAAY,SAAS;AAC1C,OAAI,CAAC,gBAAgB,IAAI,aAAa,CAAE;AAGxC,OADmB,mBAAmB,mBAAmB,SAC3C,EAAE,UACd,oBAAmB,IAAI,cAAc;IACnC;IACA,SAAS,SAAS,SAAS;IAC5B,CAAC;;;AAKR,KAAI,SAAS,WAAW,GAAG;AACzB,SAAO,MACL,yEACD;AACD;;AAGF,KAAI,CAAC,SAAS,cACZ,QAAO,gBAAgB,qBAAqB;AAE9C,KAAI,CAAC,oBAAoB,SAAS,CAAE,QAAO,SAAS,EAAE;CAEtD,MAAM,UAAU,SAAS,WAAW,EAAE;CAEtC,MAAM,cAAc,kBAClB,WACA,kBACA,gBACA,kBACA,SACA,SAAS,cACV;CAGD,MAAM,aAAa,SAAS,KAAK,SAAS;EACxC,MAAM,aAA2B,EAAE,GAAG,MAAM;EAE5C,MAAM,eAA+B,EAAE;EACvC,MAAM,gBAAgB,mBAAmB,IAAI,KAAK,SAAS;AAE3D,OAAK,MAAM,UAAU,QACnB,KAAI,eAAe;GAEjB,MAAM,YAAY,YAChB,cAAc,SACd,cAAc,UACd,mBACA,QACA,SAAS,cACV;AACD,OAAI,UACF,cAAa,KAAK;IAChB,SAAS;IACT,UAAU,KAAK;IACf,YAAY,KAAK,mBAAmB,KAAK;IACzC,YAAY,KAAK;IACjB;IACA,QAAQ,KAAK;IACb,WAAW,KAAK;IACjB,CAAC;SAEC;GAEL,MAAM,qBAAqB,YAAY,UAAU,KAAK;AACtD,OAAI,sBAAsB,WAAW,mBAAmB,EAAE;IACxD,MAAM,oBAAoB,KAAK,mBAAmB,KAAK;IACvD,IAAI;AACJ,QAAI;AACF,yBAAoB,gBAClB,oBACA,kBACD;aACM,OAAO;KAGd,MAAM,eAAe,YAAY,mBAAmB;KACpD,MAAM,SAAS,uBAAuB,MAAM;AAC5C,YAAO,KAAK,YAAY,aAAa,IAAI,SAAS;AAClD,mBAAc,gBAAgB,cAAc,OAAO;AACnD;;AAEF,iBAAa,KAAK;KAChB,SAAS;KACT,UAAU;KACV,YAAY;KACZ,YAAY,KAAK;KACjB;KACA,QAAQ,KAAK;KACb,WAAW,KAAK;KACjB,CAAC;;;AAIR,SAAO;GACL,QAAQ;GACR;GACD;GACD;AAEF,KAAI;EAEF,MAAM,EAAE,eAAe,MAAM,uBAAuB;GAClD,OAAO;GACP,SAAS;GACV,CAAC;AAGF,QAAM,mBACJ,UACA,YACA,WAAW,cAAc,IACzB,SACD;UACM,OAAO;AACd,kBAAgB,0BAA0B,QAAQ"}
1
+ {"version":3,"file":"upload.js","names":[],"sources":["../../../src/cli/commands/upload.ts"],"sourcesContent":["import {\n fileEncodingSkipReason,\n noDefaultLocaleError,\n} from '../../console/index.js';\nimport { exitSync, logErrorAndExit } from '../../console/logging.js';\nimport { logger } from '../../console/logger.js';\nimport { getRelative, readFile } from '../../fs/findFilepath.js';\nimport { readFileContent } from '../../fs/fileContent.js';\nimport { Settings } from '../../types/index.js';\nimport { UploadOptions } from '../base.js';\nimport { extractJson } from '../../formats/json/extractJson.js';\nimport { validateJsonSchema } from '../../formats/json/utils.js';\nimport { runUploadFilesWorkflow } from '../../workflows/upload.js';\nimport { existsSync } from 'node:fs';\nimport { createFileMapping } from '../../formats/files/fileMapping.js';\nimport type { FileToUpload } from 'generaltranslation/types';\nimport { hasValidCredentials } from './utils/validation.js';\nimport { runPublishWorkflow } from '../../workflows/publish.js';\nimport { aggregateFiles } from '../../formats/files/aggregateFiles.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\nimport {\n parseXcstringsCatalog,\n serializeXcstringsSlice,\n sliceTranslationCatalog,\n type XcstringsCatalog,\n} from '../../formats/xcstrings/parseXcstrings.js';\nimport { gt } from '../../utils/gt.js';\n\n/**\n * Sends multiple files to the API for translation\n * @param settings - Translation options including API settings\n * @returns Promise that resolves when translation is complete\n */\nexport async function upload(\n settings: Settings & UploadOptions\n): Promise<void> {\n if (!settings.files) {\n return;\n }\n\n const additionalOptions = settings.options || {};\n const {\n resolvedPaths: filePaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n } = settings.files;\n\n // Reuse the same source aggregation path as translate/stage so source\n // parsing behavior stays consistent across commands.\n const { files: allFiles, publishMap } = await aggregateFiles(settings);\n const sourceFileNames = new Set(allFiles.map((file) => file.fileName));\n const compositeJsonFiles = new Map<\n string,\n { filePath: string; content: string }\n >();\n\n if (filePaths.json) {\n for (const filePath of filePaths.json) {\n const relativePath = getRelative(filePath);\n if (!sourceFileNames.has(relativePath)) continue;\n\n const jsonSchema = validateJsonSchema(additionalOptions, filePath);\n if (jsonSchema?.composite) {\n compositeJsonFiles.set(relativePath, {\n filePath,\n content: readFile(filePath),\n });\n }\n }\n }\n\n // An .xcstrings catalog carries its own translations, so each locale's\n // upload is a slice of the same file rather than a separate translation\n // file. Catalogs aggregateFiles could not parse are already reported.\n const xcstringsCatalogs = new Map<string, XcstringsCatalog>();\n if (filePaths.xcstrings) {\n for (const filePath of filePaths.xcstrings) {\n const relativePath = getRelative(filePath);\n if (!sourceFileNames.has(relativePath)) continue;\n xcstringsCatalogs.set(\n relativePath,\n parseXcstringsCatalog(readFile(filePath))\n );\n }\n }\n\n if (allFiles.length === 0) {\n logger.error(\n 'No files to upload were found. Check your configuration and try again.'\n );\n return;\n }\n\n if (!settings.defaultLocale) {\n return logErrorAndExit(noDefaultLocaleError);\n }\n if (!hasValidCredentials(settings)) return exitSync(1);\n\n const locales = settings.locales || [];\n // Create file mapping for all file types\n const fileMapping = createFileMapping(\n filePaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n locales,\n settings.defaultLocale\n );\n\n // construct object\n const uploadData = allFiles.map((file) => {\n const sourceFile: FileToUpload = { ...file };\n\n const translations: FileToUpload[] = [];\n const compositeInfo = compositeJsonFiles.get(file.fileName);\n const xcstringsCatalog = xcstringsCatalogs.get(file.fileName);\n\n for (const locale of locales) {\n if (xcstringsCatalog) {\n // Catalog keys are canonical tags; the configured locale may be an alias.\n const slice = sliceTranslationCatalog(\n xcstringsCatalog,\n gt.resolveCanonicalLocale(locale)\n );\n // A locale the catalog does not carry has nothing to upload\n if (!slice) continue;\n translations.push({\n content: serializeXcstringsSlice(slice),\n fileName: file.fileName,\n fileFormat: file.transformFormat ?? file.fileFormat,\n dataFormat: file.dataFormat,\n locale,\n fileId: file.fileId,\n versionId: file.versionId,\n });\n } else if (compositeInfo) {\n // Composite JSON: extract translations from the same source file\n const extracted = extractJson(\n compositeInfo.content,\n compositeInfo.filePath,\n additionalOptions,\n locale,\n settings.defaultLocale\n );\n if (extracted) {\n translations.push({\n content: extracted,\n fileName: file.fileName,\n fileFormat: file.transformFormat ?? file.fileFormat,\n dataFormat: file.dataFormat,\n locale,\n fileId: file.fileId,\n versionId: file.versionId,\n });\n }\n } else {\n // Non-composite: look for separate translation files\n const translatedFileName = fileMapping[locale]?.[file.fileName];\n if (translatedFileName && existsSync(translatedFileName)) {\n const translationFormat = file.transformFormat ?? file.fileFormat;\n let translatedContent: string;\n try {\n translatedContent = readFileContent(\n translatedFileName,\n translationFormat\n );\n } catch (error) {\n // One undecodable locale should not block the others: skip it and\n // report it in the summary.\n const relativePath = getRelative(translatedFileName);\n const reason = fileEncodingSkipReason(error);\n logger.warn(`Skipping ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n continue;\n }\n translations.push({\n content: translatedContent,\n fileName: translatedFileName,\n fileFormat: translationFormat,\n dataFormat: file.dataFormat,\n locale,\n fileId: file.fileId,\n versionId: file.versionId,\n });\n }\n }\n }\n return {\n source: sourceFile,\n translations,\n };\n });\n\n try {\n // Send all files in a single API call\n const { branchData } = await runUploadFilesWorkflow({\n files: uploadData,\n options: settings,\n });\n\n // Publish files to CDN if publish config exists\n await runPublishWorkflow(\n allFiles,\n publishMap,\n branchData.currentBranch.id,\n settings\n );\n } catch (error) {\n logErrorAndExit(`Error uploading files: ${error}`);\n }\n}\n"],"mappings":"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"}
@@ -45,5 +45,9 @@ export declare const invalidConfigurationError = "The files configuration cannot
45
45
  export declare const lottieTranslateError = "Lottie translations are processed asynchronously, so the translate command cannot complete them in one run. Use the stage and download flow instead:\n 1. Stage the files for translation: npx gt stage\n 2. Fetch the finished translations: npx gt download\nTranslations that are still processing are skipped, so re-run npx gt download until all of them have been downloaded.";
46
46
  export declare const branchResolutionError = "The current git branch could not be resolved. Specify a branch explicitly or run the command from a git worktree with branch metadata available.";
47
47
  export declare const lottieExpressionsError: (files: string[]) => string;
48
+ export declare const xcstringsSourceLanguageMismatchError: (catalogs: {
49
+ file: string;
50
+ sourceLanguage: string;
51
+ }[], defaultLocale: string) => string;
48
52
  export declare const fileEncodingSkipReason: (error: unknown) => string;
49
53
  export declare const withOriginalError: (message: string, error: unknown) => string;
@@ -58,9 +58,10 @@ const invalidConfigurationError = `The files configuration cannot be used for th
58
58
  const lottieTranslateError = `Lottie translations are processed asynchronously, so the translate command cannot complete them in one run. Use the stage and download flow instead:\n 1. Stage the files for translation: npx gt stage\n 2. Fetch the finished translations: npx gt download\nTranslations that are still processing are skipped, so re-run npx gt download until all of them have been downloaded.`;
59
59
  const branchResolutionError = `The current git branch could not be resolved. Specify a branch explicitly or run the command from a git worktree with branch metadata available.`;
60
60
  const lottieExpressionsError = (files) => `The following Lottie file(s) use After Effects expressions, which contain executable code and are not supported yet:\n${files.map((file) => ` - ${file}`).join("\n")}\nRe-export the animation with expressions baked into keyframes (or removed), then try again.`;
61
+ const xcstringsSourceLanguageMismatchError = (catalogs, defaultLocale) => `The following .xcstrings catalog(s) declare a sourceLanguage that does not match the configured defaultLocale "${defaultLocale}":\n${catalogs.map(({ file, sourceLanguage }) => ` - ${file} (sourceLanguage "${sourceLanguage}")`).join("\n")}\nSet defaultLocale in gt.config.json to the catalog's sourceLanguage, or change the catalog's source language in Xcode, then try again.`;
61
62
  const fileEncodingSkipReason = (error) => `File could not be decoded: ${error instanceof Error ? error.message : String(error)}`;
62
63
  const withOriginalError = (message, error) => error != null ? `${message} Original error: ${String(error)}` : message;
63
64
  //#endregion
64
- export { branchResolutionError, devApiKeyError, fileEncodingSkipReason, invalidConfigurationError, lottieExpressionsError, lottieTranslateError, noApiKeyError, noDefaultLocaleError, noFilesError, noLocalesError, noProjectIdError, noSourceFileError, noVersionIdError, warnApiKeyInConfigSync, warnAsyncUseGT, warnAutoderiveNoResultsSync, warnDataAttrOnBranch, warnDeprecatedTSugarPropsSync, warnDeriveCircularSpreadSync, warnDeriveDestructuringSync, warnDeriveFunctionNoResultsSync, warnDeriveFunctionNotWrappedSync, warnDeriveNonConstVariableSync, warnDeriveOptionalChainingSync, warnDeriveUnresolvableValueSync, warnDuplicateFunctionDefinitionSync, warnFailedToConstructJsxTreeSync, warnFunctionNotFoundSync, warnHasUnwrappedExpressionSync, warnInvalidDeclareVarNameSync, warnInvalidDeriveInitSync, warnInvalidFormatSync, warnInvalidIcuSync, warnInvalidMaxCharsSync, warnInvalidRequiresReviewSync, warnInvalidReturnSync, warnMissingReturnSync, warnNestedInternalTComponent, warnNestedTComponent, warnNonStaticExpressionSync, warnNonStringSync, warnRecursiveFunctionCallSync, warnSyncGetGT, warnTemplateLiteralSync, warnTernarySync, warnUnresolvedImportSync, warnVariablePropSync, withLocation, withOriginalError };
65
+ export { branchResolutionError, devApiKeyError, fileEncodingSkipReason, invalidConfigurationError, lottieExpressionsError, lottieTranslateError, noApiKeyError, noDefaultLocaleError, noFilesError, noLocalesError, noProjectIdError, noSourceFileError, noVersionIdError, warnApiKeyInConfigSync, warnAsyncUseGT, warnAutoderiveNoResultsSync, warnDataAttrOnBranch, warnDeprecatedTSugarPropsSync, warnDeriveCircularSpreadSync, warnDeriveDestructuringSync, warnDeriveFunctionNoResultsSync, warnDeriveFunctionNotWrappedSync, warnDeriveNonConstVariableSync, warnDeriveOptionalChainingSync, warnDeriveUnresolvableValueSync, warnDuplicateFunctionDefinitionSync, warnFailedToConstructJsxTreeSync, warnFunctionNotFoundSync, warnHasUnwrappedExpressionSync, warnInvalidDeclareVarNameSync, warnInvalidDeriveInitSync, warnInvalidFormatSync, warnInvalidIcuSync, warnInvalidMaxCharsSync, warnInvalidRequiresReviewSync, warnInvalidReturnSync, warnMissingReturnSync, warnNestedInternalTComponent, warnNestedTComponent, warnNonStaticExpressionSync, warnNonStringSync, warnRecursiveFunctionCallSync, warnSyncGetGT, warnTemplateLiteralSync, warnTernarySync, warnUnresolvedImportSync, warnVariablePropSync, withLocation, withOriginalError, xcstringsSourceLanguageMismatchError };
65
66
 
66
67
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/console/index.ts"],"sourcesContent":["import { createDiagnosticMessage } from 'generaltranslation/internal';\nimport { BRANCH_COMPONENT } from '../react/jsx/utils/constants.js';\nimport {\n colorizeFilepath,\n colorizeComponent,\n colorizeIdString,\n colorizeContent,\n colorizeLine,\n colorizeFunctionName,\n} from './colors.js';\nimport { formatCodeClamp } from './formatting.js';\n\nconst withWillErrorInNextVersion = (message: string): string =>\n `${message} (This will become an error in the next major version of the CLI.)`;\n\n// Derive function related errors\nconst withDeriveComponentError = (message: string): string =>\n `<Derive> rules violation: ${message}`;\n\nconst withDeriveFunctionError = (message: string): string =>\n `derive() rules violation: ${message}`;\n// Synchronous wrappers for backward compatibility\nexport const warnApiKeyInConfigSync = (optionsFilepath: string): string =>\n `${colorizeFilepath(\n optionsFilepath\n )}: Your API key is exposed! Remove it from the file and include it as an environment variable.`;\n\nexport const warnVariablePropSync = (\n file: string,\n attrName: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent('<T>')} component has dynamic attribute ${colorizeIdString(attrName)} with value: ${colorizeContent(\n value\n )}. Change ${colorizeIdString(attrName)} to ensure this content is translated.`,\n location\n );\n\nexport const warnInvalidReturnSync = (\n file: string,\n functionName: string,\n expression: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `Function ${colorizeFunctionName(functionName)} does not return a derivable (statically analyzable) expression. ${colorizeFunctionName(functionName)} must return either (1) a derivable string literal, (2) another derivable function invocation, (3) derivable JSX content, or (4) a ternary expression. Instead got:\\n${colorizeContent(expression)}`\n ),\n location\n );\n\n// TODO: this is temporary until we handle implicit returns\nexport const warnMissingReturnSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} is wrapped in ${colorizeComponent('<Derive>')} tags but does not have an explicit return statement. Derivable functions must have an explicit return statement.`,\n location\n );\n\nexport const warnHasUnwrappedExpressionSync = (\n file: string,\n unwrappedExpressions: string[],\n id?: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent('<T>')} component${\n id ? ` with id ${colorizeIdString(id)}` : ''\n } has children that could change at runtime. Use a variable component like ${colorizeComponent(\n '<Var>'\n )} to ensure this content is translated.\\n${colorizeContent(\n unwrappedExpressions.join('\\n')\n )}`,\n location\n );\n\nexport const warnFailedToConstructJsxTreeSync = (\n file: string,\n code: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Failed to construct JsxTree! Call expression is not a valid createElement call: ${colorizeContent(code)}`,\n location\n );\n\nexport const warnNestedTComponent = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found nested <T> component. <T> components cannot be directly nested.`,\n location\n );\n\nexport const warnNestedInternalTComponent = (\n file: string,\n location?: string\n): string =>\n withLocation(\n file,\n `DEBUG: Found nested <GtInternalTranslateJsx> component. <GtInternalTranslateJsx> components cannot be directly nested.`,\n location\n );\n\nexport const warnNonStaticExpressionSync = (\n file: string,\n attrName: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found non-static expression for attribute ${colorizeIdString(\n attrName\n )}: ${colorizeContent(value)}. Change \"${colorizeIdString(attrName)}\" to ensure this content is translated.`,\n location\n );\n\nexport const warnInvalidMaxCharsSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid maxChars value: ${colorizeContent(value)}. maxChars must be a numeric literal, e.g. maxChars={10}, to ensure this content is translated.`,\n location\n );\n\nexport const warnDeprecatedTSugarPropsSync = (locations: string[]): string =>\n createDiagnosticMessage({\n whatHappened: `Found ${locations.length} deprecated ${colorizeIdString(\n '$'\n )}-prefixed prop${locations.length === 1 ? '' : 's'} on ${colorizeComponent(\n '<T>'\n )} components`,\n fix: `Use the unprefixed forms instead (${colorizeIdString(\n 'context'\n )}, ${colorizeIdString('id')}, ${colorizeIdString('maxChars')}, ${colorizeIdString(\n 'requiresReview'\n )}); support for the ${colorizeIdString('$')}-prefixed forms will be removed in the next major version`,\n }) +\n '\\n' +\n locations.map((location) => ` ${colorizeLine(location)}`).join('\\n');\n\nexport const warnInvalidFormatSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid $format value: ${colorizeContent(value)}. Must be one of: 'ICU', 'STRING', 'I18NEXT'.`,\n location\n );\n\nexport const warnInvalidRequiresReviewSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid requiresReview value: ${colorizeContent(value)}. Must be a boolean literal (true or false) — strings and dynamic expressions are not allowed.`,\n location\n );\n\nexport const warnInvalidIcuSync = (\n file: string,\n value: string,\n error: string,\n location?: string\n): string =>\n withWillErrorInNextVersion(\n withLocation(\n file,\n `Found invalid ICU string: ${colorizeContent(value)}. Change the value to a valid ICU to ensure this content is translated. Error message: ${error}.`,\n location\n )\n );\n\nexport const warnTemplateLiteralSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found template literal with quasis (${colorizeContent(value)}). Change the template literal to a string to ensure this content is translated.`,\n location\n );\n\nexport const warnNonStringSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found non-string literal (${colorizeContent(value)}). Change the value to a string literal to ensure this content is translated.`,\n location\n );\n\nexport const warnAsyncUseGT = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found useGT() in an async function. Use getGT() instead, or make the function synchronous.`,\n location\n );\n\nexport const warnSyncGetGT = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found getGT() in a synchronous function. Use useGT() instead, or make the function async.`,\n location\n );\n\nexport const warnTernarySync = (file: string, location?: string): string =>\n withLocation(\n file,\n 'Found ternary expression. A Branch component may be more appropriate here.',\n location\n );\n\nexport const withLocation = (\n file: string,\n message: string,\n location?: string\n): string =>\n `${colorizeFilepath(file)}${location ? ` (${colorizeLine(location)})` : ''}: ${message}`;\n\nexport const warnFunctionNotFoundSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} definition could not be resolved. This might affect translation resolution for this ${colorizeComponent('<T>')} component.`,\n location\n );\n\nexport const warnInvalidDeclareVarNameSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid declareVar() $name tag. Must be a derivable (statically analyzable) expression. Received: ${colorizeContent(value)}.`,\n location\n );\n\nexport const warnDuplicateFunctionDefinitionSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} is defined multiple times. Only the first definition will be used.`,\n location\n );\n\nexport const warnInvalidDeriveInitSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `The definition for ${colorizeFunctionName(functionName)} could not be resolved. When using arrow syntax to define a derivable (statically analyzable) function, the right hand side or the assignment MUST only contain the arrow function itself and no other expressions.\nExample: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = () => { ... }`)}\nInvalid: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = [() => { ... }][0]`)}`\n ),\n location\n );\n\nexport const warnDataAttrOnBranch = (\n file: string,\n attrName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent(`<${BRANCH_COMPONENT}>`)} component ignores attributes prefixed with ${colorizeIdString('\"data-\"')}. Found ${colorizeIdString(attrName)}. Remove it or use a different attribute name.`,\n location\n );\n\nexport const warnRecursiveFunctionCallSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `Recursive function call detected: ${colorizeFunctionName(functionName)}. A derivable (statically analyzable) function cannot use recursive calls to construct its result.`\n ),\n location\n );\n\nexport const warnDeriveFunctionNotWrappedSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. This call is not wrapped in derive(). Ensure the function is properly wrapped with derive() and does not have circular import dependencies.`\n ),\n location\n );\n\nexport const warnDeriveNonConstVariableSync = (\n file: string,\n varName: string,\n kind: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Variable ${colorizeFunctionName(varName)} is declared with '${kind}' but only 'const' declarations can be resolved statically. Change it to 'const'.`\n ),\n location\n );\n\nexport const warnDeriveFunctionNoResultsSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. derive() can only receive function invocations and cannot use undefined values or looped calls to construct its result.`\n ),\n location\n );\n\nexport const warnAutoderiveNoResultsSync = (\n file: string,\n expression: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Autoderive could not resolve ${colorizeFunctionName(formatCodeClamp(expression))}. Only function calls with statically determinable return values can be used directly in t(). Consider wrapping with derive() for explicit derivation, or use an interpolation variable instead.`,\n location\n );\n\nexport const warnDeriveUnresolvableValueSync = (\n file: string,\n key: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Object property ${colorizeFunctionName(formatCodeClamp(key))} could not be resolved to a static string value. Only string literals, template literals, conditionals, and function calls returning strings are supported.`\n ),\n location\n );\n\nexport const warnDeriveCircularSpreadSync = (\n file: string,\n varName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Circular spread detected involving ${colorizeFunctionName(varName)}. Spread references that form a cycle cannot be resolved statically.`\n ),\n location\n );\n\nexport const warnDeriveDestructuringSync = (\n file: string,\n varName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Variable ${colorizeFunctionName(varName)} uses destructuring syntax, which is not yet supported in derive(). Assign the value to a const variable directly instead.`\n ),\n location\n );\n\nexport const warnDeriveOptionalChainingSync = (\n file: string,\n code: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Optional chaining (${colorizeFunctionName(formatCodeClamp(code))}) is not supported in derive(). Optional chaining implies the value could be undefined, which cannot be resolved statically. Use a non-optional access instead.`\n ),\n location\n );\n\nexport const warnUnresolvedImportSync = (\n file: string,\n functionName: string,\n importPath: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Could not resolve import ${colorizeFunctionName(importPath)} for function ${colorizeFunctionName(functionName)}. Translation strings inside this function will not be extracted.`,\n location\n );\n\n// Re-export error messages\nexport const noLocalesError = `No target locales were found. Add locales to gt.config.json or pass them with the --locales flag.`;\nexport const noDefaultLocaleError = `No default locale was found. Add defaultLocale to gt.config.json or pass it with --default-locale.`;\nexport const noFilesError = `The files configuration is missing or invalid. Check the files section in gt.config.json and try again.`;\nexport const noSourceFileError = `No source translation file was found. Check your translations directory and default locale configuration.`;\nexport const noApiKeyError = `No API key was found. Pass --api-key or set the GT_API_KEY environment variable.`;\nexport const devApiKeyError = `Development API keys cannot be used with the General Translation API. Use a production API key instead.\\nGenerate a production API key with: npx gt auth -t production`;\nexport const noProjectIdError = `No project ID was found. Pass --project-id, add projectId to gt.config.json, or set the GT_PROJECT_ID environment variable.`;\nexport const noVersionIdError = `No version ID was found. Pass --version-id or add _versionId to gt.config.json.`;\nexport const invalidConfigurationError = `The files configuration cannot be used for this operation. Provide a valid download configuration or set --publish true to upload translations to the CDN.`;\nexport const lottieTranslateError = `Lottie translations are processed asynchronously, so the translate command cannot complete them in one run. Use the stage and download flow instead:\\n 1. Stage the files for translation: npx gt stage\\n 2. Fetch the finished translations: npx gt download\\nTranslations that are still processing are skipped, so re-run npx gt download until all of them have been downloaded.`;\nexport const branchResolutionError = `The current git branch could not be resolved. Specify a branch explicitly or run the command from a git worktree with branch metadata available.`;\n\nexport const lottieExpressionsError = (files: string[]): string =>\n `The following Lottie file(s) use After Effects expressions, which contain executable code and are not supported yet:\\n${files\n .map((file) => ` - ${file}`)\n .join(\n '\\n'\n )}\\nRe-export the animation with expressions baked into keyframes (or removed), then try again.`;\n\nexport const fileEncodingSkipReason = (error: unknown): string =>\n `File could not be decoded: ${error instanceof Error ? error.message : String(error)}`;\n\nexport const withOriginalError = (message: string, error: unknown): string =>\n error != null ? `${message} Original error: ${String(error)}` : message;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/console/index.ts"],"sourcesContent":["import { createDiagnosticMessage } from 'generaltranslation/internal';\nimport { BRANCH_COMPONENT } from '../react/jsx/utils/constants.js';\nimport {\n colorizeFilepath,\n colorizeComponent,\n colorizeIdString,\n colorizeContent,\n colorizeLine,\n colorizeFunctionName,\n} from './colors.js';\nimport { formatCodeClamp } from './formatting.js';\n\nconst withWillErrorInNextVersion = (message: string): string =>\n `${message} (This will become an error in the next major version of the CLI.)`;\n\n// Derive function related errors\nconst withDeriveComponentError = (message: string): string =>\n `<Derive> rules violation: ${message}`;\n\nconst withDeriveFunctionError = (message: string): string =>\n `derive() rules violation: ${message}`;\n// Synchronous wrappers for backward compatibility\nexport const warnApiKeyInConfigSync = (optionsFilepath: string): string =>\n `${colorizeFilepath(\n optionsFilepath\n )}: Your API key is exposed! Remove it from the file and include it as an environment variable.`;\n\nexport const warnVariablePropSync = (\n file: string,\n attrName: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent('<T>')} component has dynamic attribute ${colorizeIdString(attrName)} with value: ${colorizeContent(\n value\n )}. Change ${colorizeIdString(attrName)} to ensure this content is translated.`,\n location\n );\n\nexport const warnInvalidReturnSync = (\n file: string,\n functionName: string,\n expression: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `Function ${colorizeFunctionName(functionName)} does not return a derivable (statically analyzable) expression. ${colorizeFunctionName(functionName)} must return either (1) a derivable string literal, (2) another derivable function invocation, (3) derivable JSX content, or (4) a ternary expression. Instead got:\\n${colorizeContent(expression)}`\n ),\n location\n );\n\n// TODO: this is temporary until we handle implicit returns\nexport const warnMissingReturnSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} is wrapped in ${colorizeComponent('<Derive>')} tags but does not have an explicit return statement. Derivable functions must have an explicit return statement.`,\n location\n );\n\nexport const warnHasUnwrappedExpressionSync = (\n file: string,\n unwrappedExpressions: string[],\n id?: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent('<T>')} component${\n id ? ` with id ${colorizeIdString(id)}` : ''\n } has children that could change at runtime. Use a variable component like ${colorizeComponent(\n '<Var>'\n )} to ensure this content is translated.\\n${colorizeContent(\n unwrappedExpressions.join('\\n')\n )}`,\n location\n );\n\nexport const warnFailedToConstructJsxTreeSync = (\n file: string,\n code: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Failed to construct JsxTree! Call expression is not a valid createElement call: ${colorizeContent(code)}`,\n location\n );\n\nexport const warnNestedTComponent = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found nested <T> component. <T> components cannot be directly nested.`,\n location\n );\n\nexport const warnNestedInternalTComponent = (\n file: string,\n location?: string\n): string =>\n withLocation(\n file,\n `DEBUG: Found nested <GtInternalTranslateJsx> component. <GtInternalTranslateJsx> components cannot be directly nested.`,\n location\n );\n\nexport const warnNonStaticExpressionSync = (\n file: string,\n attrName: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found non-static expression for attribute ${colorizeIdString(\n attrName\n )}: ${colorizeContent(value)}. Change \"${colorizeIdString(attrName)}\" to ensure this content is translated.`,\n location\n );\n\nexport const warnInvalidMaxCharsSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid maxChars value: ${colorizeContent(value)}. maxChars must be a numeric literal, e.g. maxChars={10}, to ensure this content is translated.`,\n location\n );\n\nexport const warnDeprecatedTSugarPropsSync = (locations: string[]): string =>\n createDiagnosticMessage({\n whatHappened: `Found ${locations.length} deprecated ${colorizeIdString(\n '$'\n )}-prefixed prop${locations.length === 1 ? '' : 's'} on ${colorizeComponent(\n '<T>'\n )} components`,\n fix: `Use the unprefixed forms instead (${colorizeIdString(\n 'context'\n )}, ${colorizeIdString('id')}, ${colorizeIdString('maxChars')}, ${colorizeIdString(\n 'requiresReview'\n )}); support for the ${colorizeIdString('$')}-prefixed forms will be removed in the next major version`,\n }) +\n '\\n' +\n locations.map((location) => ` ${colorizeLine(location)}`).join('\\n');\n\nexport const warnInvalidFormatSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid $format value: ${colorizeContent(value)}. Must be one of: 'ICU', 'STRING', 'I18NEXT'.`,\n location\n );\n\nexport const warnInvalidRequiresReviewSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid requiresReview value: ${colorizeContent(value)}. Must be a boolean literal (true or false) — strings and dynamic expressions are not allowed.`,\n location\n );\n\nexport const warnInvalidIcuSync = (\n file: string,\n value: string,\n error: string,\n location?: string\n): string =>\n withWillErrorInNextVersion(\n withLocation(\n file,\n `Found invalid ICU string: ${colorizeContent(value)}. Change the value to a valid ICU to ensure this content is translated. Error message: ${error}.`,\n location\n )\n );\n\nexport const warnTemplateLiteralSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found template literal with quasis (${colorizeContent(value)}). Change the template literal to a string to ensure this content is translated.`,\n location\n );\n\nexport const warnNonStringSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found non-string literal (${colorizeContent(value)}). Change the value to a string literal to ensure this content is translated.`,\n location\n );\n\nexport const warnAsyncUseGT = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found useGT() in an async function. Use getGT() instead, or make the function synchronous.`,\n location\n );\n\nexport const warnSyncGetGT = (file: string, location?: string): string =>\n withLocation(\n file,\n `Found getGT() in a synchronous function. Use useGT() instead, or make the function async.`,\n location\n );\n\nexport const warnTernarySync = (file: string, location?: string): string =>\n withLocation(\n file,\n 'Found ternary expression. A Branch component may be more appropriate here.',\n location\n );\n\nexport const withLocation = (\n file: string,\n message: string,\n location?: string\n): string =>\n `${colorizeFilepath(file)}${location ? ` (${colorizeLine(location)})` : ''}: ${message}`;\n\nexport const warnFunctionNotFoundSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} definition could not be resolved. This might affect translation resolution for this ${colorizeComponent('<T>')} component.`,\n location\n );\n\nexport const warnInvalidDeclareVarNameSync = (\n file: string,\n value: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Found invalid declareVar() $name tag. Must be a derivable (statically analyzable) expression. Received: ${colorizeContent(value)}.`,\n location\n );\n\nexport const warnDuplicateFunctionDefinitionSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Function ${colorizeFunctionName(functionName)} is defined multiple times. Only the first definition will be used.`,\n location\n );\n\nexport const warnInvalidDeriveInitSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `The definition for ${colorizeFunctionName(functionName)} could not be resolved. When using arrow syntax to define a derivable (statically analyzable) function, the right hand side or the assignment MUST only contain the arrow function itself and no other expressions.\nExample: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = () => { ... }`)}\nInvalid: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = [() => { ... }][0]`)}`\n ),\n location\n );\n\nexport const warnDataAttrOnBranch = (\n file: string,\n attrName: string,\n location?: string\n): string =>\n withLocation(\n file,\n `${colorizeComponent(`<${BRANCH_COMPONENT}>`)} component ignores attributes prefixed with ${colorizeIdString('\"data-\"')}. Found ${colorizeIdString(attrName)}. Remove it or use a different attribute name.`,\n location\n );\n\nexport const warnRecursiveFunctionCallSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveComponentError(\n `Recursive function call detected: ${colorizeFunctionName(functionName)}. A derivable (statically analyzable) function cannot use recursive calls to construct its result.`\n ),\n location\n );\n\nexport const warnDeriveFunctionNotWrappedSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. This call is not wrapped in derive(). Ensure the function is properly wrapped with derive() and does not have circular import dependencies.`\n ),\n location\n );\n\nexport const warnDeriveNonConstVariableSync = (\n file: string,\n varName: string,\n kind: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Variable ${colorizeFunctionName(varName)} is declared with '${kind}' but only 'const' declarations can be resolved statically. Change it to 'const'.`\n ),\n location\n );\n\nexport const warnDeriveFunctionNoResultsSync = (\n file: string,\n functionName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. derive() can only receive function invocations and cannot use undefined values or looped calls to construct its result.`\n ),\n location\n );\n\nexport const warnAutoderiveNoResultsSync = (\n file: string,\n expression: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Autoderive could not resolve ${colorizeFunctionName(formatCodeClamp(expression))}. Only function calls with statically determinable return values can be used directly in t(). Consider wrapping with derive() for explicit derivation, or use an interpolation variable instead.`,\n location\n );\n\nexport const warnDeriveUnresolvableValueSync = (\n file: string,\n key: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Object property ${colorizeFunctionName(formatCodeClamp(key))} could not be resolved to a static string value. Only string literals, template literals, conditionals, and function calls returning strings are supported.`\n ),\n location\n );\n\nexport const warnDeriveCircularSpreadSync = (\n file: string,\n varName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Circular spread detected involving ${colorizeFunctionName(varName)}. Spread references that form a cycle cannot be resolved statically.`\n ),\n location\n );\n\nexport const warnDeriveDestructuringSync = (\n file: string,\n varName: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Variable ${colorizeFunctionName(varName)} uses destructuring syntax, which is not yet supported in derive(). Assign the value to a const variable directly instead.`\n ),\n location\n );\n\nexport const warnDeriveOptionalChainingSync = (\n file: string,\n code: string,\n location?: string\n): string =>\n withLocation(\n file,\n withDeriveFunctionError(\n `Optional chaining (${colorizeFunctionName(formatCodeClamp(code))}) is not supported in derive(). Optional chaining implies the value could be undefined, which cannot be resolved statically. Use a non-optional access instead.`\n ),\n location\n );\n\nexport const warnUnresolvedImportSync = (\n file: string,\n functionName: string,\n importPath: string,\n location?: string\n): string =>\n withLocation(\n file,\n `Could not resolve import ${colorizeFunctionName(importPath)} for function ${colorizeFunctionName(functionName)}. Translation strings inside this function will not be extracted.`,\n location\n );\n\n// Re-export error messages\nexport const noLocalesError = `No target locales were found. Add locales to gt.config.json or pass them with the --locales flag.`;\nexport const noDefaultLocaleError = `No default locale was found. Add defaultLocale to gt.config.json or pass it with --default-locale.`;\nexport const noFilesError = `The files configuration is missing or invalid. Check the files section in gt.config.json and try again.`;\nexport const noSourceFileError = `No source translation file was found. Check your translations directory and default locale configuration.`;\nexport const noApiKeyError = `No API key was found. Pass --api-key or set the GT_API_KEY environment variable.`;\nexport const devApiKeyError = `Development API keys cannot be used with the General Translation API. Use a production API key instead.\\nGenerate a production API key with: npx gt auth -t production`;\nexport const noProjectIdError = `No project ID was found. Pass --project-id, add projectId to gt.config.json, or set the GT_PROJECT_ID environment variable.`;\nexport const noVersionIdError = `No version ID was found. Pass --version-id or add _versionId to gt.config.json.`;\nexport const invalidConfigurationError = `The files configuration cannot be used for this operation. Provide a valid download configuration or set --publish true to upload translations to the CDN.`;\nexport const lottieTranslateError = `Lottie translations are processed asynchronously, so the translate command cannot complete them in one run. Use the stage and download flow instead:\\n 1. Stage the files for translation: npx gt stage\\n 2. Fetch the finished translations: npx gt download\\nTranslations that are still processing are skipped, so re-run npx gt download until all of them have been downloaded.`;\nexport const branchResolutionError = `The current git branch could not be resolved. Specify a branch explicitly or run the command from a git worktree with branch metadata available.`;\n\nexport const lottieExpressionsError = (files: string[]): string =>\n `The following Lottie file(s) use After Effects expressions, which contain executable code and are not supported yet:\\n${files\n .map((file) => ` - ${file}`)\n .join(\n '\\n'\n )}\\nRe-export the animation with expressions baked into keyframes (or removed), then try again.`;\n\nexport const xcstringsSourceLanguageMismatchError = (\n catalogs: { file: string; sourceLanguage: string }[],\n defaultLocale: string\n): string =>\n `The following .xcstrings catalog(s) declare a sourceLanguage that does not match the configured defaultLocale \"${defaultLocale}\":\\n${catalogs\n .map(\n ({ file, sourceLanguage }) =>\n ` - ${file} (sourceLanguage \"${sourceLanguage}\")`\n )\n .join(\n '\\n'\n )}\\nSet defaultLocale in gt.config.json to the catalog's sourceLanguage, or change the catalog's source language in Xcode, then try again.`;\n\nexport const fileEncodingSkipReason = (error: unknown): string =>\n `File could not be decoded: ${error instanceof Error ? error.message : String(error)}`;\n\nexport const withOriginalError = (message: string, error: unknown): string =>\n error != null ? `${message} Original error: ${String(error)}` : message;\n"],"mappings":"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"}