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.
- package/CHANGELOG.md +16 -0
- package/dist/api/collectUserEditDiffs.d.ts +2 -0
- package/dist/api/collectUserEditDiffs.js +31 -8
- package/dist/api/collectUserEditDiffs.js.map +1 -1
- package/dist/api/downloadFileBatch.js +13 -6
- package/dist/api/downloadFileBatch.js.map +1 -1
- package/dist/cli/commands/upload.js +30 -11
- package/dist/cli/commands/upload.js.map +1 -1
- package/dist/console/index.d.ts +4 -0
- package/dist/console/index.js +2 -1
- package/dist/console/index.js.map +1 -1
- package/dist/formats/files/aggregateFiles.js +39 -2
- package/dist/formats/files/aggregateFiles.js.map +1 -1
- package/dist/formats/files/localeContent.d.ts +18 -0
- package/dist/formats/files/localeContent.js +34 -0
- package/dist/formats/files/localeContent.js.map +1 -0
- package/dist/formats/files/supportedFiles.d.ts +2 -1
- package/dist/formats/files/supportedFiles.js +4 -2
- package/dist/formats/files/supportedFiles.js.map +1 -1
- package/dist/formats/files/transformFormat.d.ts +2 -0
- package/dist/formats/files/transformFormat.js +6 -3
- package/dist/formats/files/transformFormat.js.map +1 -1
- package/dist/formats/xcstrings/mergeXcstrings.d.ts +13 -0
- package/dist/formats/xcstrings/mergeXcstrings.js +71 -0
- package/dist/formats/xcstrings/mergeXcstrings.js.map +1 -0
- package/dist/formats/xcstrings/parseXcstrings.d.ts +43 -0
- package/dist/formats/xcstrings/parseXcstrings.js +123 -0
- package/dist/formats/xcstrings/parseXcstrings.js.map +1 -0
- package/dist/fs/config/parseFilesConfig.d.ts +2 -2
- package/dist/fs/config/parseFilesConfig.js +17 -3
- package/dist/fs/config/parseFilesConfig.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/state/recentDownloads.d.ts +1 -1
- package/dist/state/recentDownloads.js +3 -1
- package/dist/state/recentDownloads.js.map +1 -1
- package/dist/utils/persistPostprocessHashes.d.ts +1 -1
- package/dist/utils/persistPostprocessHashes.js +18 -14
- package/dist/utils/persistPostprocessHashes.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { logger } from "../../console/logger.js";
|
|
2
2
|
import { hashStringSync, hashVersionId } from "../../utils/hash.js";
|
|
3
|
+
import { gt } from "../../utils/gt.js";
|
|
3
4
|
import { logErrorAndExit } from "../../console/logging.js";
|
|
4
5
|
import { getRelative, readBinaryFileBase64, readFile } from "../../fs/findFilepath.js";
|
|
5
|
-
import { fileEncodingSkipReason, lottieExpressionsError } from "../../console/index.js";
|
|
6
|
+
import { fileEncodingSkipReason, lottieExpressionsError, xcstringsSourceLanguageMismatchError } from "../../console/index.js";
|
|
6
7
|
import { SUPPORTED_FILE_EXTENSIONS } from "./supportedFiles.js";
|
|
7
8
|
import { getTransformFormatProperty } from "./transformFormat.js";
|
|
8
9
|
import { determineLibrary } from "../../fs/determineFramework/index.js";
|
|
@@ -12,6 +13,7 @@ import { recordWarning } from "../../state/translateWarnings.js";
|
|
|
12
13
|
import { buildPublishMap } from "../../utils/resolvePublish.js";
|
|
13
14
|
import { lottieHasExpressions } from "./detectLottieExpressions.js";
|
|
14
15
|
import { parseJson } from "../json/parseJson.js";
|
|
16
|
+
import { parseXcstringsCatalog, serializeXcstringsSlice, sliceSourceCatalog } from "../xcstrings/parseXcstrings.js";
|
|
15
17
|
import { resolveMintlifyRefs, shouldResolveRefs } from "../../utils/resolveMintlifyRefs.js";
|
|
16
18
|
import { storeRefMap } from "../../state/mintlifyRefMap.js";
|
|
17
19
|
import { validateYamlSchema } from "../yaml/utils.js";
|
|
@@ -245,8 +247,43 @@ async function aggregateFiles(settings) {
|
|
|
245
247
|
});
|
|
246
248
|
files.push(...verbatimFiles.filter((file) => file !== null));
|
|
247
249
|
}
|
|
250
|
+
if (filePaths.xcstrings) {
|
|
251
|
+
const sourceLanguageMismatches = [];
|
|
252
|
+
const xcstringsFiles = filePaths.xcstrings.map((filePath) => {
|
|
253
|
+
const content = readFile(filePath);
|
|
254
|
+
const relativePath = getRelative(filePath);
|
|
255
|
+
let catalog;
|
|
256
|
+
try {
|
|
257
|
+
catalog = parseXcstringsCatalog(content);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const reason = error instanceof Error ? error.message : "xcstrings file is not parsable";
|
|
260
|
+
logger.warn(`Skipping ${relativePath}: ${reason}`);
|
|
261
|
+
recordWarning("skipped_file", relativePath, reason);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (gt.resolveCanonicalLocale(catalog.sourceLanguage) !== gt.resolveCanonicalLocale(settings.defaultLocale)) {
|
|
265
|
+
sourceLanguageMismatches.push({
|
|
266
|
+
file: relativePath,
|
|
267
|
+
sourceLanguage: catalog.sourceLanguage
|
|
268
|
+
});
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const sourceSlice = serializeXcstringsSlice(sliceSourceCatalog(catalog));
|
|
272
|
+
return {
|
|
273
|
+
content: sourceSlice,
|
|
274
|
+
fileName: relativePath,
|
|
275
|
+
fileFormat: "XCSTRINGS",
|
|
276
|
+
...getTransformFormatProperty(settings, "xcstrings"),
|
|
277
|
+
fileId: hashStringSync(relativePath),
|
|
278
|
+
versionId: hashVersionId(sourceSlice, requiresReviewPaths.has(filePath)),
|
|
279
|
+
locale: settings.defaultLocale
|
|
280
|
+
};
|
|
281
|
+
}).filter((file) => file !== null);
|
|
282
|
+
if (sourceLanguageMismatches.length > 0) logErrorAndExit(xcstringsSourceLanguageMismatchError(sourceLanguageMismatches, settings.defaultLocale));
|
|
283
|
+
files.push(...xcstringsFiles);
|
|
284
|
+
}
|
|
248
285
|
for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
|
|
249
|
-
if (fileType === "json" || fileType === "yaml" || fileType === "twilioContentJson" || fileType === "lottie" || fileType === "dotStrings" || fileType === "dotStringsdict" || fileType === "androidStrings") continue;
|
|
286
|
+
if (fileType === "json" || fileType === "yaml" || fileType === "twilioContentJson" || fileType === "lottie" || fileType === "dotStrings" || fileType === "dotStringsdict" || fileType === "androidStrings" || fileType === "xcstrings") continue;
|
|
250
287
|
if (filePaths[fileType]) {
|
|
251
288
|
const parsed = filePaths[fileType].map((filePath) => {
|
|
252
289
|
const content = readFile(filePath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregateFiles.js","names":[],"sources":["../../../src/formats/files/aggregateFiles.ts"],"sourcesContent":["import { logger } from '../../console/logger.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport {\n fileEncodingSkipReason,\n lottieExpressionsError,\n} from '../../console/index.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\nimport { lottieHasExpressions } from './detectLottieExpressions.js';\nimport {\n getRelative,\n readFile,\n readBinaryFileBase64,\n} from '../../fs/findFilepath.js';\nimport { readFileContent } from '../../fs/fileContent.js';\nimport { Settings } from '../../types/index.js';\nimport type { FileFormat, DataFormat, FileToUpload } from '../../types/data.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';\nimport { parseJson } from '../json/parseJson.js';\nimport {\n resolveMintlifyRefs,\n shouldResolveRefs,\n} from '../../utils/resolveMintlifyRefs.js';\nimport { storeRefMap } from '../../state/mintlifyRefMap.js';\nimport parseYaml from '../yaml/parseYaml.js';\nimport { validateYamlSchema } from '../yaml/utils.js';\nimport { flattenJson } from '../json/flattenJson.js';\nimport type { JSONObject } from '../../types/data/json.js';\nimport YAML from 'yaml';\nimport { determineLibrary } from '../../fs/determineFramework/index.js';\nimport { hashStringSync, hashVersionId } from '../../utils/hash.js';\nimport { preprocessContent } from './preprocessContent.js';\nimport {\n parseKeyedMetadata,\n type KeyedMetadata,\n} from '../parseKeyedMetadata.js';\nimport { buildPublishMap } from '../../utils/resolvePublish.js';\nimport { getTransformFormatProperty } from './transformFormat.js';\n\n/**\n * Checks if a file path is a metadata companion file (e.g. foo.metadata.json)\n * AND its corresponding source file (e.g. foo.json) exists in the file list.\n * If both conditions are true, the metadata file should be skipped as a translation source.\n */\nfunction isCompanionMetadataFile(\n filePath: string,\n allFilePaths: string[]\n): boolean {\n const metadataPattern = /\\.metadata\\.(json|yaml|yml)$/;\n if (!metadataPattern.test(filePath)) return false;\n\n // Derive the source file path: foo.metadata.json -> foo.json\n const sourceFilePath = filePath.replace(\n /\\.metadata\\.(json|yaml|yml)$/,\n '.$1'\n );\n return allFilePaths.includes(sourceFilePath);\n}\n\nexport async function aggregateFiles(\n settings: Settings\n): Promise<{ files: FileToUpload[]; publishMap: Map<string, boolean> }> {\n // Aggregate all files to translate\n const files: FileToUpload[] = [];\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return { files, publishMap: new Map<string, boolean>() };\n }\n\n const { resolvedPaths: filePaths } = settings.files;\n // Tolerate partially-constructed settings from programmatic callers\n const requiresReviewPaths =\n settings.files.requiresReviewPaths ?? new Set<string>();\n const skipValidation = settings.options?.skipFileValidation;\n\n // Build publish map upfront from resolved paths.\n const publishMap = buildPublishMap(filePaths, settings);\n\n // Process JSON files\n if (filePaths.json) {\n const { library, additionalModules } = determineLibrary();\n\n // Determine dataFormat for JSONs\n let dataFormat: DataFormat;\n if (library === 'next-intl') {\n dataFormat = 'ICU';\n } else if (library === 'i18next') {\n if (additionalModules.includes('i18next-icu')) {\n dataFormat = 'ICU';\n } else {\n dataFormat = 'I18NEXT';\n }\n } else {\n dataFormat = 'STRING';\n }\n\n const jsonFiles = filePaths.json\n .filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.json!))\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate JSON parseability\n if (!skipValidation?.json) {\n try {\n JSON.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: JSON file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'JSON file is not parsable'\n );\n return null;\n }\n }\n\n // Resolve $ref before parsing if configured\n let contentForParsing = content;\n if (shouldResolveRefs(filePath, settings.options)) {\n try {\n const json = JSON.parse(content);\n const { resolved, refMap } = resolveMintlifyRefs(json, filePath);\n storeRefMap(refMap);\n contentForParsing = JSON.stringify(resolved, null, 2);\n } catch {\n // JSON parse errors are handled below by parseJson\n }\n }\n\n const parsedJson = parseJson(\n contentForParsing,\n filePath,\n settings.options || {},\n settings.defaultLocale\n );\n\n // Detect companion metadata file\n let keyedMetadata: KeyedMetadata | undefined;\n let parsedContent: JSONObject | undefined;\n try {\n parsedContent = JSON.parse(content) as JSONObject;\n } catch {\n // Content not parsable — skip metadata detection\n }\n if (parsedContent) {\n const rawMetadata = parseKeyedMetadata(filePath, parsedContent);\n if (rawMetadata) {\n // Run metadata through the same include/composite schema as the source\n // so key paths align at translation time\n const transformed = parseJson(\n JSON.stringify(rawMetadata),\n filePath,\n settings.options || {},\n settings.defaultLocale,\n false\n );\n const transformedMetadata = JSON.parse(transformed);\n\n // Filter metadata to only keep keys that exist in the transformed source\n // This prevents misaligned entries from wide JSONPath patterns\n const sourceKeys = new Set(Object.keys(JSON.parse(parsedJson)));\n const filtered = Object.fromEntries(\n Object.entries(transformedMetadata).filter(([k]) =>\n sourceKeys.has(k)\n )\n ) as KeyedMetadata;\n\n if (Object.keys(filtered).length > 0) {\n keyedMetadata = filtered;\n } else {\n logger.warn(\n `Companion metadata found for ${relativePath} but no keys aligned with the JSON schema — metadata was not attached`\n );\n }\n }\n }\n\n return {\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedJson,\n requiresReviewPaths.has(filePath)\n ),\n content: parsedJson,\n fileName: relativePath,\n fileFormat: 'JSON' as const,\n ...getTransformFormatProperty(settings, 'json'),\n dataFormat,\n locale: settings.defaultLocale,\n ...(keyedMetadata && {\n formatMetadata: { keyedMetadata },\n }),\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: JSON file is empty`);\n recordWarning('skipped_file', file.fileName, 'JSON file is empty');\n return false;\n }\n return true;\n });\n files.push(...jsonFiles.filter((file) => file !== null));\n }\n\n // Process YAML files\n if (filePaths.yaml) {\n const yamlFiles = filePaths.yaml\n .filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.yaml!))\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate YAML parseability\n if (!skipValidation?.yaml) {\n try {\n YAML.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: YAML file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'YAML file is not parsable'\n );\n return null;\n }\n }\n\n const { content: parsedYaml, fileFormat } = parseYaml(\n content,\n filePath,\n settings.options || {}\n );\n\n // Detect companion metadata file\n let keyedMetadata: KeyedMetadata | undefined;\n try {\n const parsedYamlContent = YAML.parse(content);\n const rawMetadata = parseKeyedMetadata(filePath, parsedYamlContent);\n if (rawMetadata) {\n const yamlSchema = validateYamlSchema(\n settings.options || {},\n filePath\n );\n if (yamlSchema?.include) {\n // Flatten metadata through the same include schema as the source\n const flattened = flattenJson(rawMetadata, yamlSchema.include);\n // Filter to only keep keys that exist in the transformed source\n const sourceKeys = new Set(Object.keys(JSON.parse(parsedYaml)));\n const filtered = Object.fromEntries(\n Object.entries(flattened).filter(([k]) => sourceKeys.has(k))\n ) as KeyedMetadata;\n if (Object.keys(filtered).length > 0) {\n keyedMetadata = filtered;\n } else {\n logger.warn(\n `Companion metadata found for ${relativePath} but no keys aligned with the YAML schema — metadata was not attached`\n );\n }\n } else {\n keyedMetadata = rawMetadata;\n }\n }\n } catch {\n // Content not parsable as YAML — skip metadata detection\n }\n\n return {\n content: parsedYaml,\n fileName: relativePath,\n fileFormat,\n ...getTransformFormatProperty(settings, 'yaml'),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedYaml,\n requiresReviewPaths.has(filePath)\n ),\n locale: settings.defaultLocale,\n ...(keyedMetadata && {\n formatMetadata: { keyedMetadata },\n }),\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file || typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(\n `Skipping ${file?.fileName ?? 'unknown'}: YAML file is empty`\n );\n recordWarning(\n 'skipped_file',\n file?.fileName ?? 'unknown',\n 'YAML file is empty'\n );\n return false;\n }\n return true;\n });\n files.push(...yamlFiles.filter((file) => file !== null));\n }\n\n // Process Twilio Content JSON files\n if (filePaths.twilioContentJson) {\n const twilioContentJsonFiles = filePaths.twilioContentJson\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate JSON parseability\n if (!skipValidation?.json) {\n try {\n JSON.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: JSON file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'JSON file is not parsable'\n );\n return null;\n }\n }\n\n const parsedJson = parseJson(\n content,\n filePath,\n settings.options || {},\n settings.defaultLocale\n );\n\n return {\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedJson,\n requiresReviewPaths.has(filePath)\n ),\n content: parsedJson,\n fileName: relativePath,\n fileFormat: 'TWILIO_CONTENT_JSON' as const,\n ...getTransformFormatProperty(settings, 'twilioContentJson'),\n dataFormat: 'STRING' as const,\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: JSON file is empty`);\n recordWarning('skipped_file', file.fileName, 'JSON file is empty');\n return false;\n }\n return true;\n });\n files.push(...twilioContentJsonFiles.filter((file) => file !== null));\n }\n\n // Process Lottie files (binary zip bundles). Content is read as raw bytes and\n // carried base64-encoded end-to-end; the downloaded translation is written\n // back as bytes. No text parsing/merge.\n if (filePaths.lottie) {\n // Lottie files carrying After Effects expressions (executable JS) are\n // rejected outright — collect every offender, then fail once with all of\n // them named rather than aborting on the first.\n const expressionOffenders: string[] = [];\n const lottieFiles = filePaths.lottie\n .map((filePath) => {\n const content = readBinaryFileBase64(filePath);\n const relativePath = getRelative(filePath);\n if (!content) {\n logger.warn(`Skipping ${relativePath}: file is empty or unreadable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'File is empty or unreadable'\n );\n return null;\n }\n if (lottieHasExpressions(content)) {\n expressionOffenders.push(relativePath);\n return null;\n }\n return {\n content,\n fileName: relativePath,\n fileFormat: 'LOTTIE' as const,\n ...getTransformFormatProperty(settings, 'lottie'),\n fileId: hashStringSync(relativePath),\n versionId: hashStringSync(content),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => file !== null);\n if (expressionOffenders.length > 0) {\n logErrorAndExit(lottieExpressionsError(expressionOffenders));\n }\n files.push(...lottieFiles);\n }\n\n // These formats are uploaded verbatim. Their backslash escapes and format\n // specifiers must survive byte-for-byte, so they skip the generic\n // markdown-oriented preprocessing below.\n for (const [fileType, fileFormat] of [\n ['dotStrings', 'DOT_STRINGS'],\n ['dotStringsdict', 'DOT_STRINGSDICT'],\n ['androidStrings', 'ANDROID_STRINGS'],\n ] as const) {\n if (!filePaths[fileType]) continue;\n const verbatimFiles = filePaths[fileType]\n .map((filePath) => {\n const relativePath = getRelative(filePath);\n // Reading is the one place an encoding is resolved; from here on the\n // content is a UTF-8 string like every other format's.\n let content: string;\n try {\n content = readFileContent(filePath, fileFormat);\n } catch (error) {\n // One unreadable file should not abort the whole run: skip it and\n // report it in the summary\n const reason = fileEncodingSkipReason(error);\n logger.warn(`Skipping ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n return null;\n }\n return {\n content,\n fileName: relativePath,\n fileFormat,\n ...getTransformFormatProperty(settings, fileType),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(content, requiresReviewPaths.has(filePath)),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (!file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: File is empty`);\n recordWarning('skipped_file', file.fileName, 'File is empty');\n return false;\n }\n return true;\n });\n files.push(...verbatimFiles.filter((file) => file !== null));\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (\n fileType === 'json' ||\n fileType === 'yaml' ||\n fileType === 'twilioContentJson' ||\n fileType === 'lottie' ||\n fileType === 'dotStrings' ||\n fileType === 'dotStringsdict' ||\n fileType === 'androidStrings'\n )\n continue;\n if (filePaths[fileType]) {\n const parsed = filePaths[fileType]\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n const processed = preprocessContent(\n content,\n relativePath,\n fileType,\n settings\n );\n\n if (typeof processed !== 'string') {\n logger.warn(`Skipping ${relativePath}: ${processed.skip}`);\n recordWarning('skipped_file', relativePath, processed.skip);\n return null;\n }\n\n return {\n content: processed,\n fileName: relativePath,\n fileFormat: fileType.toUpperCase() as FileFormat,\n ...getTransformFormatProperty(settings, fileType),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n processed,\n requiresReviewPaths.has(filePath)\n ),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (\n !file ||\n typeof file.content !== 'string' ||\n !file.content.trim()\n ) {\n logger.warn(\n `Skipping ${file?.fileName ?? 'unknown'}: File is empty after sanitization`\n );\n recordWarning(\n 'skipped_file',\n file?.fileName ?? 'unknown',\n 'File is empty after sanitization'\n );\n return false;\n }\n return true;\n });\n files.push(...parsed.filter((file) => file !== null));\n }\n }\n\n // Remove stale entries for files that were skipped during validation\n const validFileIds = new Set(files.map((f) => f.fileId));\n for (const fileId of publishMap.keys()) {\n if (!validFileIds.has(fileId)) {\n publishMap.delete(fileId);\n }\n }\n\n return { files, publishMap };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"aggregateFiles.js","names":[],"sources":["../../../src/formats/files/aggregateFiles.ts"],"sourcesContent":["import { logger } from '../../console/logger.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport {\n fileEncodingSkipReason,\n lottieExpressionsError,\n xcstringsSourceLanguageMismatchError,\n} from '../../console/index.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\nimport { lottieHasExpressions } from './detectLottieExpressions.js';\nimport {\n getRelative,\n readFile,\n readBinaryFileBase64,\n} from '../../fs/findFilepath.js';\nimport { readFileContent } from '../../fs/fileContent.js';\nimport { Settings } from '../../types/index.js';\nimport type { FileFormat, DataFormat, FileToUpload } from '../../types/data.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';\nimport { parseJson } from '../json/parseJson.js';\nimport {\n parseXcstringsCatalog,\n serializeXcstringsSlice,\n sliceSourceCatalog,\n type XcstringsCatalog,\n} from '../xcstrings/parseXcstrings.js';\nimport {\n resolveMintlifyRefs,\n shouldResolveRefs,\n} from '../../utils/resolveMintlifyRefs.js';\nimport { storeRefMap } from '../../state/mintlifyRefMap.js';\nimport parseYaml from '../yaml/parseYaml.js';\nimport { validateYamlSchema } from '../yaml/utils.js';\nimport { flattenJson } from '../json/flattenJson.js';\nimport type { JSONObject } from '../../types/data/json.js';\nimport YAML from 'yaml';\nimport { determineLibrary } from '../../fs/determineFramework/index.js';\nimport { hashStringSync, hashVersionId } from '../../utils/hash.js';\nimport { gt } from '../../utils/gt.js';\nimport { preprocessContent } from './preprocessContent.js';\nimport {\n parseKeyedMetadata,\n type KeyedMetadata,\n} from '../parseKeyedMetadata.js';\nimport { buildPublishMap } from '../../utils/resolvePublish.js';\nimport { getTransformFormatProperty } from './transformFormat.js';\n\n/**\n * Checks if a file path is a metadata companion file (e.g. foo.metadata.json)\n * AND its corresponding source file (e.g. foo.json) exists in the file list.\n * If both conditions are true, the metadata file should be skipped as a translation source.\n */\nfunction isCompanionMetadataFile(\n filePath: string,\n allFilePaths: string[]\n): boolean {\n const metadataPattern = /\\.metadata\\.(json|yaml|yml)$/;\n if (!metadataPattern.test(filePath)) return false;\n\n // Derive the source file path: foo.metadata.json -> foo.json\n const sourceFilePath = filePath.replace(\n /\\.metadata\\.(json|yaml|yml)$/,\n '.$1'\n );\n return allFilePaths.includes(sourceFilePath);\n}\n\nexport async function aggregateFiles(\n settings: Settings\n): Promise<{ files: FileToUpload[]; publishMap: Map<string, boolean> }> {\n // Aggregate all files to translate\n const files: FileToUpload[] = [];\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return { files, publishMap: new Map<string, boolean>() };\n }\n\n const { resolvedPaths: filePaths } = settings.files;\n // Tolerate partially-constructed settings from programmatic callers\n const requiresReviewPaths =\n settings.files.requiresReviewPaths ?? new Set<string>();\n const skipValidation = settings.options?.skipFileValidation;\n\n // Build publish map upfront from resolved paths.\n const publishMap = buildPublishMap(filePaths, settings);\n\n // Process JSON files\n if (filePaths.json) {\n const { library, additionalModules } = determineLibrary();\n\n // Determine dataFormat for JSONs\n let dataFormat: DataFormat;\n if (library === 'next-intl') {\n dataFormat = 'ICU';\n } else if (library === 'i18next') {\n if (additionalModules.includes('i18next-icu')) {\n dataFormat = 'ICU';\n } else {\n dataFormat = 'I18NEXT';\n }\n } else {\n dataFormat = 'STRING';\n }\n\n const jsonFiles = filePaths.json\n .filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.json!))\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate JSON parseability\n if (!skipValidation?.json) {\n try {\n JSON.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: JSON file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'JSON file is not parsable'\n );\n return null;\n }\n }\n\n // Resolve $ref before parsing if configured\n let contentForParsing = content;\n if (shouldResolveRefs(filePath, settings.options)) {\n try {\n const json = JSON.parse(content);\n const { resolved, refMap } = resolveMintlifyRefs(json, filePath);\n storeRefMap(refMap);\n contentForParsing = JSON.stringify(resolved, null, 2);\n } catch {\n // JSON parse errors are handled below by parseJson\n }\n }\n\n const parsedJson = parseJson(\n contentForParsing,\n filePath,\n settings.options || {},\n settings.defaultLocale\n );\n\n // Detect companion metadata file\n let keyedMetadata: KeyedMetadata | undefined;\n let parsedContent: JSONObject | undefined;\n try {\n parsedContent = JSON.parse(content) as JSONObject;\n } catch {\n // Content not parsable — skip metadata detection\n }\n if (parsedContent) {\n const rawMetadata = parseKeyedMetadata(filePath, parsedContent);\n if (rawMetadata) {\n // Run metadata through the same include/composite schema as the source\n // so key paths align at translation time\n const transformed = parseJson(\n JSON.stringify(rawMetadata),\n filePath,\n settings.options || {},\n settings.defaultLocale,\n false\n );\n const transformedMetadata = JSON.parse(transformed);\n\n // Filter metadata to only keep keys that exist in the transformed source\n // This prevents misaligned entries from wide JSONPath patterns\n const sourceKeys = new Set(Object.keys(JSON.parse(parsedJson)));\n const filtered = Object.fromEntries(\n Object.entries(transformedMetadata).filter(([k]) =>\n sourceKeys.has(k)\n )\n ) as KeyedMetadata;\n\n if (Object.keys(filtered).length > 0) {\n keyedMetadata = filtered;\n } else {\n logger.warn(\n `Companion metadata found for ${relativePath} but no keys aligned with the JSON schema — metadata was not attached`\n );\n }\n }\n }\n\n return {\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedJson,\n requiresReviewPaths.has(filePath)\n ),\n content: parsedJson,\n fileName: relativePath,\n fileFormat: 'JSON' as const,\n ...getTransformFormatProperty(settings, 'json'),\n dataFormat,\n locale: settings.defaultLocale,\n ...(keyedMetadata && {\n formatMetadata: { keyedMetadata },\n }),\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: JSON file is empty`);\n recordWarning('skipped_file', file.fileName, 'JSON file is empty');\n return false;\n }\n return true;\n });\n files.push(...jsonFiles.filter((file) => file !== null));\n }\n\n // Process YAML files\n if (filePaths.yaml) {\n const yamlFiles = filePaths.yaml\n .filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.yaml!))\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate YAML parseability\n if (!skipValidation?.yaml) {\n try {\n YAML.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: YAML file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'YAML file is not parsable'\n );\n return null;\n }\n }\n\n const { content: parsedYaml, fileFormat } = parseYaml(\n content,\n filePath,\n settings.options || {}\n );\n\n // Detect companion metadata file\n let keyedMetadata: KeyedMetadata | undefined;\n try {\n const parsedYamlContent = YAML.parse(content);\n const rawMetadata = parseKeyedMetadata(filePath, parsedYamlContent);\n if (rawMetadata) {\n const yamlSchema = validateYamlSchema(\n settings.options || {},\n filePath\n );\n if (yamlSchema?.include) {\n // Flatten metadata through the same include schema as the source\n const flattened = flattenJson(rawMetadata, yamlSchema.include);\n // Filter to only keep keys that exist in the transformed source\n const sourceKeys = new Set(Object.keys(JSON.parse(parsedYaml)));\n const filtered = Object.fromEntries(\n Object.entries(flattened).filter(([k]) => sourceKeys.has(k))\n ) as KeyedMetadata;\n if (Object.keys(filtered).length > 0) {\n keyedMetadata = filtered;\n } else {\n logger.warn(\n `Companion metadata found for ${relativePath} but no keys aligned with the YAML schema — metadata was not attached`\n );\n }\n } else {\n keyedMetadata = rawMetadata;\n }\n }\n } catch {\n // Content not parsable as YAML — skip metadata detection\n }\n\n return {\n content: parsedYaml,\n fileName: relativePath,\n fileFormat,\n ...getTransformFormatProperty(settings, 'yaml'),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedYaml,\n requiresReviewPaths.has(filePath)\n ),\n locale: settings.defaultLocale,\n ...(keyedMetadata && {\n formatMetadata: { keyedMetadata },\n }),\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file || typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(\n `Skipping ${file?.fileName ?? 'unknown'}: YAML file is empty`\n );\n recordWarning(\n 'skipped_file',\n file?.fileName ?? 'unknown',\n 'YAML file is empty'\n );\n return false;\n }\n return true;\n });\n files.push(...yamlFiles.filter((file) => file !== null));\n }\n\n // Process Twilio Content JSON files\n if (filePaths.twilioContentJson) {\n const twilioContentJsonFiles = filePaths.twilioContentJson\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n // Pre-validate JSON parseability\n if (!skipValidation?.json) {\n try {\n JSON.parse(content);\n } catch {\n logger.warn(`Skipping ${relativePath}: JSON file is not parsable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'JSON file is not parsable'\n );\n return null;\n }\n }\n\n const parsedJson = parseJson(\n content,\n filePath,\n settings.options || {},\n settings.defaultLocale\n );\n\n return {\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n parsedJson,\n requiresReviewPaths.has(filePath)\n ),\n content: parsedJson,\n fileName: relativePath,\n fileFormat: 'TWILIO_CONTENT_JSON' as const,\n ...getTransformFormatProperty(settings, 'twilioContentJson'),\n dataFormat: 'STRING' as const,\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (typeof file.content !== 'string' || !file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: JSON file is empty`);\n recordWarning('skipped_file', file.fileName, 'JSON file is empty');\n return false;\n }\n return true;\n });\n files.push(...twilioContentJsonFiles.filter((file) => file !== null));\n }\n\n // Process Lottie files (binary zip bundles). Content is read as raw bytes and\n // carried base64-encoded end-to-end; the downloaded translation is written\n // back as bytes. No text parsing/merge.\n if (filePaths.lottie) {\n // Lottie files carrying After Effects expressions (executable JS) are\n // rejected outright — collect every offender, then fail once with all of\n // them named rather than aborting on the first.\n const expressionOffenders: string[] = [];\n const lottieFiles = filePaths.lottie\n .map((filePath) => {\n const content = readBinaryFileBase64(filePath);\n const relativePath = getRelative(filePath);\n if (!content) {\n logger.warn(`Skipping ${relativePath}: file is empty or unreadable`);\n recordWarning(\n 'skipped_file',\n relativePath,\n 'File is empty or unreadable'\n );\n return null;\n }\n if (lottieHasExpressions(content)) {\n expressionOffenders.push(relativePath);\n return null;\n }\n return {\n content,\n fileName: relativePath,\n fileFormat: 'LOTTIE' as const,\n ...getTransformFormatProperty(settings, 'lottie'),\n fileId: hashStringSync(relativePath),\n versionId: hashStringSync(content),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => file !== null);\n if (expressionOffenders.length > 0) {\n logErrorAndExit(lottieExpressionsError(expressionOffenders));\n }\n files.push(...lottieFiles);\n }\n\n // These formats are uploaded verbatim. Their backslash escapes and format\n // specifiers must survive byte-for-byte, so they skip the generic\n // markdown-oriented preprocessing below.\n for (const [fileType, fileFormat] of [\n ['dotStrings', 'DOT_STRINGS'],\n ['dotStringsdict', 'DOT_STRINGSDICT'],\n ['androidStrings', 'ANDROID_STRINGS'],\n ] as const) {\n if (!filePaths[fileType]) continue;\n const verbatimFiles = filePaths[fileType]\n .map((filePath) => {\n const relativePath = getRelative(filePath);\n // Reading is the one place an encoding is resolved; from here on the\n // content is a UTF-8 string like every other format's.\n let content: string;\n try {\n content = readFileContent(filePath, fileFormat);\n } catch (error) {\n // One unreadable file should not abort the whole run: skip it and\n // report it in the summary\n const reason = fileEncodingSkipReason(error);\n logger.warn(`Skipping ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n return null;\n }\n return {\n content,\n fileName: relativePath,\n fileFormat,\n ...getTransformFormatProperty(settings, fileType),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(content, requiresReviewPaths.has(filePath)),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (!file) return false;\n if (!file.content.trim()) {\n logger.warn(`Skipping ${file.fileName}: File is empty`);\n recordWarning('skipped_file', file.fileName, 'File is empty');\n return false;\n }\n return true;\n });\n files.push(...verbatimFiles.filter((file) => file !== null));\n }\n\n // Process Apple .xcstrings catalogs. One catalog holds every locale; only the\n // source-language slice is uploaded as the source document. The slice is\n // hashed into versionId, so an unchanged catalog re-slices byte-identically\n // and does not re-upload.\n if (filePaths.xcstrings) {\n const sourceLanguageMismatches: { file: string; sourceLanguage: string }[] =\n [];\n const xcstringsFiles = filePaths.xcstrings\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n let catalog: XcstringsCatalog;\n try {\n catalog = parseXcstringsCatalog(content);\n } catch (error) {\n const reason =\n error instanceof Error\n ? error.message\n : 'xcstrings file is not parsable';\n logger.warn(`Skipping ${relativePath}: ${reason}`);\n recordWarning('skipped_file', relativePath, reason);\n return null;\n }\n // Slicing follows the catalog's own sourceLanguage while the upload is\n // labeled settings.defaultLocale; a mismatch would upload mislabeled\n // source content, so it is a configuration error that stops the run\n // rather than a skipped file. The configured locale may be a custom\n // alias, so canonical forms are compared.\n if (\n gt.resolveCanonicalLocale(catalog.sourceLanguage) !==\n gt.resolveCanonicalLocale(settings.defaultLocale)\n ) {\n sourceLanguageMismatches.push({\n file: relativePath,\n sourceLanguage: catalog.sourceLanguage,\n });\n return null;\n }\n const sourceSlice = serializeXcstringsSlice(\n sliceSourceCatalog(catalog)\n );\n\n return {\n content: sourceSlice,\n fileName: relativePath,\n fileFormat: 'XCSTRINGS' as const,\n ...getTransformFormatProperty(settings, 'xcstrings'),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n sourceSlice,\n requiresReviewPaths.has(filePath)\n ),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => file !== null);\n if (sourceLanguageMismatches.length > 0) {\n logErrorAndExit(\n xcstringsSourceLanguageMismatchError(\n sourceLanguageMismatches,\n settings.defaultLocale\n )\n );\n }\n files.push(...xcstringsFiles);\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (\n fileType === 'json' ||\n fileType === 'yaml' ||\n fileType === 'twilioContentJson' ||\n fileType === 'lottie' ||\n fileType === 'dotStrings' ||\n fileType === 'dotStringsdict' ||\n fileType === 'androidStrings' ||\n fileType === 'xcstrings'\n )\n continue;\n if (filePaths[fileType]) {\n const parsed = filePaths[fileType]\n .map((filePath) => {\n const content = readFile(filePath);\n const relativePath = getRelative(filePath);\n\n const processed = preprocessContent(\n content,\n relativePath,\n fileType,\n settings\n );\n\n if (typeof processed !== 'string') {\n logger.warn(`Skipping ${relativePath}: ${processed.skip}`);\n recordWarning('skipped_file', relativePath, processed.skip);\n return null;\n }\n\n return {\n content: processed,\n fileName: relativePath,\n fileFormat: fileType.toUpperCase() as FileFormat,\n ...getTransformFormatProperty(settings, fileType),\n fileId: hashStringSync(relativePath),\n versionId: hashVersionId(\n processed,\n requiresReviewPaths.has(filePath)\n ),\n locale: settings.defaultLocale,\n } satisfies FileToUpload;\n })\n .filter((file) => {\n if (\n !file ||\n typeof file.content !== 'string' ||\n !file.content.trim()\n ) {\n logger.warn(\n `Skipping ${file?.fileName ?? 'unknown'}: File is empty after sanitization`\n );\n recordWarning(\n 'skipped_file',\n file?.fileName ?? 'unknown',\n 'File is empty after sanitization'\n );\n return false;\n }\n return true;\n });\n files.push(...parsed.filter((file) => file !== null));\n }\n }\n\n // Remove stale entries for files that were skipped during validation\n const validFileIds = new Set(files.map((f) => f.fileId));\n for (const fileId of publishMap.keys()) {\n if (!validFileIds.has(fileId)) {\n publishMap.delete(fileId);\n }\n }\n\n return { files, publishMap };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FileFormat } from 'generaltranslation/types';
|
|
2
|
+
/**
|
|
3
|
+
* The content one locale of a translation file stands for on the platform.
|
|
4
|
+
* A file that holds every locale (an .xcstrings catalog) stands for its
|
|
5
|
+
* single-locale slice — what upload sends and download merges back — so the
|
|
6
|
+
* on-disk file and a download compare slice to slice. Every other format
|
|
7
|
+
* stands for the whole file. The catalog is keyed by canonical tags, so a
|
|
8
|
+
* configured alias is resolved first. Undefined when the content carries
|
|
9
|
+
* nothing for the locale. Throws on invalid content.
|
|
10
|
+
*/
|
|
11
|
+
export declare function localeContent(content: string, fileFormat: FileFormat, locale: string): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The content a file stands for when it carries nothing for a locale: for a
|
|
14
|
+
* file that holds every locale, the catalog with no entries and its other
|
|
15
|
+
* fields intact; for every other format, empty content. Throws on invalid
|
|
16
|
+
* content.
|
|
17
|
+
*/
|
|
18
|
+
export declare function emptyLocaleContent(content: string, fileFormat: FileFormat): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { gt } from "../../utils/gt.js";
|
|
2
|
+
import { parseXcstringsCatalog, serializeXcstringsSlice, sliceTranslationCatalog } from "../xcstrings/parseXcstrings.js";
|
|
3
|
+
//#region src/formats/files/localeContent.ts
|
|
4
|
+
/**
|
|
5
|
+
* The content one locale of a translation file stands for on the platform.
|
|
6
|
+
* A file that holds every locale (an .xcstrings catalog) stands for its
|
|
7
|
+
* single-locale slice — what upload sends and download merges back — so the
|
|
8
|
+
* on-disk file and a download compare slice to slice. Every other format
|
|
9
|
+
* stands for the whole file. The catalog is keyed by canonical tags, so a
|
|
10
|
+
* configured alias is resolved first. Undefined when the content carries
|
|
11
|
+
* nothing for the locale. Throws on invalid content.
|
|
12
|
+
*/
|
|
13
|
+
function localeContent(content, fileFormat, locale) {
|
|
14
|
+
if (fileFormat !== "XCSTRINGS") return content;
|
|
15
|
+
const slice = sliceTranslationCatalog(parseXcstringsCatalog(content), gt.resolveCanonicalLocale(locale));
|
|
16
|
+
return slice === void 0 ? void 0 : serializeXcstringsSlice(slice);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The content a file stands for when it carries nothing for a locale: for a
|
|
20
|
+
* file that holds every locale, the catalog with no entries and its other
|
|
21
|
+
* fields intact; for every other format, empty content. Throws on invalid
|
|
22
|
+
* content.
|
|
23
|
+
*/
|
|
24
|
+
function emptyLocaleContent(content, fileFormat) {
|
|
25
|
+
if (fileFormat !== "XCSTRINGS") return "";
|
|
26
|
+
return serializeXcstringsSlice({
|
|
27
|
+
...parseXcstringsCatalog(content),
|
|
28
|
+
strings: {}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { emptyLocaleContent, localeContent };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=localeContent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localeContent.js","names":[],"sources":["../../../src/formats/files/localeContent.ts"],"sourcesContent":["import type { FileFormat } from 'generaltranslation/types';\nimport { gt } from '../../utils/gt.js';\nimport {\n parseXcstringsCatalog,\n serializeXcstringsSlice,\n sliceTranslationCatalog,\n} from '../xcstrings/parseXcstrings.js';\n\n/**\n * The content one locale of a translation file stands for on the platform.\n * A file that holds every locale (an .xcstrings catalog) stands for its\n * single-locale slice — what upload sends and download merges back — so the\n * on-disk file and a download compare slice to slice. Every other format\n * stands for the whole file. The catalog is keyed by canonical tags, so a\n * configured alias is resolved first. Undefined when the content carries\n * nothing for the locale. Throws on invalid content.\n */\nexport function localeContent(\n content: string,\n fileFormat: FileFormat,\n locale: string\n): string | undefined {\n if (fileFormat !== 'XCSTRINGS') return content;\n const slice = sliceTranslationCatalog(\n parseXcstringsCatalog(content),\n gt.resolveCanonicalLocale(locale)\n );\n return slice === undefined ? undefined : serializeXcstringsSlice(slice);\n}\n\n/**\n * The content a file stands for when it carries nothing for a locale: for a\n * file that holds every locale, the catalog with no entries and its other\n * fields intact; for every other format, empty content. Throws on invalid\n * content.\n */\nexport function emptyLocaleContent(\n content: string,\n fileFormat: FileFormat\n): string {\n if (fileFormat !== 'XCSTRINGS') return '';\n return serializeXcstringsSlice({\n ...parseXcstringsCatalog(content),\n strings: {},\n });\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAgB,cACd,SACA,YACA,QACoB;AACpB,KAAI,eAAe,YAAa,QAAO;CACvC,MAAM,QAAQ,wBACZ,sBAAsB,QAAQ,EAC9B,GAAG,uBAAuB,OAAO,CAClC;AACD,QAAO,UAAU,KAAA,IAAY,KAAA,IAAY,wBAAwB,MAAM;;;;;;;;AASzE,SAAgB,mBACd,SACA,YACQ;AACR,KAAI,eAAe,YAAa,QAAO;AACvC,QAAO,wBAAwB;EAC7B,GAAG,sBAAsB,QAAQ;EACjC,SAAS,EAAE;EACZ,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "pot", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson", "lottie", "dotStrings", "dotStringsdict", "androidStrings"];
|
|
1
|
+
export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "pot", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson", "lottie", "dotStrings", "dotStringsdict", "androidStrings", "xcstrings"];
|
|
2
2
|
export declare const FILE_EXT_TO_EXT_LABEL: {
|
|
3
3
|
json: string;
|
|
4
4
|
pot: string;
|
|
@@ -14,4 +14,5 @@ export declare const FILE_EXT_TO_EXT_LABEL: {
|
|
|
14
14
|
dotStrings: string;
|
|
15
15
|
dotStringsdict: string;
|
|
16
16
|
androidStrings: string;
|
|
17
|
+
xcstrings: string;
|
|
17
18
|
};
|
|
@@ -13,7 +13,8 @@ const SUPPORTED_FILE_EXTENSIONS = [
|
|
|
13
13
|
"lottie",
|
|
14
14
|
"dotStrings",
|
|
15
15
|
"dotStringsdict",
|
|
16
|
-
"androidStrings"
|
|
16
|
+
"androidStrings",
|
|
17
|
+
"xcstrings"
|
|
17
18
|
];
|
|
18
19
|
const FILE_EXT_TO_EXT_LABEL = {
|
|
19
20
|
json: "JSON",
|
|
@@ -29,7 +30,8 @@ const FILE_EXT_TO_EXT_LABEL = {
|
|
|
29
30
|
lottie: "Lottie",
|
|
30
31
|
dotStrings: ".strings",
|
|
31
32
|
dotStringsdict: ".stringsdict",
|
|
32
|
-
androidStrings: "Android strings.xml"
|
|
33
|
+
androidStrings: "Android strings.xml",
|
|
34
|
+
xcstrings: ".xcstrings"
|
|
33
35
|
};
|
|
34
36
|
//#endregion
|
|
35
37
|
export { FILE_EXT_TO_EXT_LABEL, SUPPORTED_FILE_EXTENSIONS };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supportedFiles.js","names":[],"sources":["../../../src/formats/files/supportedFiles.ts"],"sourcesContent":["export const SUPPORTED_FILE_EXTENSIONS = [\n 'json',\n 'pot',\n 'mdx',\n 'md',\n 'ts',\n 'js',\n 'yaml',\n 'html',\n 'txt',\n 'twilioContentJson',\n 'lottie',\n 'dotStrings',\n 'dotStringsdict',\n 'androidStrings',\n] as const;\n\nexport const FILE_EXT_TO_EXT_LABEL = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'Markdown',\n ts: 'TypeScript',\n js: 'JavaScript',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'Text',\n twilioContentJson: 'Twilio Content JSON',\n lottie: 'Lottie',\n dotStrings: '.strings',\n dotStringsdict: '.stringsdict',\n androidStrings: 'Android strings.xml',\n};\n"],"mappings":";AAAA,MAAa,4BAA4B;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAa,wBAAwB;CACnC,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;
|
|
1
|
+
{"version":3,"file":"supportedFiles.js","names":[],"sources":["../../../src/formats/files/supportedFiles.ts"],"sourcesContent":["export const SUPPORTED_FILE_EXTENSIONS = [\n 'json',\n 'pot',\n 'mdx',\n 'md',\n 'ts',\n 'js',\n 'yaml',\n 'html',\n 'txt',\n 'twilioContentJson',\n 'lottie',\n 'dotStrings',\n 'dotStringsdict',\n 'androidStrings',\n 'xcstrings',\n] as const;\n\nexport const FILE_EXT_TO_EXT_LABEL = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'Markdown',\n ts: 'TypeScript',\n js: 'JavaScript',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'Text',\n twilioContentJson: 'Twilio Content JSON',\n lottie: 'Lottie',\n dotStrings: '.strings',\n dotStringsdict: '.stringsdict',\n androidStrings: 'Android strings.xml',\n xcstrings: '.xcstrings',\n};\n"],"mappings":";AAAA,MAAa,4BAA4B;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAa,wBAAwB;CACnC,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CAChB,WAAW;CACZ"}
|
|
@@ -18,6 +18,7 @@ export declare const CONFIG_FILE_TYPE_TO_FILE_FORMAT: {
|
|
|
18
18
|
readonly dotStrings: "DOT_STRINGS";
|
|
19
19
|
readonly dotStringsdict: "DOT_STRINGSDICT";
|
|
20
20
|
readonly androidStrings: "ANDROID_STRINGS";
|
|
21
|
+
readonly xcstrings: "XCSTRINGS";
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* Maps uppercase config aliases to the CLI's canonical lowercase file keys.
|
|
@@ -37,6 +38,7 @@ export declare const FILE_FORMAT_TO_CONFIG_FILE_TYPE: {
|
|
|
37
38
|
readonly DOT_STRINGS: "dotStrings";
|
|
38
39
|
readonly DOT_STRINGSDICT: "dotStringsdict";
|
|
39
40
|
readonly ANDROID_STRINGS: "androidStrings";
|
|
41
|
+
readonly XCSTRINGS: "xcstrings";
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
44
|
* Converts uppercase file format config keys into the CLI's canonical lowercase keys.
|
|
@@ -17,7 +17,8 @@ const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {
|
|
|
17
17
|
lottie: "LOTTIE",
|
|
18
18
|
dotStrings: "DOT_STRINGS",
|
|
19
19
|
dotStringsdict: "DOT_STRINGSDICT",
|
|
20
|
-
androidStrings: "ANDROID_STRINGS"
|
|
20
|
+
androidStrings: "ANDROID_STRINGS",
|
|
21
|
+
xcstrings: "XCSTRINGS"
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* Maps uppercase config aliases to the CLI's canonical lowercase file keys.
|
|
@@ -36,7 +37,8 @@ const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {
|
|
|
36
37
|
LOTTIE: "lottie",
|
|
37
38
|
DOT_STRINGS: "dotStrings",
|
|
38
39
|
DOT_STRINGSDICT: "dotStringsdict",
|
|
39
|
-
ANDROID_STRINGS: "androidStrings"
|
|
40
|
+
ANDROID_STRINGS: "androidStrings",
|
|
41
|
+
XCSTRINGS: "xcstrings"
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
44
|
* Maps API file format enum values to the extension the CLI should write.
|
|
@@ -58,7 +60,8 @@ const FILE_FORMAT_EXTENSIONS = {
|
|
|
58
60
|
SVG: "svg",
|
|
59
61
|
DOT_STRINGS: "strings",
|
|
60
62
|
DOT_STRINGSDICT: "stringsdict",
|
|
61
|
-
ANDROID_STRINGS: "xml"
|
|
63
|
+
ANDROID_STRINGS: "xml",
|
|
64
|
+
XCSTRINGS: "xcstrings"
|
|
62
65
|
};
|
|
63
66
|
/**
|
|
64
67
|
* Converts uppercase file format config keys into the CLI's canonical lowercase keys.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformFormat.js","names":[],"sources":["../../../src/formats/files/transformFormat.ts"],"sourcesContent":["import type { FileFormat } from '../../types/data.js';\nimport type {\n FilesOptions,\n Settings,\n SupportedFileExtension,\n} from '../../types/index.js';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\n/**\n * Maps CLI config file keys to API file format enum values.\n */\nexport const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'MD',\n ts: 'TS',\n js: 'JS',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'TXT',\n twilioContentJson: 'TWILIO_CONTENT_JSON',\n lottie: 'LOTTIE',\n dotStrings: 'DOT_STRINGS',\n dotStringsdict: 'DOT_STRINGSDICT',\n androidStrings: 'ANDROID_STRINGS',\n} as const satisfies Record<SupportedFileExtension, FileFormat>;\n\n/**\n * Maps uppercase config aliases to the CLI's canonical lowercase file keys.\n */\nexport const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {\n JSON: 'json',\n POT: 'pot',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n YAML: 'yaml',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'twilioContentJson',\n LOTTIE: 'lottie',\n DOT_STRINGS: 'dotStrings',\n DOT_STRINGSDICT: 'dotStringsdict',\n ANDROID_STRINGS: 'androidStrings',\n} as const satisfies Partial<Record<FileFormat, SupportedFileExtension>>;\n\n/**\n * Maps API file format enum values to the extension the CLI should write.\n */\nconst FILE_FORMAT_EXTENSIONS = {\n GTJSON: 'json',\n JSON: 'json',\n PO: 'po',\n POT: 'pot',\n YAML: 'yaml',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'json',\n LOTTIE: 'lottie',\n SVG: 'svg',\n DOT_STRINGS: 'strings',\n DOT_STRINGSDICT: 'stringsdict',\n ANDROID_STRINGS: 'xml',\n} as const satisfies Record<FileFormat, string>;\n\n/**\n * Converts uppercase file format config keys into the CLI's canonical lowercase keys.\n *\n * This lets users write either `files.POT` or `files.pot` while keeping the\n * rest of the CLI on its existing lowercase file-type convention.\n */\nexport function normalizeFilesOptions(files: FilesOptions): FilesOptions {\n const normalized = { ...files } as FilesOptions & Record<string, unknown>;\n\n for (const [fileFormat, fileType] of Object.entries(\n FILE_FORMAT_TO_CONFIG_FILE_TYPE\n ) as [string, SupportedFileExtension][]) {\n const uppercaseConfig = normalized[fileFormat] as\n | FilesOptions[SupportedFileExtension]\n | undefined;\n if (!normalized[fileType] && uppercaseConfig) {\n normalized[fileType] = uppercaseConfig;\n }\n delete normalized[fileFormat];\n }\n\n return normalized;\n}\n\n/**\n * Validates and resolves a configured output format for a source file type.\n *\n * Throws when the requested source -> output format is not supported by\n * `generaltranslation/internal`.\n */\nexport function resolveTransformationFormat(\n fileType: SupportedFileExtension,\n transformationFormat: string | undefined\n): FileFormat | undefined {\n if (!transformationFormat) return undefined;\n\n // Normalize to uppercase to match the FileFormat enum (e.g. \"po\" -> \"PO\")\n const normalized = transformationFormat.toUpperCase() as FileFormat;\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (!isSupportedFileFormatTransform(fileFormat, normalized)) {\n throw new Error(\n `Unsupported file format transform: ${fileFormat} -> ${normalized} in files.${fileType}. ` +\n `\"${normalized}\" is not a valid transformationFormat for ${fileFormat} source files.`\n );\n }\n\n return normalized;\n}\n\n/**\n * Returns the API upload/enqueue property for a file type when one is configured.\n */\nexport function getTransformFormatProperty(\n settings: Settings,\n fileType: SupportedFileExtension\n): { transformFormat?: FileFormat } {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return transformFormat ? { transformFormat } : {};\n}\n\n/**\n * Returns the preferred file extension for a translated file format.\n */\nexport function getFileExtensionForFormat(format: FileFormat): string {\n return FILE_FORMAT_EXTENSIONS[format];\n}\n\n/**\n * Rewrites a path's extension to match the translated file format.\n */\nexport function replaceFileExtensionForFormat(\n filePath: string,\n format: FileFormat\n): string {\n const extension = getFileExtensionForFormat(format);\n return /\\.[^/.]+$/.test(filePath)\n ? filePath.replace(/\\.[^/.]+$/, `.${extension}`)\n : `${filePath}.${extension}`;\n}\n\n/**\n * Detects whether any configured format transform changes the output file type.\n */\nexport function hasNonIdentityFileFormatTransform(settings: Settings): boolean {\n return Object.entries(settings.files?.transformFormats || {}).some(\n ([fileType, transformFormat]) =>\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType as SupportedFileExtension] !==\n transformFormat\n );\n}\n\n/**\n * Returns true when the configured transform for a file type changes its format.\n */\nexport function hasNonIdentityFileFormatTransformForType(\n settings: Settings,\n fileType: SupportedFileExtension\n): boolean {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return !!(\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !== transformFormat\n );\n}\n"],"mappings":";;;;;AAWA,MAAa,kCAAkC;CAC7C,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;
|
|
1
|
+
{"version":3,"file":"transformFormat.js","names":[],"sources":["../../../src/formats/files/transformFormat.ts"],"sourcesContent":["import type { FileFormat } from '../../types/data.js';\nimport type {\n FilesOptions,\n Settings,\n SupportedFileExtension,\n} from '../../types/index.js';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\n/**\n * Maps CLI config file keys to API file format enum values.\n */\nexport const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'MD',\n ts: 'TS',\n js: 'JS',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'TXT',\n twilioContentJson: 'TWILIO_CONTENT_JSON',\n lottie: 'LOTTIE',\n dotStrings: 'DOT_STRINGS',\n dotStringsdict: 'DOT_STRINGSDICT',\n androidStrings: 'ANDROID_STRINGS',\n xcstrings: 'XCSTRINGS',\n} as const satisfies Record<SupportedFileExtension, FileFormat>;\n\n/**\n * Maps uppercase config aliases to the CLI's canonical lowercase file keys.\n */\nexport const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {\n JSON: 'json',\n POT: 'pot',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n YAML: 'yaml',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'twilioContentJson',\n LOTTIE: 'lottie',\n DOT_STRINGS: 'dotStrings',\n DOT_STRINGSDICT: 'dotStringsdict',\n ANDROID_STRINGS: 'androidStrings',\n XCSTRINGS: 'xcstrings',\n} as const satisfies Partial<Record<FileFormat, SupportedFileExtension>>;\n\n/**\n * Maps API file format enum values to the extension the CLI should write.\n */\nconst FILE_FORMAT_EXTENSIONS = {\n GTJSON: 'json',\n JSON: 'json',\n PO: 'po',\n POT: 'pot',\n YAML: 'yaml',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'json',\n LOTTIE: 'lottie',\n SVG: 'svg',\n DOT_STRINGS: 'strings',\n DOT_STRINGSDICT: 'stringsdict',\n ANDROID_STRINGS: 'xml',\n XCSTRINGS: 'xcstrings',\n} as const satisfies Record<FileFormat, string>;\n\n/**\n * Converts uppercase file format config keys into the CLI's canonical lowercase keys.\n *\n * This lets users write either `files.POT` or `files.pot` while keeping the\n * rest of the CLI on its existing lowercase file-type convention.\n */\nexport function normalizeFilesOptions(files: FilesOptions): FilesOptions {\n const normalized = { ...files } as FilesOptions & Record<string, unknown>;\n\n for (const [fileFormat, fileType] of Object.entries(\n FILE_FORMAT_TO_CONFIG_FILE_TYPE\n ) as [string, SupportedFileExtension][]) {\n const uppercaseConfig = normalized[fileFormat] as\n | FilesOptions[SupportedFileExtension]\n | undefined;\n if (!normalized[fileType] && uppercaseConfig) {\n normalized[fileType] = uppercaseConfig;\n }\n delete normalized[fileFormat];\n }\n\n return normalized;\n}\n\n/**\n * Validates and resolves a configured output format for a source file type.\n *\n * Throws when the requested source -> output format is not supported by\n * `generaltranslation/internal`.\n */\nexport function resolveTransformationFormat(\n fileType: SupportedFileExtension,\n transformationFormat: string | undefined\n): FileFormat | undefined {\n if (!transformationFormat) return undefined;\n\n // Normalize to uppercase to match the FileFormat enum (e.g. \"po\" -> \"PO\")\n const normalized = transformationFormat.toUpperCase() as FileFormat;\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (!isSupportedFileFormatTransform(fileFormat, normalized)) {\n throw new Error(\n `Unsupported file format transform: ${fileFormat} -> ${normalized} in files.${fileType}. ` +\n `\"${normalized}\" is not a valid transformationFormat for ${fileFormat} source files.`\n );\n }\n\n return normalized;\n}\n\n/**\n * Returns the API upload/enqueue property for a file type when one is configured.\n */\nexport function getTransformFormatProperty(\n settings: Settings,\n fileType: SupportedFileExtension\n): { transformFormat?: FileFormat } {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return transformFormat ? { transformFormat } : {};\n}\n\n/**\n * Returns the preferred file extension for a translated file format.\n */\nexport function getFileExtensionForFormat(format: FileFormat): string {\n return FILE_FORMAT_EXTENSIONS[format];\n}\n\n/**\n * Rewrites a path's extension to match the translated file format.\n */\nexport function replaceFileExtensionForFormat(\n filePath: string,\n format: FileFormat\n): string {\n const extension = getFileExtensionForFormat(format);\n return /\\.[^/.]+$/.test(filePath)\n ? filePath.replace(/\\.[^/.]+$/, `.${extension}`)\n : `${filePath}.${extension}`;\n}\n\n/**\n * Detects whether any configured format transform changes the output file type.\n */\nexport function hasNonIdentityFileFormatTransform(settings: Settings): boolean {\n return Object.entries(settings.files?.transformFormats || {}).some(\n ([fileType, transformFormat]) =>\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType as SupportedFileExtension] !==\n transformFormat\n );\n}\n\n/**\n * Returns true when the configured transform for a file type changes its format.\n */\nexport function hasNonIdentityFileFormatTransformForType(\n settings: Settings,\n fileType: SupportedFileExtension\n): boolean {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return !!(\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !== transformFormat\n );\n}\n"],"mappings":";;;;;AAWA,MAAa,kCAAkC;CAC7C,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CAChB,WAAW;CACZ;;;;AAKD,MAAa,kCAAkC;CAC7C,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,qBAAqB;CACrB,QAAQ;CACR,aAAa;CACb,iBAAiB;CACjB,iBAAiB;CACjB,WAAW;CACZ;;;;AAKD,MAAM,yBAAyB;CAC7B,QAAQ;CACR,MAAM;CACN,IAAI;CACJ,KAAK;CACL,MAAM;CACN,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,KAAK;CACL,qBAAqB;CACrB,QAAQ;CACR,KAAK;CACL,aAAa;CACb,iBAAiB;CACjB,iBAAiB;CACjB,WAAW;CACZ;;;;;;;AAQD,SAAgB,sBAAsB,OAAmC;CACvE,MAAM,aAAa,EAAE,GAAG,OAAO;AAE/B,MAAK,MAAM,CAAC,YAAY,aAAa,OAAO,QAC1C,gCACD,EAAwC;EACvC,MAAM,kBAAkB,WAAW;AAGnC,MAAI,CAAC,WAAW,aAAa,gBAC3B,YAAW,YAAY;AAEzB,SAAO,WAAW;;AAGpB,QAAO;;;;;;;;AAST,SAAgB,4BACd,UACA,sBACwB;AACxB,KAAI,CAAC,qBAAsB,QAAO,KAAA;CAGlC,MAAM,aAAa,qBAAqB,aAAa;CACrD,MAAM,aAAa,gCAAgC;AAEnD,KAAI,CAAC,+BAA+B,YAAY,WAAW,CACzD,OAAM,IAAI,MACR,sCAAsC,WAAW,MAAM,WAAW,YAAY,SAAS,KACjF,WAAW,4CAA4C,WAAW,gBACzE;AAGH,QAAO;;;;;AAMT,SAAgB,2BACd,UACA,UACkC;CAClC,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,kBAAkB,EAAE,iBAAiB,GAAG,EAAE;;;;;AAMnD,SAAgB,0BAA0B,QAA4B;AACpE,QAAO,uBAAuB;;;;;AAMhC,SAAgB,8BACd,UACA,QACQ;CACR,MAAM,YAAY,0BAA0B,OAAO;AACnD,QAAO,YAAY,KAAK,SAAS,GAC7B,SAAS,QAAQ,aAAa,IAAI,YAAY,GAC9C,GAAG,SAAS,GAAG;;;;;AAMrB,SAAgB,kCAAkC,UAA6B;AAC7E,QAAO,OAAO,QAAQ,SAAS,OAAO,oBAAoB,EAAE,CAAC,CAAC,MAC3D,CAAC,UAAU,qBACV,mBACA,gCAAgC,cAC9B,gBACL;;;;;AAMH,SAAgB,yCACd,UACA,UACS;CACT,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,CAAC,EACN,mBACA,gCAAgC,cAAc"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folds one locale's downloaded translations into the catalog at `inputPath`.
|
|
3
|
+
*
|
|
4
|
+
* Catalog keys are canonical tags and the configured `locale` may be a custom
|
|
5
|
+
* alias, so the alias is resolved before it is used as the key on either side.
|
|
6
|
+
* The catalog is read fresh so a batch of locales accumulates. It is truth for
|
|
7
|
+
* everything except that key: for each entry the download carries that
|
|
8
|
+
* localization for, only it is copied into the catalog's entry. Entries,
|
|
9
|
+
* entry-level fields, other locales, and unknown fields at every level are
|
|
10
|
+
* untouched. Keys the download did not translate are left alone; keys the
|
|
11
|
+
* catalog no longer has are skipped with a warning. Throws on invalid content.
|
|
12
|
+
*/
|
|
13
|
+
export declare function mergeXcstringsLocale(downloadedContent: string, locale: string, inputPath: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { logger } from "../../console/logger.js";
|
|
2
|
+
import { gt } from "../../utils/gt.js";
|
|
3
|
+
import { parseXcstringsCatalog, serializeXcstringsSlice } from "./parseXcstrings.js";
|
|
4
|
+
import * as fs$1 from "fs";
|
|
5
|
+
//#region src/formats/xcstrings/mergeXcstrings.ts
|
|
6
|
+
/**
|
|
7
|
+
* Folds one locale's downloaded translations into the catalog at `inputPath`.
|
|
8
|
+
*
|
|
9
|
+
* Catalog keys are canonical tags and the configured `locale` may be a custom
|
|
10
|
+
* alias, so the alias is resolved before it is used as the key on either side.
|
|
11
|
+
* The catalog is read fresh so a batch of locales accumulates. It is truth for
|
|
12
|
+
* everything except that key: for each entry the download carries that
|
|
13
|
+
* localization for, only it is copied into the catalog's entry. Entries,
|
|
14
|
+
* entry-level fields, other locales, and unknown fields at every level are
|
|
15
|
+
* untouched. Keys the download did not translate are left alone; keys the
|
|
16
|
+
* catalog no longer has are skipped with a warning. Throws on invalid content.
|
|
17
|
+
*/
|
|
18
|
+
function mergeXcstringsLocale(downloadedContent, locale, inputPath) {
|
|
19
|
+
const canonicalLocale = gt.resolveCanonicalLocale(locale);
|
|
20
|
+
const catalog = parseXcstringsCatalog(fs$1.readFileSync(inputPath, "utf8"));
|
|
21
|
+
const downloaded = parseXcstringsCatalog(downloadedContent);
|
|
22
|
+
const strings = Object.create(null);
|
|
23
|
+
for (const [key, entry] of Object.entries(catalog.strings)) strings[key] = entry;
|
|
24
|
+
const unmatchedKeys = [];
|
|
25
|
+
for (const [key, downloadedEntry] of Object.entries(downloaded.strings)) {
|
|
26
|
+
const localization = downloadedEntry.localizations?.[canonicalLocale];
|
|
27
|
+
if (localization === void 0) continue;
|
|
28
|
+
const entry = strings[key];
|
|
29
|
+
if (entry === void 0) {
|
|
30
|
+
unmatchedKeys.push(key);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
strings[key] = {
|
|
34
|
+
...entry,
|
|
35
|
+
localizations: withLocalization(entry.localizations, canonicalLocale, localization)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (unmatchedKeys.length > 0) logger.warn(`Skipped ${unmatchedKeys.length} downloaded ${locale} translation(s) with no matching entry in the local catalog: ${unmatchedKeys.map((key) => JSON.stringify(key)).join(", ")}`);
|
|
39
|
+
return serializeXcstringsSlice({
|
|
40
|
+
...catalog,
|
|
41
|
+
strings
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clones `localizations` with `locale` set. An existing locale keeps its
|
|
46
|
+
* position; a new one is inserted before the first existing key that sorts
|
|
47
|
+
* after it, so the bytes do not depend on the order locales were downloaded
|
|
48
|
+
* and keys already present never move.
|
|
49
|
+
*/
|
|
50
|
+
function withLocalization(localizations, locale, localization) {
|
|
51
|
+
const merged = Object.create(null);
|
|
52
|
+
let inserted = false;
|
|
53
|
+
for (const [existing, value] of Object.entries(localizations ?? {})) {
|
|
54
|
+
if (existing === locale) {
|
|
55
|
+
merged[locale] = localization;
|
|
56
|
+
inserted = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!inserted && locale < existing) {
|
|
60
|
+
merged[locale] = localization;
|
|
61
|
+
inserted = true;
|
|
62
|
+
}
|
|
63
|
+
merged[existing] = value;
|
|
64
|
+
}
|
|
65
|
+
if (!inserted) merged[locale] = localization;
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { mergeXcstringsLocale };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=mergeXcstrings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeXcstrings.js","names":["fs"],"sources":["../../../src/formats/xcstrings/mergeXcstrings.ts"],"sourcesContent":["import * as fs from 'fs';\nimport { logger } from '../../console/logger.js';\nimport { gt } from '../../utils/gt.js';\nimport {\n parseXcstringsCatalog,\n serializeXcstringsSlice,\n type XcstringsEntry,\n} from './parseXcstrings.js';\n\n/**\n * Folds one locale's downloaded translations into the catalog at `inputPath`.\n *\n * Catalog keys are canonical tags and the configured `locale` may be a custom\n * alias, so the alias is resolved before it is used as the key on either side.\n * The catalog is read fresh so a batch of locales accumulates. It is truth for\n * everything except that key: for each entry the download carries that\n * localization for, only it is copied into the catalog's entry. Entries,\n * entry-level fields, other locales, and unknown fields at every level are\n * untouched. Keys the download did not translate are left alone; keys the\n * catalog no longer has are skipped with a warning. Throws on invalid content.\n */\nexport function mergeXcstringsLocale(\n downloadedContent: string,\n locale: string,\n inputPath: string\n): string {\n const canonicalLocale = gt.resolveCanonicalLocale(locale);\n const catalog = parseXcstringsCatalog(fs.readFileSync(inputPath, 'utf8'));\n const downloaded = parseXcstringsCatalog(downloadedContent);\n\n const strings: Record<string, XcstringsEntry> = Object.create(null);\n for (const [key, entry] of Object.entries(catalog.strings)) {\n strings[key] = entry;\n }\n\n const unmatchedKeys: string[] = [];\n for (const [key, downloadedEntry] of Object.entries(downloaded.strings)) {\n const localization = downloadedEntry.localizations?.[canonicalLocale];\n if (localization === undefined) continue;\n const entry = strings[key];\n if (entry === undefined) {\n unmatchedKeys.push(key);\n continue;\n }\n strings[key] = {\n ...entry,\n localizations: withLocalization(\n entry.localizations,\n canonicalLocale,\n localization\n ),\n };\n }\n\n if (unmatchedKeys.length > 0) {\n logger.warn(\n `Skipped ${unmatchedKeys.length} downloaded ${locale} translation(s) with no matching entry in the local catalog: ${unmatchedKeys\n .map((key) => JSON.stringify(key))\n .join(', ')}`\n );\n }\n\n return serializeXcstringsSlice({ ...catalog, strings });\n}\n\n/**\n * Clones `localizations` with `locale` set. An existing locale keeps its\n * position; a new one is inserted before the first existing key that sorts\n * after it, so the bytes do not depend on the order locales were downloaded\n * and keys already present never move.\n */\nfunction withLocalization(\n localizations: Record<string, unknown> | undefined,\n locale: string,\n localization: unknown\n): Record<string, unknown> {\n const merged: Record<string, unknown> = Object.create(null);\n let inserted = false;\n for (const [existing, value] of Object.entries(localizations ?? {})) {\n if (existing === locale) {\n merged[locale] = localization;\n inserted = true;\n continue;\n }\n if (!inserted && locale < existing) {\n merged[locale] = localization;\n inserted = true;\n }\n merged[existing] = value;\n }\n if (!inserted) merged[locale] = localization;\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqBA,SAAgB,qBACd,mBACA,QACA,WACQ;CACR,MAAM,kBAAkB,GAAG,uBAAuB,OAAO;CACzD,MAAM,UAAU,sBAAsBA,KAAG,aAAa,WAAW,OAAO,CAAC;CACzE,MAAM,aAAa,sBAAsB,kBAAkB;CAE3D,MAAM,UAA0C,OAAO,OAAO,KAAK;AACnE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,QAAQ,CACxD,SAAQ,OAAO;CAGjB,MAAM,gBAA0B,EAAE;AAClC,MAAK,MAAM,CAAC,KAAK,oBAAoB,OAAO,QAAQ,WAAW,QAAQ,EAAE;EACvE,MAAM,eAAe,gBAAgB,gBAAgB;AACrD,MAAI,iBAAiB,KAAA,EAAW;EAChC,MAAM,QAAQ,QAAQ;AACtB,MAAI,UAAU,KAAA,GAAW;AACvB,iBAAc,KAAK,IAAI;AACvB;;AAEF,UAAQ,OAAO;GACb,GAAG;GACH,eAAe,iBACb,MAAM,eACN,iBACA,aACD;GACF;;AAGH,KAAI,cAAc,SAAS,EACzB,QAAO,KACL,WAAW,cAAc,OAAO,cAAc,OAAO,+DAA+D,cACjH,KAAK,QAAQ,KAAK,UAAU,IAAI,CAAC,CACjC,KAAK,KAAK,GACd;AAGH,QAAO,wBAAwB;EAAE,GAAG;EAAS;EAAS,CAAC;;;;;;;;AASzD,SAAS,iBACP,eACA,QACA,cACyB;CACzB,MAAM,SAAkC,OAAO,OAAO,KAAK;CAC3D,IAAI,WAAW;AACf,MAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,iBAAiB,EAAE,CAAC,EAAE;AACnE,MAAI,aAAa,QAAQ;AACvB,UAAO,UAAU;AACjB,cAAW;AACX;;AAEF,MAAI,CAAC,YAAY,SAAS,UAAU;AAClC,UAAO,UAAU;AACjB,cAAW;;AAEb,SAAO,YAAY;;AAErB,KAAI,CAAC,SAAU,QAAO,UAAU;AAChC,QAAO"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type XcstringsEntry = {
|
|
2
|
+
localizations?: Record<string, unknown>;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
};
|
|
5
|
+
export type XcstringsCatalog = {
|
|
6
|
+
sourceLanguage: string;
|
|
7
|
+
strings: Record<string, XcstringsEntry>;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Parses a raw .xcstrings document and validates the structure the slicer
|
|
12
|
+
* traverses. The `strings` and `localizations` containers are re-keyed onto
|
|
13
|
+
* prototype-less records so lookups by catalog-chosen names cannot resolve to
|
|
14
|
+
* inherited properties. Throws on invalid content.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseXcstringsCatalog(content: string): XcstringsCatalog;
|
|
17
|
+
/**
|
|
18
|
+
* PINNED SERIALIZATION — DO NOT CHANGE.
|
|
19
|
+
*
|
|
20
|
+
* The source slice is hashed into versionId (see aggregateFiles), so any
|
|
21
|
+
* change to these bytes re-versions every customer .xcstrings file and
|
|
22
|
+
* re-triggers translation fleet-wide. The byte-exact tests on this format are
|
|
23
|
+
* the contract.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serializeXcstringsSlice(catalog: XcstringsCatalog): string;
|
|
26
|
+
/**
|
|
27
|
+
* Produces the source-language slice of a validated catalog: a single-locale
|
|
28
|
+
* catalog holding, per entry, only the source-language localization. Entries
|
|
29
|
+
* without a source localization (the key itself is the source) keep their
|
|
30
|
+
* other fields and carry no `localizations`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sliceSourceCatalog(catalog: XcstringsCatalog): XcstringsCatalog;
|
|
33
|
+
/**
|
|
34
|
+
* Produces one locale's translation slice: the entries that carry
|
|
35
|
+
* `localizations[locale]`, each holding only that localization. Returns
|
|
36
|
+
* undefined when the catalog carries no translation for the locale.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sliceTranslationCatalog(catalog: XcstringsCatalog, locale: string): XcstringsCatalog | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Parses, slices to the catalog's source language, and serializes with the
|
|
41
|
+
* pinned byte layout. Throws on invalid content.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseXcstrings(content: string): string;
|