gt 2.22.1 → 2.22.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.22.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2340](https://github.com/generaltranslation/gt/pull/2340) [`5d75eb4`](https://github.com/generaltranslation/gt/commit/5d75eb475a9e36a61845f0368566a9d40466251c) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Add SubRip (`.srt`) subtitle files as a supported file type, uploaded verbatim under the `SRT` format
8
+
9
+ - Updated dependencies [[`5d75eb4`](https://github.com/generaltranslation/gt/commit/5d75eb475a9e36a61845f0368566a9d40466251c)]:
10
+ - generaltranslation@9.5.1
11
+ - @generaltranslation/python-extractor@0.2.55
12
+ - @generaltranslation/supported-locales@2.1.35
13
+ - @generaltranslation/vue-extractor@0.1.15
14
+
3
15
  ## 2.22.1
4
16
 
5
17
  ### Patch Changes
@@ -213,7 +213,8 @@ async function aggregateFiles(settings) {
213
213
  for (const [fileType, fileFormat] of [
214
214
  ["dotStrings", "DOT_STRINGS"],
215
215
  ["dotStringsdict", "DOT_STRINGSDICT"],
216
- ["androidStrings", "ANDROID_STRINGS"]
216
+ ["androidStrings", "ANDROID_STRINGS"],
217
+ ["srt", "SRT"]
217
218
  ]) {
218
219
  if (!filePaths[fileType]) continue;
219
220
  const verbatimFiles = filePaths[fileType].map((filePath) => {
@@ -283,7 +284,7 @@ async function aggregateFiles(settings) {
283
284
  files.push(...xcstringsFiles);
284
285
  }
285
286
  for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
286
- if (fileType === "json" || fileType === "yaml" || fileType === "twilioContentJson" || fileType === "lottie" || fileType === "dotStrings" || fileType === "dotStringsdict" || fileType === "androidStrings" || fileType === "xcstrings") continue;
287
+ if (fileType === "json" || fileType === "yaml" || fileType === "twilioContentJson" || fileType === "lottie" || fileType === "dotStrings" || fileType === "dotStringsdict" || fileType === "androidStrings" || fileType === "xcstrings" || fileType === "srt") continue;
287
288
  if (filePaths[fileType]) {
288
289
  const parsed = filePaths[fileType].map((filePath) => {
289
290
  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 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 { 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 { api } from '../../utils/api.js';\nimport { preprocessContent } from './preprocessContent.js';\nimport {\n parseKeyedMetadata,\n type KeyedMetadata,\n} from '../parseKeyedMetadata.js';\nimport { buildPublishMap } from '../../utils/resolvePublish.js';\nimport {\n CONFIG_FILE_TYPE_TO_FILE_FORMAT,\n getTransformFormatProperty,\n} 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 api.resolveCanonicalLocale(catalog.sourceLanguage) !==\n api.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: CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType],\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAS,wBACP,UACA,cACS;AAET,KAAI,CAAC,+BAAgB,KAAK,SAAS,CAAE,QAAO;CAG5C,MAAM,iBAAiB,SAAS,QAC9B,gCACA,MACD;AACD,QAAO,aAAa,SAAS,eAAe;;AAG9C,eAAsB,eACpB,UACsE;CAEtE,MAAM,QAAwB,EAAE;AAChC,KACE,CAAC,SAAS,SACT,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC,WAAW,KACvD,SAAS,MAAM,iBAAiB,GAElC,QAAO;EAAE;EAAO,4BAAY,IAAI,KAAsB;EAAE;CAG1D,MAAM,EAAE,eAAe,cAAc,SAAS;CAE9C,MAAM,sBACJ,SAAS,MAAM,uCAAuB,IAAI,KAAa;CACzD,MAAM,iBAAiB,SAAS,SAAS;CAGzC,MAAM,aAAa,gBAAgB,WAAW,SAAS;AAGvD,KAAI,UAAU,MAAM;EAClB,MAAM,EAAE,SAAS,sBAAsB,kBAAkB;EAGzD,IAAI;AACJ,MAAI,YAAY,YACd,cAAa;WACJ,YAAY,UACrB,KAAI,kBAAkB,SAAS,cAAc,CAC3C,cAAa;MAEb,cAAa;MAGf,cAAa;EAGf,MAAM,YAAY,UAAU,KACzB,QAAQ,aAAa,CAAC,wBAAwB,UAAU,UAAU,KAAM,CAAC,CACzE,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAKX,IAAI,oBAAoB;AACxB,OAAI,kBAAkB,UAAU,SAAS,QAAQ,CAC/C,KAAI;IAEF,MAAM,EAAE,UAAU,WAAW,oBADhB,KAAK,MAAM,QAC6B,EAAE,SAAS;AAChE,gBAAY,OAAO;AACnB,wBAAoB,KAAK,UAAU,UAAU,MAAM,EAAE;WAC/C;GAKV,MAAM,aAAa,UACjB,mBACA,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,cACV;GAGD,IAAI;GACJ,IAAI;AACJ,OAAI;AACF,oBAAgB,KAAK,MAAM,QAAQ;WAC7B;AAGR,OAAI,eAAe;IACjB,MAAM,cAAc,mBAAmB,UAAU,cAAc;AAC/D,QAAI,aAAa;KAGf,MAAM,cAAc,UAClB,KAAK,UAAU,YAAY,EAC3B,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,eACT,MACD;KACD,MAAM,sBAAsB,KAAK,MAAM,YAAY;KAInD,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,WAAW,CAAC,CAAC;KAC/D,MAAM,WAAW,OAAO,YACtB,OAAO,QAAQ,oBAAoB,CAAC,QAAQ,CAAC,OAC3C,WAAW,IAAI,EAAE,CAClB,CACF;AAED,SAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,iBAAgB;SAEhB,QAAO,KACL,gCAAgC,aAAa,uEAC9C;;;AAKP,UAAO;IACL,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,OAAO;IAC/C;IACA,QAAQ,SAAS;IACjB,GAAI,iBAAiB,EACnB,gBAAgB,EAAE,eAAe,EAClC;IACF;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AAC5D,WAAO,KAAK,YAAY,KAAK,SAAS,sBAAsB;AAC5D,kBAAc,gBAAgB,KAAK,UAAU,qBAAqB;AAClE,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,UAAU,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAI1D,KAAI,UAAU,MAAM;EAClB,MAAM,YAAY,UAAU,KACzB,QAAQ,aAAa,CAAC,wBAAwB,UAAU,UAAU,KAAM,CAAC,CACzE,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAIX,MAAM,EAAE,SAAS,YAAY,eAAe,UAC1C,SACA,UACA,SAAS,WAAW,EAAE,CACvB;GAGD,IAAI;AACJ,OAAI;IAEF,MAAM,cAAc,mBAAmB,UADb,KAAK,MAAM,QAC6B,CAAC;AACnE,QAAI,aAAa;KACf,MAAM,aAAa,mBACjB,SAAS,WAAW,EAAE,EACtB,SACD;AACD,SAAI,YAAY,SAAS;MAEvB,MAAM,YAAY,YAAY,aAAa,WAAW,QAAQ;MAE9D,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,WAAW,CAAC,CAAC;MAC/D,MAAM,WAAW,OAAO,YACtB,OAAO,QAAQ,UAAU,CAAC,QAAQ,CAAC,OAAO,WAAW,IAAI,EAAE,CAAC,CAC7D;AACD,UAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,iBAAgB;UAEhB,QAAO,KACL,gCAAgC,aAAa,uEAC9C;WAGH,iBAAgB;;WAGd;AAIR,UAAO;IACL,SAAS;IACT,UAAU;IACV;IACA,GAAG,2BAA2B,UAAU,OAAO;IAC/C,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,QAAQ,SAAS;IACjB,GAAI,iBAAiB,EACnB,gBAAgB,EAAE,eAAe,EAClC;IACF;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,QAAQ,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AACrE,WAAO,KACL,YAAY,MAAM,YAAY,UAAU,sBACzC;AACD,kBACE,gBACA,MAAM,YAAY,WAClB,qBACD;AACD,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,UAAU,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAI1D,KAAI,UAAU,mBAAmB;EAC/B,MAAM,yBAAyB,UAAU,kBACtC,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAIX,MAAM,aAAa,UACjB,SACA,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,cACV;AAED,UAAO;IACL,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,oBAAoB;IAC5D,YAAY;IACZ,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AAC5D,WAAO,KAAK,YAAY,KAAK,SAAS,sBAAsB;AAC5D,kBAAc,gBAAgB,KAAK,UAAU,qBAAqB;AAClE,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,uBAAuB,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAMvE,KAAI,UAAU,QAAQ;EAIpB,MAAM,sBAAgC,EAAE;EACxC,MAAM,cAAc,UAAU,OAC3B,KAAK,aAAa;GACjB,MAAM,UAAU,qBAAqB,SAAS;GAC9C,MAAM,eAAe,YAAY,SAAS;AAC1C,OAAI,CAAC,SAAS;AACZ,WAAO,KAAK,YAAY,aAAa,+BAA+B;AACpE,kBACE,gBACA,cACA,8BACD;AACD,WAAO;;AAET,OAAI,qBAAqB,QAAQ,EAAE;AACjC,wBAAoB,KAAK,aAAa;AACtC,WAAO;;AAET,UAAO;IACL;IACA,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,SAAS;IACjD,QAAQ,eAAe,aAAa;IACpC,WAAW,eAAe,QAAQ;IAClC,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS,SAAS,KAAK;AAClC,MAAI,oBAAoB,SAAS,EAC/B,iBAAgB,uBAAuB,oBAAoB,CAAC;AAE9D,QAAM,KAAK,GAAG,YAAY;;AAM5B,MAAK,MAAM,CAAC,UAAU,eAAe;EACnC,CAAC,cAAc,cAAc;EAC7B,CAAC,kBAAkB,kBAAkB;EACrC,CAAC,kBAAkB,kBAAkB;EACtC,EAAW;AACV,MAAI,CAAC,UAAU,UAAW;EAC1B,MAAM,gBAAgB,UAAU,UAC7B,KAAK,aAAa;GACjB,MAAM,eAAe,YAAY,SAAS;GAG1C,IAAI;AACJ,OAAI;AACF,cAAU,gBAAgB,UAAU,WAAW;YACxC,OAAO;IAGd,MAAM,SAAS,uBAAuB,MAAM;AAC5C,WAAO,KAAK,YAAY,aAAa,IAAI,SAAS;AAClD,kBAAc,gBAAgB,cAAc,OAAO;AACnD,WAAO;;AAET,UAAO;IACL;IACA,UAAU;IACV;IACA,GAAG,2BAA2B,UAAU,SAAS;IACjD,QAAQ,eAAe,aAAa;IACpC,WAAW,cAAc,SAAS,oBAAoB,IAAI,SAAS,CAAC;IACpE,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,CAAC,KAAK,QAAQ,MAAM,EAAE;AACxB,WAAO,KAAK,YAAY,KAAK,SAAS,iBAAiB;AACvD,kBAAc,gBAAgB,KAAK,UAAU,gBAAgB;AAC7D,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,cAAc,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAO9D,KAAI,UAAU,WAAW;EACvB,MAAM,2BACJ,EAAE;EACJ,MAAM,iBAAiB,UAAU,UAC9B,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;GAE1C,IAAI;AACJ,OAAI;AACF,cAAU,sBAAsB,QAAQ;YACjC,OAAO;IACd,MAAM,SACJ,iBAAiB,QACb,MAAM,UACN;AACN,WAAO,KAAK,YAAY,aAAa,IAAI,SAAS;AAClD,kBAAc,gBAAgB,cAAc,OAAO;AACnD,WAAO;;AAOT,OACE,IAAI,uBAAuB,QAAQ,eAAe,KAClD,IAAI,uBAAuB,SAAS,cAAc,EAClD;AACA,6BAAyB,KAAK;KAC5B,MAAM;KACN,gBAAgB,QAAQ;KACzB,CAAC;AACF,WAAO;;GAET,MAAM,cAAc,wBAClB,mBAAmB,QAAQ,CAC5B;AAED,UAAO;IACL,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,YAAY;IACpD,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,aACA,oBAAoB,IAAI,SAAS,CAClC;IACD,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS,SAAS,KAAK;AAClC,MAAI,yBAAyB,SAAS,EACpC,iBACE,qCACE,0BACA,SAAS,cACV,CACF;AAEH,QAAM,KAAK,GAAG,eAAe;;AAG/B,MAAK,MAAM,YAAY,2BAA2B;AAChD,MACE,aAAa,UACb,aAAa,UACb,aAAa,uBACb,aAAa,YACb,aAAa,gBACb,aAAa,oBACb,aAAa,oBACb,aAAa,YAEb;AACF,MAAI,UAAU,WAAW;GACvB,MAAM,SAAS,UAAU,UACtB,KAAK,aAAa;IACjB,MAAM,UAAU,SAAS,SAAS;IAClC,MAAM,eAAe,YAAY,SAAS;IAE1C,MAAM,YAAY,kBAChB,SACA,cACA,UACA,SACD;AAED,QAAI,OAAO,cAAc,UAAU;AACjC,YAAO,KAAK,YAAY,aAAa,IAAI,UAAU,OAAO;AAC1D,mBAAc,gBAAgB,cAAc,UAAU,KAAK;AAC3D,YAAO;;AAGT,WAAO;KACL,SAAS;KACT,UAAU;KACV,YAAY,gCAAgC;KAC5C,GAAG,2BAA2B,UAAU,SAAS;KACjD,QAAQ,eAAe,aAAa;KACpC,WAAW,cACT,WACA,oBAAoB,IAAI,SAAS,CAClC;KACD,QAAQ,SAAS;KAClB;KACD,CACD,QAAQ,SAAS;AAChB,QACE,CAAC,QACD,OAAO,KAAK,YAAY,YACxB,CAAC,KAAK,QAAQ,MAAM,EACpB;AACA,YAAO,KACL,YAAY,MAAM,YAAY,UAAU,oCACzC;AACD,mBACE,gBACA,MAAM,YAAY,WAClB,mCACD;AACD,YAAO;;AAET,WAAO;KACP;AACJ,SAAM,KAAK,GAAG,OAAO,QAAQ,SAAS,SAAS,KAAK,CAAC;;;CAKzD,MAAM,eAAe,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,OAAO,CAAC;AACxD,MAAK,MAAM,UAAU,WAAW,MAAM,CACpC,KAAI,CAAC,aAAa,IAAI,OAAO,CAC3B,YAAW,OAAO,OAAO;AAI7B,QAAO;EAAE;EAAO;EAAY"}
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 { 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 { api } from '../../utils/api.js';\nimport { preprocessContent } from './preprocessContent.js';\nimport {\n parseKeyedMetadata,\n type KeyedMetadata,\n} from '../parseKeyedMetadata.js';\nimport { buildPublishMap } from '../../utils/resolvePublish.js';\nimport {\n CONFIG_FILE_TYPE_TO_FILE_FORMAT,\n getTransformFormatProperty,\n} 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, format\n // specifiers, timing lines and line breaks must survive byte-for-byte, so\n // they skip the generic markdown-oriented preprocessing below.\n for (const [fileType, fileFormat] of [\n ['dotStrings', 'DOT_STRINGS'],\n ['dotStringsdict', 'DOT_STRINGSDICT'],\n ['androidStrings', 'ANDROID_STRINGS'],\n ['srt', 'SRT'],\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 api.resolveCanonicalLocale(catalog.sourceLanguage) !==\n api.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 fileType === 'srt'\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: CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType],\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAS,wBACP,UACA,cACS;AAET,KAAI,CAAC,+BAAgB,KAAK,SAAS,CAAE,QAAO;CAG5C,MAAM,iBAAiB,SAAS,QAC9B,gCACA,MACD;AACD,QAAO,aAAa,SAAS,eAAe;;AAG9C,eAAsB,eACpB,UACsE;CAEtE,MAAM,QAAwB,EAAE;AAChC,KACE,CAAC,SAAS,SACT,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC,WAAW,KACvD,SAAS,MAAM,iBAAiB,GAElC,QAAO;EAAE;EAAO,4BAAY,IAAI,KAAsB;EAAE;CAG1D,MAAM,EAAE,eAAe,cAAc,SAAS;CAE9C,MAAM,sBACJ,SAAS,MAAM,uCAAuB,IAAI,KAAa;CACzD,MAAM,iBAAiB,SAAS,SAAS;CAGzC,MAAM,aAAa,gBAAgB,WAAW,SAAS;AAGvD,KAAI,UAAU,MAAM;EAClB,MAAM,EAAE,SAAS,sBAAsB,kBAAkB;EAGzD,IAAI;AACJ,MAAI,YAAY,YACd,cAAa;WACJ,YAAY,UACrB,KAAI,kBAAkB,SAAS,cAAc,CAC3C,cAAa;MAEb,cAAa;MAGf,cAAa;EAGf,MAAM,YAAY,UAAU,KACzB,QAAQ,aAAa,CAAC,wBAAwB,UAAU,UAAU,KAAM,CAAC,CACzE,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAKX,IAAI,oBAAoB;AACxB,OAAI,kBAAkB,UAAU,SAAS,QAAQ,CAC/C,KAAI;IAEF,MAAM,EAAE,UAAU,WAAW,oBADhB,KAAK,MAAM,QAC6B,EAAE,SAAS;AAChE,gBAAY,OAAO;AACnB,wBAAoB,KAAK,UAAU,UAAU,MAAM,EAAE;WAC/C;GAKV,MAAM,aAAa,UACjB,mBACA,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,cACV;GAGD,IAAI;GACJ,IAAI;AACJ,OAAI;AACF,oBAAgB,KAAK,MAAM,QAAQ;WAC7B;AAGR,OAAI,eAAe;IACjB,MAAM,cAAc,mBAAmB,UAAU,cAAc;AAC/D,QAAI,aAAa;KAGf,MAAM,cAAc,UAClB,KAAK,UAAU,YAAY,EAC3B,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,eACT,MACD;KACD,MAAM,sBAAsB,KAAK,MAAM,YAAY;KAInD,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,WAAW,CAAC,CAAC;KAC/D,MAAM,WAAW,OAAO,YACtB,OAAO,QAAQ,oBAAoB,CAAC,QAAQ,CAAC,OAC3C,WAAW,IAAI,EAAE,CAClB,CACF;AAED,SAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,iBAAgB;SAEhB,QAAO,KACL,gCAAgC,aAAa,uEAC9C;;;AAKP,UAAO;IACL,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,OAAO;IAC/C;IACA,QAAQ,SAAS;IACjB,GAAI,iBAAiB,EACnB,gBAAgB,EAAE,eAAe,EAClC;IACF;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AAC5D,WAAO,KAAK,YAAY,KAAK,SAAS,sBAAsB;AAC5D,kBAAc,gBAAgB,KAAK,UAAU,qBAAqB;AAClE,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,UAAU,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAI1D,KAAI,UAAU,MAAM;EAClB,MAAM,YAAY,UAAU,KACzB,QAAQ,aAAa,CAAC,wBAAwB,UAAU,UAAU,KAAM,CAAC,CACzE,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAIX,MAAM,EAAE,SAAS,YAAY,eAAe,UAC1C,SACA,UACA,SAAS,WAAW,EAAE,CACvB;GAGD,IAAI;AACJ,OAAI;IAEF,MAAM,cAAc,mBAAmB,UADb,KAAK,MAAM,QAC6B,CAAC;AACnE,QAAI,aAAa;KACf,MAAM,aAAa,mBACjB,SAAS,WAAW,EAAE,EACtB,SACD;AACD,SAAI,YAAY,SAAS;MAEvB,MAAM,YAAY,YAAY,aAAa,WAAW,QAAQ;MAE9D,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,WAAW,CAAC,CAAC;MAC/D,MAAM,WAAW,OAAO,YACtB,OAAO,QAAQ,UAAU,CAAC,QAAQ,CAAC,OAAO,WAAW,IAAI,EAAE,CAAC,CAC7D;AACD,UAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,iBAAgB;UAEhB,QAAO,KACL,gCAAgC,aAAa,uEAC9C;WAGH,iBAAgB;;WAGd;AAIR,UAAO;IACL,SAAS;IACT,UAAU;IACV;IACA,GAAG,2BAA2B,UAAU,OAAO;IAC/C,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,QAAQ,SAAS;IACjB,GAAI,iBAAiB,EACnB,gBAAgB,EAAE,eAAe,EAClC;IACF;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,QAAQ,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AACrE,WAAO,KACL,YAAY,MAAM,YAAY,UAAU,sBACzC;AACD,kBACE,gBACA,MAAM,YAAY,WAClB,qBACD;AACD,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,UAAU,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAI1D,KAAI,UAAU,mBAAmB;EAC/B,MAAM,yBAAyB,UAAU,kBACtC,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;AAG1C,OAAI,CAAC,gBAAgB,KACnB,KAAI;AACF,SAAK,MAAM,QAAQ;WACb;AACN,WAAO,KAAK,YAAY,aAAa,6BAA6B;AAClE,kBACE,gBACA,cACA,4BACD;AACD,WAAO;;GAIX,MAAM,aAAa,UACjB,SACA,UACA,SAAS,WAAW,EAAE,EACtB,SAAS,cACV;AAED,UAAO;IACL,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,YACA,oBAAoB,IAAI,SAAS,CAClC;IACD,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,oBAAoB;IAC5D,YAAY;IACZ,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,OAAO,KAAK,YAAY,YAAY,CAAC,KAAK,QAAQ,MAAM,EAAE;AAC5D,WAAO,KAAK,YAAY,KAAK,SAAS,sBAAsB;AAC5D,kBAAc,gBAAgB,KAAK,UAAU,qBAAqB;AAClE,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,uBAAuB,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAMvE,KAAI,UAAU,QAAQ;EAIpB,MAAM,sBAAgC,EAAE;EACxC,MAAM,cAAc,UAAU,OAC3B,KAAK,aAAa;GACjB,MAAM,UAAU,qBAAqB,SAAS;GAC9C,MAAM,eAAe,YAAY,SAAS;AAC1C,OAAI,CAAC,SAAS;AACZ,WAAO,KAAK,YAAY,aAAa,+BAA+B;AACpE,kBACE,gBACA,cACA,8BACD;AACD,WAAO;;AAET,OAAI,qBAAqB,QAAQ,EAAE;AACjC,wBAAoB,KAAK,aAAa;AACtC,WAAO;;AAET,UAAO;IACL;IACA,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,SAAS;IACjD,QAAQ,eAAe,aAAa;IACpC,WAAW,eAAe,QAAQ;IAClC,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS,SAAS,KAAK;AAClC,MAAI,oBAAoB,SAAS,EAC/B,iBAAgB,uBAAuB,oBAAoB,CAAC;AAE9D,QAAM,KAAK,GAAG,YAAY;;AAM5B,MAAK,MAAM,CAAC,UAAU,eAAe;EACnC,CAAC,cAAc,cAAc;EAC7B,CAAC,kBAAkB,kBAAkB;EACrC,CAAC,kBAAkB,kBAAkB;EACrC,CAAC,OAAO,MAAM;EACf,EAAW;AACV,MAAI,CAAC,UAAU,UAAW;EAC1B,MAAM,gBAAgB,UAAU,UAC7B,KAAK,aAAa;GACjB,MAAM,eAAe,YAAY,SAAS;GAG1C,IAAI;AACJ,OAAI;AACF,cAAU,gBAAgB,UAAU,WAAW;YACxC,OAAO;IAGd,MAAM,SAAS,uBAAuB,MAAM;AAC5C,WAAO,KAAK,YAAY,aAAa,IAAI,SAAS;AAClD,kBAAc,gBAAgB,cAAc,OAAO;AACnD,WAAO;;AAET,UAAO;IACL;IACA,UAAU;IACV;IACA,GAAG,2BAA2B,UAAU,SAAS;IACjD,QAAQ,eAAe,aAAa;IACpC,WAAW,cAAc,SAAS,oBAAoB,IAAI,SAAS,CAAC;IACpE,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS;AAChB,OAAI,CAAC,KAAM,QAAO;AAClB,OAAI,CAAC,KAAK,QAAQ,MAAM,EAAE;AACxB,WAAO,KAAK,YAAY,KAAK,SAAS,iBAAiB;AACvD,kBAAc,gBAAgB,KAAK,UAAU,gBAAgB;AAC7D,WAAO;;AAET,UAAO;IACP;AACJ,QAAM,KAAK,GAAG,cAAc,QAAQ,SAAS,SAAS,KAAK,CAAC;;AAO9D,KAAI,UAAU,WAAW;EACvB,MAAM,2BACJ,EAAE;EACJ,MAAM,iBAAiB,UAAU,UAC9B,KAAK,aAAa;GACjB,MAAM,UAAU,SAAS,SAAS;GAClC,MAAM,eAAe,YAAY,SAAS;GAE1C,IAAI;AACJ,OAAI;AACF,cAAU,sBAAsB,QAAQ;YACjC,OAAO;IACd,MAAM,SACJ,iBAAiB,QACb,MAAM,UACN;AACN,WAAO,KAAK,YAAY,aAAa,IAAI,SAAS;AAClD,kBAAc,gBAAgB,cAAc,OAAO;AACnD,WAAO;;AAOT,OACE,IAAI,uBAAuB,QAAQ,eAAe,KAClD,IAAI,uBAAuB,SAAS,cAAc,EAClD;AACA,6BAAyB,KAAK;KAC5B,MAAM;KACN,gBAAgB,QAAQ;KACzB,CAAC;AACF,WAAO;;GAET,MAAM,cAAc,wBAClB,mBAAmB,QAAQ,CAC5B;AAED,UAAO;IACL,SAAS;IACT,UAAU;IACV,YAAY;IACZ,GAAG,2BAA2B,UAAU,YAAY;IACpD,QAAQ,eAAe,aAAa;IACpC,WAAW,cACT,aACA,oBAAoB,IAAI,SAAS,CAClC;IACD,QAAQ,SAAS;IAClB;IACD,CACD,QAAQ,SAAS,SAAS,KAAK;AAClC,MAAI,yBAAyB,SAAS,EACpC,iBACE,qCACE,0BACA,SAAS,cACV,CACF;AAEH,QAAM,KAAK,GAAG,eAAe;;AAG/B,MAAK,MAAM,YAAY,2BAA2B;AAChD,MACE,aAAa,UACb,aAAa,UACb,aAAa,uBACb,aAAa,YACb,aAAa,gBACb,aAAa,oBACb,aAAa,oBACb,aAAa,eACb,aAAa,MAEb;AACF,MAAI,UAAU,WAAW;GACvB,MAAM,SAAS,UAAU,UACtB,KAAK,aAAa;IACjB,MAAM,UAAU,SAAS,SAAS;IAClC,MAAM,eAAe,YAAY,SAAS;IAE1C,MAAM,YAAY,kBAChB,SACA,cACA,UACA,SACD;AAED,QAAI,OAAO,cAAc,UAAU;AACjC,YAAO,KAAK,YAAY,aAAa,IAAI,UAAU,OAAO;AAC1D,mBAAc,gBAAgB,cAAc,UAAU,KAAK;AAC3D,YAAO;;AAGT,WAAO;KACL,SAAS;KACT,UAAU;KACV,YAAY,gCAAgC;KAC5C,GAAG,2BAA2B,UAAU,SAAS;KACjD,QAAQ,eAAe,aAAa;KACpC,WAAW,cACT,WACA,oBAAoB,IAAI,SAAS,CAClC;KACD,QAAQ,SAAS;KAClB;KACD,CACD,QAAQ,SAAS;AAChB,QACE,CAAC,QACD,OAAO,KAAK,YAAY,YACxB,CAAC,KAAK,QAAQ,MAAM,EACpB;AACA,YAAO,KACL,YAAY,MAAM,YAAY,UAAU,oCACzC;AACD,mBACE,gBACA,MAAM,YAAY,WAClB,mCACD;AACD,YAAO;;AAET,WAAO;KACP;AACJ,SAAM,KAAK,GAAG,OAAO,QAAQ,SAAS,SAAS,KAAK,CAAC;;;CAKzD,MAAM,eAAe,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,OAAO,CAAC;AACxD,MAAK,MAAM,UAAU,WAAW,MAAM,CACpC,KAAI,CAAC,aAAa,IAAI,OAAO,CAC3B,YAAW,OAAO,OAAO;AAI7B,QAAO;EAAE;EAAO;EAAY"}
@@ -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", "xcstrings"];
1
+ export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "pot", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson", "lottie", "dotStrings", "dotStringsdict", "androidStrings", "xcstrings", "srt"];
2
2
  export declare const FILE_EXT_TO_EXT_LABEL: {
3
3
  json: string;
4
4
  pot: string;
@@ -15,4 +15,5 @@ export declare const FILE_EXT_TO_EXT_LABEL: {
15
15
  dotStringsdict: string;
16
16
  androidStrings: string;
17
17
  xcstrings: string;
18
+ srt: string;
18
19
  };
@@ -14,7 +14,8 @@ const SUPPORTED_FILE_EXTENSIONS = [
14
14
  "dotStrings",
15
15
  "dotStringsdict",
16
16
  "androidStrings",
17
- "xcstrings"
17
+ "xcstrings",
18
+ "srt"
18
19
  ];
19
20
  const FILE_EXT_TO_EXT_LABEL = {
20
21
  json: "JSON",
@@ -31,7 +32,8 @@ const FILE_EXT_TO_EXT_LABEL = {
31
32
  dotStrings: ".strings",
32
33
  dotStringsdict: ".stringsdict",
33
34
  androidStrings: "Android strings.xml",
34
- xcstrings: ".xcstrings"
35
+ xcstrings: ".xcstrings",
36
+ srt: "SubRip subtitles (.srt)"
35
37
  };
36
38
  //#endregion
37
39
  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 '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"}
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 'srt',\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 srt: 'SubRip subtitles (.srt)',\n};\n"],"mappings":";AAAA,MAAa,4BAA4B;CACvC;CACA;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;CACX,KAAK;CACN"}
@@ -21,6 +21,7 @@ export declare const CONFIG_FILE_TYPE_TO_FILE_FORMAT: {
21
21
  readonly dotStringsdict: "DOT_STRINGSDICT";
22
22
  readonly androidStrings: "ANDROID_STRINGS";
23
23
  readonly xcstrings: "XCSTRINGS";
24
+ readonly srt: "SRT";
24
25
  };
25
26
  /**
26
27
  * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
@@ -41,6 +42,7 @@ export declare const FILE_FORMAT_TO_CONFIG_FILE_TYPE: {
41
42
  readonly DOT_STRINGSDICT: "dotStringsdict";
42
43
  readonly ANDROID_STRINGS: "androidStrings";
43
44
  readonly XCSTRINGS: "xcstrings";
45
+ readonly SRT: "srt";
44
46
  };
45
47
  /**
46
48
  * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
@@ -24,7 +24,8 @@ const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {
24
24
  dotStrings: "DOT_STRINGS",
25
25
  dotStringsdict: "DOT_STRINGSDICT",
26
26
  androidStrings: "ANDROID_STRINGS",
27
- xcstrings: "XCSTRINGS"
27
+ xcstrings: "XCSTRINGS",
28
+ srt: "SRT"
28
29
  };
29
30
  /**
30
31
  * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
@@ -44,7 +45,8 @@ const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {
44
45
  DOT_STRINGS: "dotStrings",
45
46
  DOT_STRINGSDICT: "dotStringsdict",
46
47
  ANDROID_STRINGS: "androidStrings",
47
- XCSTRINGS: "xcstrings"
48
+ XCSTRINGS: "xcstrings",
49
+ SRT: "srt"
48
50
  };
49
51
  /**
50
52
  * Maps API file format enum values to the extension the CLI should write.
@@ -67,7 +69,8 @@ const FILE_FORMAT_EXTENSIONS = {
67
69
  DOT_STRINGS: "strings",
68
70
  DOT_STRINGSDICT: "stringsdict",
69
71
  ANDROID_STRINGS: "xml",
70
- XCSTRINGS: "xcstrings"
72
+ XCSTRINGS: "xcstrings",
73
+ SRT: "srt"
71
74
  };
72
75
  /**
73
76
  * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
@@ -1 +1 @@
1
- {"version":3,"file":"transformFormat.js","names":["FILE_FORMATS"],"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 { FileFormat as FILE_FORMATS } from 'generaltranslation/api';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\nconst fileFormatValues = new Set<string>(Object.values(FILE_FORMATS));\n\n/** Narrows a user-supplied string to a FileFormat the API contract knows. */\nexport function isFileFormat(value: string): value is FileFormat {\n return fileFormatValues.has(value);\n}\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();\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (\n !isFileFormat(normalized) ||\n !isSupportedFileFormatTransform(fileFormat, normalized)\n ) {\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":";;;AASA,MAAM,mBAAmB,IAAI,IAAY,OAAO,OAAOA,WAAa,CAAC;;AAGrE,SAAgB,aAAa,OAAoC;AAC/D,QAAO,iBAAiB,IAAI,MAAM;;;;;AAMpC,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,KACE,CAAC,aAAa,WAAW,IACzB,CAAC,+BAA+B,YAAY,WAAW,CAEvD,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"}
1
+ {"version":3,"file":"transformFormat.js","names":["FILE_FORMATS"],"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 { FileFormat as FILE_FORMATS } from 'generaltranslation/api';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\nconst fileFormatValues = new Set<string>(Object.values(FILE_FORMATS));\n\n/** Narrows a user-supplied string to a FileFormat the API contract knows. */\nexport function isFileFormat(value: string): value is FileFormat {\n return fileFormatValues.has(value);\n}\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 srt: 'SRT',\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 SRT: 'srt',\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 SRT: 'srt',\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();\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (\n !isFileFormat(normalized) ||\n !isSupportedFileFormatTransform(fileFormat, normalized)\n ) {\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":";;;AASA,MAAM,mBAAmB,IAAI,IAAY,OAAO,OAAOA,WAAa,CAAC;;AAGrE,SAAgB,aAAa,OAAoC;AAC/D,QAAO,iBAAiB,IAAI,MAAM;;;;;AAMpC,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;CACX,KAAK;CACN;;;;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;CACX,KAAK;CACN;;;;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;CACX,KAAK;CACN;;;;;;;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,KACE,CAAC,aAAa,WAAW,IACzB,CAAC,+BAA+B,YAAY,WAAW,CAEvD,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"}
@@ -4,9 +4,14 @@ import { isBinaryFileFormat } from "generaltranslation/types";
4
4
  /**
5
5
  * Formats whose bytes on disk may carry an encoding other than UTF-8, so reads
6
6
  * decode by byte order mark and writes restore it. Older Xcode wrote both of
7
- * Apple's localization formats as UTF-16.
7
+ * Apple's localization formats as UTF-16, and Windows subtitle tools write
8
+ * SubRip files that way too.
8
9
  */
9
- const BYTE_ORDER_MARK_FORMATS = new Set(["DOT_STRINGS", "DOT_STRINGSDICT"]);
10
+ const BYTE_ORDER_MARK_FORMATS = new Set([
11
+ "DOT_STRINGS",
12
+ "DOT_STRINGSDICT",
13
+ "SRT"
14
+ ]);
10
15
  /**
11
16
  * Ingress. Reads a file as the content the pipeline carries for its format:
12
17
  * base64 for binary formats, otherwise UTF-8 text decoded from whatever
@@ -1 +1 @@
1
- {"version":3,"file":"fileContent.js","names":[],"sources":["../../src/fs/fileContent.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { isBinaryFileFormat, type FileFormat } from 'generaltranslation/types';\n\n/**\n * How file content crosses the disk boundary.\n *\n * This is the only module in the CLI that knows a file may be stored in\n * something other than UTF-8. `readFileContent` is the single ingress point and\n * `encodeFileContent` the single egress point; in between, content is a UTF-8\n * string and nothing else has to reason about encodings.\n */\n\n/**\n * On-disk byte layouts Foundation accepts for `.strings` and `.stringsdict`.\n *\n * A byte order mark is the only signal. Foundation does not sniff UTF-16 from\n * interleaved NUL bytes, so an unmarked file is UTF-8 even when its bytes are\n * plainly UTF-16 — `plutil` rejects those files, and guessing would translate\n * content Foundation itself cannot read.\n */\nexport type FileEncoding =\n | 'utf8'\n | 'utf8-bom'\n | 'utf16le'\n | 'utf16be'\n | 'utf32le'\n | 'utf32be';\n\n/**\n * Formats whose bytes on disk may carry an encoding other than UTF-8, so reads\n * decode by byte order mark and writes restore it. Older Xcode wrote both of\n * Apple's localization formats as UTF-16.\n */\nconst BYTE_ORDER_MARK_FORMATS: ReadonlySet<FileFormat> = new Set<FileFormat>([\n 'DOT_STRINGS',\n 'DOT_STRINGSDICT',\n]);\n\n/**\n * Ingress. Reads a file as the content the pipeline carries for its format:\n * base64 for binary formats, otherwise UTF-8 text decoded from whatever\n * encoding the file happens to be stored in.\n *\n * A file that cannot be read throws rather than reading as empty, which the\n * pipeline cannot tell apart from a file that is genuinely empty and would\n * upload as a deletion of every string in it.\n * @param {string} filePath - The path to the file to read.\n * @param {FileFormat} fileFormat - The format the file is configured as.\n * @returns {string} - The content.\n */\nexport function readFileContent(\n filePath: string,\n fileFormat: FileFormat\n): string {\n if (isBinaryFileFormat(fileFormat)) {\n return fs.readFileSync(filePath).toString('base64');\n }\n if (BYTE_ORDER_MARK_FORMATS.has(fileFormat)) {\n return decodeFileText(fs.readFileSync(filePath)).text;\n }\n return fs.readFileSync(filePath, 'utf8');\n}\n\n/**\n * Egress. Renders translated content for disk in the encoding `sourcePath` is\n * stored in, so a repository of UTF-16 files stays UTF-16 rather than showing\n * every one of them as rewritten.\n *\n * The source file is the record of that encoding, which is why it is re-read\n * here rather than carried from ingress: the download that writes this file is\n * usually a different process run from the upload that read it, so there is no\n * value to carry. Nothing has to be persisted in the lockfile or on the server.\n *\n * A source moved or deleted since it was uploaded leaves the translation\n * already on disk as the only remaining record, so `outputPath` is read next.\n * Defaulting straight to UTF-8 would rewrite every UTF-16 file and, for\n * `.stringsdict`, strand a UTF-16 XML declaration over unmarked UTF-8 bytes —\n * the one combination Foundation rejects.\n * @param {string} content - The translated content, as UTF-8 text.\n * @param {FileFormat} fileFormat - The format being written.\n * @param {string} sourcePath - The source file this translation came from.\n * @param {string} [outputPath] - Where the translation is about to be written.\n * @returns The value to hand to `writeFile`.\n */\nexport function encodeFileContent(\n content: string,\n fileFormat: FileFormat | undefined,\n sourcePath: string,\n outputPath?: string\n): string | Buffer {\n if (!fileFormat || !BYTE_ORDER_MARK_FORMATS.has(fileFormat)) return content;\n const encoding =\n readFileEncoding(sourcePath) ??\n (outputPath ? readFileEncoding(outputPath) : undefined) ??\n 'utf8';\n return encoding === 'utf8' ? content : encodeFileText(content, encoding);\n}\n\n/**\n * Reads a file's byte order mark without reading the rest of it. Undefined\n * separates \"no such file\" from a file that is genuinely unmarked UTF-8.\n */\nfunction readFileEncoding(filePath: string): FileEncoding | undefined {\n if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {\n return undefined;\n }\n const handle = fs.openSync(filePath, 'r');\n try {\n const mark = Buffer.alloc(LONGEST_BYTE_ORDER_MARK);\n const read = fs.readSync(handle, mark, 0, mark.length, 0);\n return detectFileEncoding(mark.subarray(0, read));\n } finally {\n fs.closeSync(handle);\n }\n}\n\nconst BYTE_ORDER_MARKS = [\n // UTF-32LE is checked before UTF-16LE because its mark begins with the same\n // two bytes; the reverse order silently decodes UTF-32 as NUL-padded UTF-16.\n ['utf32le', [0xff, 0xfe, 0x00, 0x00]],\n ['utf32be', [0x00, 0x00, 0xfe, 0xff]],\n ['utf16le', [0xff, 0xfe]],\n ['utf16be', [0xfe, 0xff]],\n ['utf8-bom', [0xef, 0xbb, 0xbf]],\n] as const satisfies ReadonlyArray<readonly [FileEncoding, number[]]>;\n\nconst BYTE_ORDER_MARK_BY_ENCODING = new Map<FileEncoding, Buffer>(\n BYTE_ORDER_MARKS.map(([encoding, mark]) => [encoding, Buffer.from(mark)])\n);\n\n/** Bytes that must be in hand before a byte order mark can be ruled out. */\nconst LONGEST_BYTE_ORDER_MARK = Math.max(\n ...BYTE_ORDER_MARKS.map(([, mark]) => mark.length)\n);\n\n/** Reads the byte order mark, defaulting to UTF-8 when there is none. */\nexport function detectFileEncoding(bytes: Buffer): FileEncoding {\n for (const [encoding, mark] of BYTE_ORDER_MARKS) {\n if (bytes.length >= mark.length && mark.every((b, i) => bytes[i] === b)) {\n return encoding;\n }\n }\n return 'utf8';\n}\n\n/**\n * Decodes a `.strings` or `.stringsdict` file to text, dropping the byte order\n * mark. The encoding is returned so the writer can restore both on the way out.\n */\nexport function decodeFileText(bytes: Buffer): {\n text: string;\n encoding: FileEncoding;\n} {\n const encoding = detectFileEncoding(bytes);\n const body = bytes.subarray(\n BYTE_ORDER_MARK_BY_ENCODING.get(encoding)?.length ?? 0\n );\n\n switch (encoding) {\n case 'utf16le':\n return { text: decodeUtf16(body, true), encoding };\n case 'utf16be':\n return { text: decodeUtf16(body, false), encoding };\n case 'utf32le':\n return { text: decodeUtf32(body, true), encoding };\n case 'utf32be':\n return { text: decodeUtf32(body, false), encoding };\n default:\n return { text: body.toString('utf8'), encoding };\n }\n}\n\n/** Re-encodes text into the given layout, byte order mark included. */\nexport function encodeFileText(text: string, encoding: FileEncoding): Buffer {\n const mark = BYTE_ORDER_MARK_BY_ENCODING.get(encoding);\n const body = (() => {\n switch (encoding) {\n case 'utf16le':\n return Buffer.from(text, 'utf16le');\n case 'utf16be':\n return Buffer.from(text, 'utf16le').swap16();\n case 'utf32le':\n return encodeUtf32(text, true);\n case 'utf32be':\n return encodeUtf32(text, false);\n default:\n return Buffer.from(text, 'utf8');\n }\n })();\n return mark ? Buffer.concat([mark, body]) : body;\n}\n\nfunction decodeUtf16(body: Buffer, littleEndian: boolean): string {\n if (body.length % 2 !== 0) {\n throw new Error(\n 'it has a UTF-16 byte order mark but an odd number of bytes. Re-save it as UTF-8, or as complete UTF-16'\n );\n }\n if (littleEndian) return body.toString('utf16le');\n // swap16 reorders in place, so copy rather than mutating the caller's bytes.\n return Buffer.from(body).swap16().toString('utf16le');\n}\n\n// String.fromCodePoint takes code points as arguments, so a whole file spread\n// in one call can overflow the stack.\nconst CODE_POINTS_PER_CALL = 4096;\n\nfunction decodeUtf32(body: Buffer, littleEndian: boolean): string {\n if (body.length % 4 !== 0) {\n throw new Error(\n 'it has a UTF-32 byte order mark but its length is not a multiple of four bytes. Re-save it as UTF-8, or as complete UTF-32'\n );\n }\n const codePoints = Array.from({ length: body.length / 4 }, (_, i) =>\n littleEndian ? body.readUInt32LE(i * 4) : body.readUInt32BE(i * 4)\n );\n let text = '';\n for (let i = 0; i < codePoints.length; i += CODE_POINTS_PER_CALL) {\n try {\n text += String.fromCodePoint(\n ...codePoints.slice(i, i + CODE_POINTS_PER_CALL)\n );\n } catch {\n throw new Error(\n 'it has a UTF-32 byte order mark but contains a value that is not a Unicode code point'\n );\n }\n }\n return text;\n}\n\nfunction encodeUtf32(text: string, littleEndian: boolean): Buffer {\n // Iterating a string yields whole code points, so surrogate pairs stay intact.\n const characters = Array.from(text);\n const body = Buffer.alloc(characters.length * 4);\n characters.forEach((character, index) => {\n const codePoint = character.codePointAt(0) ?? 0;\n if (littleEndian) body.writeUInt32LE(codePoint, index * 4);\n else body.writeUInt32BE(codePoint, index * 4);\n });\n return body;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileContent.js","names":[],"sources":["../../src/fs/fileContent.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { isBinaryFileFormat, type FileFormat } from 'generaltranslation/types';\n\n/**\n * How file content crosses the disk boundary.\n *\n * This is the only module in the CLI that knows a file may be stored in\n * something other than UTF-8. `readFileContent` is the single ingress point and\n * `encodeFileContent` the single egress point; in between, content is a UTF-8\n * string and nothing else has to reason about encodings.\n */\n\n/**\n * On-disk byte layouts Foundation accepts for `.strings` and `.stringsdict`.\n *\n * A byte order mark is the only signal. Foundation does not sniff UTF-16 from\n * interleaved NUL bytes, so an unmarked file is UTF-8 even when its bytes are\n * plainly UTF-16 — `plutil` rejects those files, and guessing would translate\n * content Foundation itself cannot read.\n */\nexport type FileEncoding =\n | 'utf8'\n | 'utf8-bom'\n | 'utf16le'\n | 'utf16be'\n | 'utf32le'\n | 'utf32be';\n\n/**\n * Formats whose bytes on disk may carry an encoding other than UTF-8, so reads\n * decode by byte order mark and writes restore it. Older Xcode wrote both of\n * Apple's localization formats as UTF-16, and Windows subtitle tools write\n * SubRip files that way too.\n */\nconst BYTE_ORDER_MARK_FORMATS: ReadonlySet<FileFormat> = new Set<FileFormat>([\n 'DOT_STRINGS',\n 'DOT_STRINGSDICT',\n 'SRT',\n]);\n\n/**\n * Ingress. Reads a file as the content the pipeline carries for its format:\n * base64 for binary formats, otherwise UTF-8 text decoded from whatever\n * encoding the file happens to be stored in.\n *\n * A file that cannot be read throws rather than reading as empty, which the\n * pipeline cannot tell apart from a file that is genuinely empty and would\n * upload as a deletion of every string in it.\n * @param {string} filePath - The path to the file to read.\n * @param {FileFormat} fileFormat - The format the file is configured as.\n * @returns {string} - The content.\n */\nexport function readFileContent(\n filePath: string,\n fileFormat: FileFormat\n): string {\n if (isBinaryFileFormat(fileFormat)) {\n return fs.readFileSync(filePath).toString('base64');\n }\n if (BYTE_ORDER_MARK_FORMATS.has(fileFormat)) {\n return decodeFileText(fs.readFileSync(filePath)).text;\n }\n return fs.readFileSync(filePath, 'utf8');\n}\n\n/**\n * Egress. Renders translated content for disk in the encoding `sourcePath` is\n * stored in, so a repository of UTF-16 files stays UTF-16 rather than showing\n * every one of them as rewritten.\n *\n * The source file is the record of that encoding, which is why it is re-read\n * here rather than carried from ingress: the download that writes this file is\n * usually a different process run from the upload that read it, so there is no\n * value to carry. Nothing has to be persisted in the lockfile or on the server.\n *\n * A source moved or deleted since it was uploaded leaves the translation\n * already on disk as the only remaining record, so `outputPath` is read next.\n * Defaulting straight to UTF-8 would rewrite every UTF-16 file and, for\n * `.stringsdict`, strand a UTF-16 XML declaration over unmarked UTF-8 bytes —\n * the one combination Foundation rejects.\n * @param {string} content - The translated content, as UTF-8 text.\n * @param {FileFormat} fileFormat - The format being written.\n * @param {string} sourcePath - The source file this translation came from.\n * @param {string} [outputPath] - Where the translation is about to be written.\n * @returns The value to hand to `writeFile`.\n */\nexport function encodeFileContent(\n content: string,\n fileFormat: FileFormat | undefined,\n sourcePath: string,\n outputPath?: string\n): string | Buffer {\n if (!fileFormat || !BYTE_ORDER_MARK_FORMATS.has(fileFormat)) return content;\n const encoding =\n readFileEncoding(sourcePath) ??\n (outputPath ? readFileEncoding(outputPath) : undefined) ??\n 'utf8';\n return encoding === 'utf8' ? content : encodeFileText(content, encoding);\n}\n\n/**\n * Reads a file's byte order mark without reading the rest of it. Undefined\n * separates \"no such file\" from a file that is genuinely unmarked UTF-8.\n */\nfunction readFileEncoding(filePath: string): FileEncoding | undefined {\n if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {\n return undefined;\n }\n const handle = fs.openSync(filePath, 'r');\n try {\n const mark = Buffer.alloc(LONGEST_BYTE_ORDER_MARK);\n const read = fs.readSync(handle, mark, 0, mark.length, 0);\n return detectFileEncoding(mark.subarray(0, read));\n } finally {\n fs.closeSync(handle);\n }\n}\n\nconst BYTE_ORDER_MARKS = [\n // UTF-32LE is checked before UTF-16LE because its mark begins with the same\n // two bytes; the reverse order silently decodes UTF-32 as NUL-padded UTF-16.\n ['utf32le', [0xff, 0xfe, 0x00, 0x00]],\n ['utf32be', [0x00, 0x00, 0xfe, 0xff]],\n ['utf16le', [0xff, 0xfe]],\n ['utf16be', [0xfe, 0xff]],\n ['utf8-bom', [0xef, 0xbb, 0xbf]],\n] as const satisfies ReadonlyArray<readonly [FileEncoding, number[]]>;\n\nconst BYTE_ORDER_MARK_BY_ENCODING = new Map<FileEncoding, Buffer>(\n BYTE_ORDER_MARKS.map(([encoding, mark]) => [encoding, Buffer.from(mark)])\n);\n\n/** Bytes that must be in hand before a byte order mark can be ruled out. */\nconst LONGEST_BYTE_ORDER_MARK = Math.max(\n ...BYTE_ORDER_MARKS.map(([, mark]) => mark.length)\n);\n\n/** Reads the byte order mark, defaulting to UTF-8 when there is none. */\nexport function detectFileEncoding(bytes: Buffer): FileEncoding {\n for (const [encoding, mark] of BYTE_ORDER_MARKS) {\n if (bytes.length >= mark.length && mark.every((b, i) => bytes[i] === b)) {\n return encoding;\n }\n }\n return 'utf8';\n}\n\n/**\n * Decodes a `.strings` or `.stringsdict` file to text, dropping the byte order\n * mark. The encoding is returned so the writer can restore both on the way out.\n */\nexport function decodeFileText(bytes: Buffer): {\n text: string;\n encoding: FileEncoding;\n} {\n const encoding = detectFileEncoding(bytes);\n const body = bytes.subarray(\n BYTE_ORDER_MARK_BY_ENCODING.get(encoding)?.length ?? 0\n );\n\n switch (encoding) {\n case 'utf16le':\n return { text: decodeUtf16(body, true), encoding };\n case 'utf16be':\n return { text: decodeUtf16(body, false), encoding };\n case 'utf32le':\n return { text: decodeUtf32(body, true), encoding };\n case 'utf32be':\n return { text: decodeUtf32(body, false), encoding };\n default:\n return { text: body.toString('utf8'), encoding };\n }\n}\n\n/** Re-encodes text into the given layout, byte order mark included. */\nexport function encodeFileText(text: string, encoding: FileEncoding): Buffer {\n const mark = BYTE_ORDER_MARK_BY_ENCODING.get(encoding);\n const body = (() => {\n switch (encoding) {\n case 'utf16le':\n return Buffer.from(text, 'utf16le');\n case 'utf16be':\n return Buffer.from(text, 'utf16le').swap16();\n case 'utf32le':\n return encodeUtf32(text, true);\n case 'utf32be':\n return encodeUtf32(text, false);\n default:\n return Buffer.from(text, 'utf8');\n }\n })();\n return mark ? Buffer.concat([mark, body]) : body;\n}\n\nfunction decodeUtf16(body: Buffer, littleEndian: boolean): string {\n if (body.length % 2 !== 0) {\n throw new Error(\n 'it has a UTF-16 byte order mark but an odd number of bytes. Re-save it as UTF-8, or as complete UTF-16'\n );\n }\n if (littleEndian) return body.toString('utf16le');\n // swap16 reorders in place, so copy rather than mutating the caller's bytes.\n return Buffer.from(body).swap16().toString('utf16le');\n}\n\n// String.fromCodePoint takes code points as arguments, so a whole file spread\n// in one call can overflow the stack.\nconst CODE_POINTS_PER_CALL = 4096;\n\nfunction decodeUtf32(body: Buffer, littleEndian: boolean): string {\n if (body.length % 4 !== 0) {\n throw new Error(\n 'it has a UTF-32 byte order mark but its length is not a multiple of four bytes. Re-save it as UTF-8, or as complete UTF-32'\n );\n }\n const codePoints = Array.from({ length: body.length / 4 }, (_, i) =>\n littleEndian ? body.readUInt32LE(i * 4) : body.readUInt32BE(i * 4)\n );\n let text = '';\n for (let i = 0; i < codePoints.length; i += CODE_POINTS_PER_CALL) {\n try {\n text += String.fromCodePoint(\n ...codePoints.slice(i, i + CODE_POINTS_PER_CALL)\n );\n } catch {\n throw new Error(\n 'it has a UTF-32 byte order mark but contains a value that is not a Unicode code point'\n );\n }\n }\n return text;\n}\n\nfunction encodeUtf32(text: string, littleEndian: boolean): Buffer {\n // Iterating a string yields whole code points, so surrogate pairs stay intact.\n const characters = Array.from(text);\n const body = Buffer.alloc(characters.length * 4);\n characters.forEach((character, index) => {\n const codePoint = character.codePointAt(0) ?? 0;\n if (littleEndian) body.writeUInt32LE(codePoint, index * 4);\n else body.writeUInt32BE(codePoint, index * 4);\n });\n return body;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.22.1";
1
+ export declare const PACKAGE_VERSION = "2.22.2";
@@ -1,5 +1,5 @@
1
1
  //#region src/generated/version.ts
2
- const PACKAGE_VERSION = "2.22.1";
2
+ const PACKAGE_VERSION = "2.22.2";
3
3
  //#endregion
4
4
  export { PACKAGE_VERSION };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.22.1';\n"],"mappings":";AACA,MAAa,kBAAkB"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.22.2';\n"],"mappings":";AACA,MAAa,kBAAkB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.22.1",
3
+ "version": "2.22.2",
4
4
  "main": "dist/index.js",
5
5
  "bin": "bin/main.js",
6
6
  "files": [
@@ -118,10 +118,10 @@
118
118
  "yaml": "^2.8.0",
119
119
  "@generaltranslation/format": "0.1.9",
120
120
  "@generaltranslation/icu": "0.1.2",
121
- "@generaltranslation/python-extractor": "0.2.54",
122
- "@generaltranslation/supported-locales": "2.1.34",
123
- "@generaltranslation/vue-extractor": "0.1.14",
124
- "generaltranslation": "9.5.0",
121
+ "@generaltranslation/python-extractor": "0.2.55",
122
+ "@generaltranslation/supported-locales": "2.1.35",
123
+ "@generaltranslation/vue-extractor": "0.1.15",
124
+ "generaltranslation": "9.5.1",
125
125
  "gt-remark": "1.0.12"
126
126
  },
127
127
  "devDependencies": {