gt 2.18.1 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/api/collectUserEditDiffs.js +23 -23
  3. package/dist/api/collectUserEditDiffs.js.map +1 -1
  4. package/dist/api/downloadFileBatch.js +7 -4
  5. package/dist/api/downloadFileBatch.js.map +1 -1
  6. package/dist/api/saveLocalEdits.js +2 -2
  7. package/dist/api/saveLocalEdits.js.map +1 -1
  8. package/dist/cli/base.d.ts +2 -0
  9. package/dist/cli/base.js +45 -2
  10. package/dist/cli/base.js.map +1 -1
  11. package/dist/cli/commands/api.d.ts +17 -0
  12. package/dist/cli/commands/api.js +153 -0
  13. package/dist/cli/commands/api.js.map +1 -0
  14. package/dist/cli/commands/upload.js +15 -5
  15. package/dist/cli/commands/upload.js.map +1 -1
  16. package/dist/cli/commands/utils/validation.d.ts +4 -0
  17. package/dist/cli/commands/utils/validation.js +12 -5
  18. package/dist/cli/commands/utils/validation.js.map +1 -1
  19. package/dist/config/generateSettings.js +7 -0
  20. package/dist/config/generateSettings.js.map +1 -1
  21. package/dist/console/index.d.ts +1 -0
  22. package/dist/console/index.js +2 -1
  23. package/dist/console/index.js.map +1 -1
  24. package/dist/formats/files/aggregateFiles.js +20 -8
  25. package/dist/formats/files/aggregateFiles.js.map +1 -1
  26. package/dist/formats/files/androidLocale.d.ts +7 -0
  27. package/dist/formats/files/androidLocale.js +32 -0
  28. package/dist/formats/files/androidLocale.js.map +1 -0
  29. package/dist/formats/files/fileMapping.js +31 -24
  30. package/dist/formats/files/fileMapping.js.map +1 -1
  31. package/dist/formats/files/localePath.d.ts +3 -0
  32. package/dist/formats/files/localePath.js +18 -0
  33. package/dist/formats/files/localePath.js.map +1 -0
  34. package/dist/formats/files/supportedFiles.d.ts +2 -1
  35. package/dist/formats/files/supportedFiles.js +4 -2
  36. package/dist/formats/files/supportedFiles.js.map +1 -1
  37. package/dist/formats/files/transformFormat.d.ts +2 -0
  38. package/dist/formats/files/transformFormat.js +6 -3
  39. package/dist/formats/files/transformFormat.js.map +1 -1
  40. package/dist/fs/config/parseFilesConfig.js +5 -1
  41. package/dist/fs/config/parseFilesConfig.js.map +1 -1
  42. package/dist/fs/fileContent.d.ts +65 -0
  43. package/dist/fs/fileContent.js +171 -0
  44. package/dist/fs/fileContent.js.map +1 -0
  45. package/dist/generated/version.d.ts +1 -1
  46. package/dist/generated/version.js +1 -1
  47. package/dist/generated/version.js.map +1 -1
  48. package/dist/state/recentDownloads.d.ts +3 -2
  49. package/dist/state/recentDownloads.js.map +1 -1
  50. package/dist/translation/reviewSetupWarning.js +3 -3
  51. package/dist/translation/reviewSetupWarning.js.map +1 -1
  52. package/dist/utils/api.d.ts +194 -0
  53. package/dist/utils/api.js +268 -0
  54. package/dist/utils/api.js.map +1 -0
  55. package/dist/utils/persistPostprocessHashes.d.ts +1 -7
  56. package/dist/utils/persistPostprocessHashes.js +15 -1
  57. package/dist/utils/persistPostprocessHashes.js.map +1 -1
  58. package/dist/workflows/download.js +4 -4
  59. package/dist/workflows/download.js.map +1 -1
  60. package/dist/workflows/enqueue.js +4 -4
  61. package/dist/workflows/enqueue.js.map +1 -1
  62. package/dist/workflows/publish.js +2 -2
  63. package/dist/workflows/publish.js.map +1 -1
  64. package/dist/workflows/setupProject.js +4 -4
  65. package/dist/workflows/setupProject.js.map +1 -1
  66. package/dist/workflows/stage.js +7 -7
  67. package/dist/workflows/stage.js.map +1 -1
  68. package/dist/workflows/steps/BranchStep.d.ts +2 -2
  69. package/dist/workflows/steps/BranchStep.js +9 -9
  70. package/dist/workflows/steps/BranchStep.js.map +1 -1
  71. package/dist/workflows/steps/DownloadStep.d.ts +3 -3
  72. package/dist/workflows/steps/DownloadStep.js +3 -3
  73. package/dist/workflows/steps/DownloadStep.js.map +1 -1
  74. package/dist/workflows/steps/EnqueueStep.d.ts +5 -3
  75. package/dist/workflows/steps/EnqueueStep.js.map +1 -1
  76. package/dist/workflows/steps/PollJobsStep.d.ts +3 -3
  77. package/dist/workflows/steps/PollJobsStep.js +6 -6
  78. package/dist/workflows/steps/PollJobsStep.js.map +1 -1
  79. package/dist/workflows/steps/PublishStep.d.ts +3 -3
  80. package/dist/workflows/steps/PublishStep.js +3 -3
  81. package/dist/workflows/steps/PublishStep.js.map +1 -1
  82. package/dist/workflows/steps/SetupStep.d.ts +6 -4
  83. package/dist/workflows/steps/SetupStep.js.map +1 -1
  84. package/dist/workflows/steps/TagStep.d.ts +3 -3
  85. package/dist/workflows/steps/TagStep.js +3 -3
  86. package/dist/workflows/steps/TagStep.js.map +1 -1
  87. package/dist/workflows/steps/UploadSourcesStep.d.ts +3 -3
  88. package/dist/workflows/steps/UploadSourcesStep.js +8 -6
  89. package/dist/workflows/steps/UploadSourcesStep.js.map +1 -1
  90. package/dist/workflows/steps/UploadTranslationsStep.d.ts +3 -3
  91. package/dist/workflows/steps/UploadTranslationsStep.js +24 -6
  92. package/dist/workflows/steps/UploadTranslationsStep.js.map +1 -1
  93. package/dist/workflows/upload.js +4 -4
  94. package/dist/workflows/upload.js.map +1 -1
  95. package/dist/workflows/utils/filterFilesForEnqueue.d.ts +2 -2
  96. package/dist/workflows/utils/filterFilesForEnqueue.js.map +1 -1
  97. package/dist/workflows/utils/queryCompletedTranslations.d.ts +3 -3
  98. package/dist/workflows/utils/queryCompletedTranslations.js +2 -2
  99. package/dist/workflows/utils/queryCompletedTranslations.js.map +1 -1
  100. package/package.json +5 -5
@@ -0,0 +1,18 @@
1
+ import { androidLocaleQualifier } from "./androidLocale.js";
2
+ //#region src/formats/files/localePath.ts
3
+ /**
4
+ * How each file type spells a locale inside a path.
5
+ *
6
+ * A file type belongs here only when its own tooling reads the locale back out
7
+ * of the path. Everything else must stay verbatim, because a transform or a
8
+ * static URL that spells a locale differently points at a file nothing wrote.
9
+ */
10
+ const LOCALE_PATH_SPELLING = { androidStrings: androidLocaleQualifier };
11
+ /** Returns the locale as `fileType` spells it in a path. */
12
+ function localeForFilePath(fileType, locale) {
13
+ return LOCALE_PATH_SPELLING[fileType]?.(locale) ?? locale;
14
+ }
15
+ //#endregion
16
+ export { localeForFilePath };
17
+
18
+ //# sourceMappingURL=localePath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localePath.js","names":[],"sources":["../../../src/formats/files/localePath.ts"],"sourcesContent":["import { androidLocaleQualifier } from './androidLocale.js';\n\nimport type { SupportedFileExtension } from '../../types/index.js';\n\n/**\n * How each file type spells a locale inside a path.\n *\n * A file type belongs here only when its own tooling reads the locale back out\n * of the path. Everything else must stay verbatim, because a transform or a\n * static URL that spells a locale differently points at a file nothing wrote.\n */\nconst LOCALE_PATH_SPELLING: Partial<\n Record<SupportedFileExtension, (locale: string) => string>\n> = {\n androidStrings: androidLocaleQualifier,\n};\n\n/** Returns the locale as `fileType` spells it in a path. */\nexport function localeForFilePath(\n fileType: SupportedFileExtension,\n locale: string\n): string {\n return LOCALE_PATH_SPELLING[fileType]?.(locale) ?? locale;\n}\n"],"mappings":";;;;;;;;;AAWA,MAAM,uBAEF,EACF,gBAAgB,wBACjB;;AAGD,SAAgB,kBACd,UACA,QACQ;AACR,QAAO,qBAAqB,YAAY,OAAO,IAAI"}
@@ -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"];
1
+ export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "pot", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson", "lottie", "dotStrings", "dotStringsdict", "androidStrings"];
2
2
  export declare const FILE_EXT_TO_EXT_LABEL: {
3
3
  json: string;
4
4
  pot: string;
@@ -13,4 +13,5 @@ export declare const FILE_EXT_TO_EXT_LABEL: {
13
13
  lottie: string;
14
14
  dotStrings: string;
15
15
  dotStringsdict: string;
16
+ androidStrings: string;
16
17
  };
@@ -12,7 +12,8 @@ const SUPPORTED_FILE_EXTENSIONS = [
12
12
  "twilioContentJson",
13
13
  "lottie",
14
14
  "dotStrings",
15
- "dotStringsdict"
15
+ "dotStringsdict",
16
+ "androidStrings"
16
17
  ];
17
18
  const FILE_EXT_TO_EXT_LABEL = {
18
19
  json: "JSON",
@@ -27,7 +28,8 @@ const FILE_EXT_TO_EXT_LABEL = {
27
28
  twilioContentJson: "Twilio Content JSON",
28
29
  lottie: "Lottie",
29
30
  dotStrings: ".strings",
30
- dotStringsdict: ".stringsdict"
31
+ dotStringsdict: ".stringsdict",
32
+ androidStrings: "Android strings.xml"
31
33
  };
32
34
  //#endregion
33
35
  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] 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};\n"],"mappings":";AAAA,MAAa,4BAA4B;CACvC;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;CACjB"}
1
+ {"version":3,"file":"supportedFiles.js","names":[],"sources":["../../../src/formats/files/supportedFiles.ts"],"sourcesContent":["export const SUPPORTED_FILE_EXTENSIONS = [\n 'json',\n 'pot',\n 'mdx',\n 'md',\n 'ts',\n 'js',\n 'yaml',\n 'html',\n 'txt',\n 'twilioContentJson',\n 'lottie',\n 'dotStrings',\n 'dotStringsdict',\n 'androidStrings',\n] as const;\n\nexport const FILE_EXT_TO_EXT_LABEL = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'Markdown',\n ts: 'TypeScript',\n js: 'JavaScript',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'Text',\n twilioContentJson: 'Twilio Content JSON',\n lottie: 'Lottie',\n dotStrings: '.strings',\n dotStringsdict: '.stringsdict',\n androidStrings: 'Android strings.xml',\n};\n"],"mappings":";AAAA,MAAa,4BAA4B;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAa,wBAAwB;CACnC,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CACjB"}
@@ -17,6 +17,7 @@ export declare const CONFIG_FILE_TYPE_TO_FILE_FORMAT: {
17
17
  readonly lottie: "LOTTIE";
18
18
  readonly dotStrings: "DOT_STRINGS";
19
19
  readonly dotStringsdict: "DOT_STRINGSDICT";
20
+ readonly androidStrings: "ANDROID_STRINGS";
20
21
  };
21
22
  /**
22
23
  * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
@@ -35,6 +36,7 @@ export declare const FILE_FORMAT_TO_CONFIG_FILE_TYPE: {
35
36
  readonly LOTTIE: "lottie";
36
37
  readonly DOT_STRINGS: "dotStrings";
37
38
  readonly DOT_STRINGSDICT: "dotStringsdict";
39
+ readonly ANDROID_STRINGS: "androidStrings";
38
40
  };
39
41
  /**
40
42
  * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
@@ -16,7 +16,8 @@ const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {
16
16
  twilioContentJson: "TWILIO_CONTENT_JSON",
17
17
  lottie: "LOTTIE",
18
18
  dotStrings: "DOT_STRINGS",
19
- dotStringsdict: "DOT_STRINGSDICT"
19
+ dotStringsdict: "DOT_STRINGSDICT",
20
+ androidStrings: "ANDROID_STRINGS"
20
21
  };
21
22
  /**
22
23
  * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
@@ -34,7 +35,8 @@ const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {
34
35
  TWILIO_CONTENT_JSON: "twilioContentJson",
35
36
  LOTTIE: "lottie",
36
37
  DOT_STRINGS: "dotStrings",
37
- DOT_STRINGSDICT: "dotStringsdict"
38
+ DOT_STRINGSDICT: "dotStringsdict",
39
+ ANDROID_STRINGS: "androidStrings"
38
40
  };
39
41
  /**
40
42
  * Maps API file format enum values to the extension the CLI should write.
@@ -55,7 +57,8 @@ const FILE_FORMAT_EXTENSIONS = {
55
57
  LOTTIE: "lottie",
56
58
  SVG: "svg",
57
59
  DOT_STRINGS: "strings",
58
- DOT_STRINGSDICT: "stringsdict"
60
+ DOT_STRINGSDICT: "stringsdict",
61
+ ANDROID_STRINGS: "xml"
59
62
  };
60
63
  /**
61
64
  * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
@@ -1 +1 @@
1
- {"version":3,"file":"transformFormat.js","names":[],"sources":["../../../src/formats/files/transformFormat.ts"],"sourcesContent":["import type { FileFormat } from '../../types/data.js';\nimport type {\n FilesOptions,\n Settings,\n SupportedFileExtension,\n} from '../../types/index.js';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\n/**\n * Maps CLI config file keys to API file format enum values.\n */\nexport const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'MD',\n ts: 'TS',\n js: 'JS',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'TXT',\n twilioContentJson: 'TWILIO_CONTENT_JSON',\n lottie: 'LOTTIE',\n dotStrings: 'DOT_STRINGS',\n dotStringsdict: 'DOT_STRINGSDICT',\n} 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} 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} as const satisfies Record<FileFormat, string>;\n\n/**\n * Converts uppercase file format config keys into the CLI's canonical lowercase keys.\n *\n * This lets users write either `files.POT` or `files.pot` while keeping the\n * rest of the CLI on its existing lowercase file-type convention.\n */\nexport function normalizeFilesOptions(files: FilesOptions): FilesOptions {\n const normalized = { ...files } as FilesOptions & Record<string, unknown>;\n\n for (const [fileFormat, fileType] of Object.entries(\n FILE_FORMAT_TO_CONFIG_FILE_TYPE\n ) as [string, SupportedFileExtension][]) {\n const uppercaseConfig = normalized[fileFormat] as\n | FilesOptions[SupportedFileExtension]\n | undefined;\n if (!normalized[fileType] && uppercaseConfig) {\n normalized[fileType] = uppercaseConfig;\n }\n delete normalized[fileFormat];\n }\n\n return normalized;\n}\n\n/**\n * Validates and resolves a configured output format for a source file type.\n *\n * Throws when the requested source -> output format is not supported by\n * `generaltranslation/internal`.\n */\nexport function resolveTransformationFormat(\n fileType: SupportedFileExtension,\n transformationFormat: string | undefined\n): FileFormat | undefined {\n if (!transformationFormat) return undefined;\n\n // Normalize to uppercase to match the FileFormat enum (e.g. \"po\" -> \"PO\")\n const normalized = transformationFormat.toUpperCase() as FileFormat;\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (!isSupportedFileFormatTransform(fileFormat, normalized)) {\n throw new Error(\n `Unsupported file format transform: ${fileFormat} -> ${normalized} in files.${fileType}. ` +\n `\"${normalized}\" is not a valid transformationFormat for ${fileFormat} source files.`\n );\n }\n\n return normalized;\n}\n\n/**\n * Returns the API upload/enqueue property for a file type when one is configured.\n */\nexport function getTransformFormatProperty(\n settings: Settings,\n fileType: SupportedFileExtension\n): { transformFormat?: FileFormat } {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return transformFormat ? { transformFormat } : {};\n}\n\n/**\n * Returns the preferred file extension for a translated file format.\n */\nexport function getFileExtensionForFormat(format: FileFormat): string {\n return FILE_FORMAT_EXTENSIONS[format];\n}\n\n/**\n * Rewrites a path's extension to match the translated file format.\n */\nexport function replaceFileExtensionForFormat(\n filePath: string,\n format: FileFormat\n): string {\n const extension = getFileExtensionForFormat(format);\n return /\\.[^/.]+$/.test(filePath)\n ? filePath.replace(/\\.[^/.]+$/, `.${extension}`)\n : `${filePath}.${extension}`;\n}\n\n/**\n * Detects whether any configured format transform changes the output file type.\n */\nexport function hasNonIdentityFileFormatTransform(settings: Settings): boolean {\n return Object.entries(settings.files?.transformFormats || {}).some(\n ([fileType, transformFormat]) =>\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType as SupportedFileExtension] !==\n transformFormat\n );\n}\n\n/**\n * Returns true when the configured transform for a file type changes its format.\n */\nexport function hasNonIdentityFileFormatTransformForType(\n settings: Settings,\n fileType: SupportedFileExtension\n): boolean {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return !!(\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !== transformFormat\n );\n}\n"],"mappings":";;;;;AAWA,MAAa,kCAAkC;CAC7C,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CACjB;;;;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;CAClB;;;;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;CAClB;;;;;;;AAQD,SAAgB,sBAAsB,OAAmC;CACvE,MAAM,aAAa,EAAE,GAAG,OAAO;AAE/B,MAAK,MAAM,CAAC,YAAY,aAAa,OAAO,QAC1C,gCACD,EAAwC;EACvC,MAAM,kBAAkB,WAAW;AAGnC,MAAI,CAAC,WAAW,aAAa,gBAC3B,YAAW,YAAY;AAEzB,SAAO,WAAW;;AAGpB,QAAO;;;;;;;;AAST,SAAgB,4BACd,UACA,sBACwB;AACxB,KAAI,CAAC,qBAAsB,QAAO,KAAA;CAGlC,MAAM,aAAa,qBAAqB,aAAa;CACrD,MAAM,aAAa,gCAAgC;AAEnD,KAAI,CAAC,+BAA+B,YAAY,WAAW,CACzD,OAAM,IAAI,MACR,sCAAsC,WAAW,MAAM,WAAW,YAAY,SAAS,KACjF,WAAW,4CAA4C,WAAW,gBACzE;AAGH,QAAO;;;;;AAMT,SAAgB,2BACd,UACA,UACkC;CAClC,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,kBAAkB,EAAE,iBAAiB,GAAG,EAAE;;;;;AAMnD,SAAgB,0BAA0B,QAA4B;AACpE,QAAO,uBAAuB;;;;;AAMhC,SAAgB,8BACd,UACA,QACQ;CACR,MAAM,YAAY,0BAA0B,OAAO;AACnD,QAAO,YAAY,KAAK,SAAS,GAC7B,SAAS,QAAQ,aAAa,IAAI,YAAY,GAC9C,GAAG,SAAS,GAAG;;;;;AAMrB,SAAgB,kCAAkC,UAA6B;AAC7E,QAAO,OAAO,QAAQ,SAAS,OAAO,oBAAoB,EAAE,CAAC,CAAC,MAC3D,CAAC,UAAU,qBACV,mBACA,gCAAgC,cAC9B,gBACL;;;;;AAMH,SAAgB,yCACd,UACA,UACS;CACT,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,CAAC,EACN,mBACA,gCAAgC,cAAc"}
1
+ {"version":3,"file":"transformFormat.js","names":[],"sources":["../../../src/formats/files/transformFormat.ts"],"sourcesContent":["import type { FileFormat } from '../../types/data.js';\nimport type {\n FilesOptions,\n Settings,\n SupportedFileExtension,\n} from '../../types/index.js';\nimport { isSupportedFileFormatTransform } from 'generaltranslation/internal';\n\n/**\n * Maps CLI config file keys to API file format enum values.\n */\nexport const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {\n json: 'JSON',\n pot: 'POT',\n mdx: 'MDX',\n md: 'MD',\n ts: 'TS',\n js: 'JS',\n yaml: 'YAML',\n html: 'HTML',\n txt: 'TXT',\n twilioContentJson: 'TWILIO_CONTENT_JSON',\n lottie: 'LOTTIE',\n dotStrings: 'DOT_STRINGS',\n dotStringsdict: 'DOT_STRINGSDICT',\n androidStrings: 'ANDROID_STRINGS',\n} as const satisfies Record<SupportedFileExtension, FileFormat>;\n\n/**\n * Maps uppercase config aliases to the CLI's canonical lowercase file keys.\n */\nexport const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {\n JSON: 'json',\n POT: 'pot',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n YAML: 'yaml',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'twilioContentJson',\n LOTTIE: 'lottie',\n DOT_STRINGS: 'dotStrings',\n DOT_STRINGSDICT: 'dotStringsdict',\n ANDROID_STRINGS: 'androidStrings',\n} as const satisfies Partial<Record<FileFormat, SupportedFileExtension>>;\n\n/**\n * Maps API file format enum values to the extension the CLI should write.\n */\nconst FILE_FORMAT_EXTENSIONS = {\n GTJSON: 'json',\n JSON: 'json',\n PO: 'po',\n POT: 'pot',\n YAML: 'yaml',\n MDX: 'mdx',\n MD: 'md',\n TS: 'ts',\n JS: 'js',\n HTML: 'html',\n TXT: 'txt',\n TWILIO_CONTENT_JSON: 'json',\n LOTTIE: 'lottie',\n SVG: 'svg',\n DOT_STRINGS: 'strings',\n DOT_STRINGSDICT: 'stringsdict',\n ANDROID_STRINGS: 'xml',\n} as const satisfies Record<FileFormat, string>;\n\n/**\n * Converts uppercase file format config keys into the CLI's canonical lowercase keys.\n *\n * This lets users write either `files.POT` or `files.pot` while keeping the\n * rest of the CLI on its existing lowercase file-type convention.\n */\nexport function normalizeFilesOptions(files: FilesOptions): FilesOptions {\n const normalized = { ...files } as FilesOptions & Record<string, unknown>;\n\n for (const [fileFormat, fileType] of Object.entries(\n FILE_FORMAT_TO_CONFIG_FILE_TYPE\n ) as [string, SupportedFileExtension][]) {\n const uppercaseConfig = normalized[fileFormat] as\n | FilesOptions[SupportedFileExtension]\n | undefined;\n if (!normalized[fileType] && uppercaseConfig) {\n normalized[fileType] = uppercaseConfig;\n }\n delete normalized[fileFormat];\n }\n\n return normalized;\n}\n\n/**\n * Validates and resolves a configured output format for a source file type.\n *\n * Throws when the requested source -> output format is not supported by\n * `generaltranslation/internal`.\n */\nexport function resolveTransformationFormat(\n fileType: SupportedFileExtension,\n transformationFormat: string | undefined\n): FileFormat | undefined {\n if (!transformationFormat) return undefined;\n\n // Normalize to uppercase to match the FileFormat enum (e.g. \"po\" -> \"PO\")\n const normalized = transformationFormat.toUpperCase() as FileFormat;\n const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];\n\n if (!isSupportedFileFormatTransform(fileFormat, normalized)) {\n throw new Error(\n `Unsupported file format transform: ${fileFormat} -> ${normalized} in files.${fileType}. ` +\n `\"${normalized}\" is not a valid transformationFormat for ${fileFormat} source files.`\n );\n }\n\n return normalized;\n}\n\n/**\n * Returns the API upload/enqueue property for a file type when one is configured.\n */\nexport function getTransformFormatProperty(\n settings: Settings,\n fileType: SupportedFileExtension\n): { transformFormat?: FileFormat } {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return transformFormat ? { transformFormat } : {};\n}\n\n/**\n * Returns the preferred file extension for a translated file format.\n */\nexport function getFileExtensionForFormat(format: FileFormat): string {\n return FILE_FORMAT_EXTENSIONS[format];\n}\n\n/**\n * Rewrites a path's extension to match the translated file format.\n */\nexport function replaceFileExtensionForFormat(\n filePath: string,\n format: FileFormat\n): string {\n const extension = getFileExtensionForFormat(format);\n return /\\.[^/.]+$/.test(filePath)\n ? filePath.replace(/\\.[^/.]+$/, `.${extension}`)\n : `${filePath}.${extension}`;\n}\n\n/**\n * Detects whether any configured format transform changes the output file type.\n */\nexport function hasNonIdentityFileFormatTransform(settings: Settings): boolean {\n return Object.entries(settings.files?.transformFormats || {}).some(\n ([fileType, transformFormat]) =>\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType as SupportedFileExtension] !==\n transformFormat\n );\n}\n\n/**\n * Returns true when the configured transform for a file type changes its format.\n */\nexport function hasNonIdentityFileFormatTransformForType(\n settings: Settings,\n fileType: SupportedFileExtension\n): boolean {\n const transformFormat = settings.files?.transformFormats?.[fileType];\n return !!(\n transformFormat &&\n CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !== transformFormat\n );\n}\n"],"mappings":";;;;;AAWA,MAAa,kCAAkC;CAC7C,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,KAAK;CACL,mBAAmB;CACnB,QAAQ;CACR,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CACjB;;;;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;CAClB;;;;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;CAClB;;;;;;;AAQD,SAAgB,sBAAsB,OAAmC;CACvE,MAAM,aAAa,EAAE,GAAG,OAAO;AAE/B,MAAK,MAAM,CAAC,YAAY,aAAa,OAAO,QAC1C,gCACD,EAAwC;EACvC,MAAM,kBAAkB,WAAW;AAGnC,MAAI,CAAC,WAAW,aAAa,gBAC3B,YAAW,YAAY;AAEzB,SAAO,WAAW;;AAGpB,QAAO;;;;;;;;AAST,SAAgB,4BACd,UACA,sBACwB;AACxB,KAAI,CAAC,qBAAsB,QAAO,KAAA;CAGlC,MAAM,aAAa,qBAAqB,aAAa;CACrD,MAAM,aAAa,gCAAgC;AAEnD,KAAI,CAAC,+BAA+B,YAAY,WAAW,CACzD,OAAM,IAAI,MACR,sCAAsC,WAAW,MAAM,WAAW,YAAY,SAAS,KACjF,WAAW,4CAA4C,WAAW,gBACzE;AAGH,QAAO;;;;;AAMT,SAAgB,2BACd,UACA,UACkC;CAClC,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,kBAAkB,EAAE,iBAAiB,GAAG,EAAE;;;;;AAMnD,SAAgB,0BAA0B,QAA4B;AACpE,QAAO,uBAAuB;;;;;AAMhC,SAAgB,8BACd,UACA,QACQ;CACR,MAAM,YAAY,0BAA0B,OAAO;AACnD,QAAO,YAAY,KAAK,SAAS,GAC7B,SAAS,QAAQ,aAAa,IAAI,YAAY,GAC9C,GAAG,SAAS,GAAG;;;;;AAMrB,SAAgB,kCAAkC,UAA6B;AAC7E,QAAO,OAAO,QAAQ,SAAS,OAAO,oBAAoB,EAAE,CAAC,CAAC,MAC3D,CAAC,UAAU,qBACV,mBACA,gCAAgC,cAC9B,gBACL;;;;;AAMH,SAAgB,yCACd,UACA,UACS;CACT,MAAM,kBAAkB,SAAS,OAAO,mBAAmB;AAC3D,QAAO,CAAC,EACN,mBACA,gCAAgC,cAAc"}
@@ -3,6 +3,7 @@ import { logErrorAndExit } from "../../console/logging.js";
3
3
  import { SUPPORTED_FILE_EXTENSIONS } from "../../formats/files/supportedFiles.js";
4
4
  import { BASE_PARSING_FLAGS_DEFAULT, GT_PARSING_FLAGS_DEFAULT } from "../../config/defaults.js";
5
5
  import { resolveTransformationFormat } from "../../formats/files/transformFormat.js";
6
+ import { localeForFilePath } from "../../formats/files/localePath.js";
6
7
  import chalk from "chalk";
7
8
  import path from "node:path";
8
9
  import fg from "fast-glob";
@@ -18,7 +19,10 @@ import micromatch from "micromatch";
18
19
  */
19
20
  function resolveLocaleFiles(files, locale) {
20
21
  const result = {};
21
- for (const fileType of SUPPORTED_FILE_EXTENSIONS) result[fileType] = files[fileType]?.map((filepath) => filepath.replace(/\[locale\]/g, locale));
22
+ for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
23
+ const replacement = localeForFilePath(fileType, locale);
24
+ result[fileType] = files[fileType]?.map((filepath) => filepath.replace(/\[locale\]/g, replacement));
25
+ }
22
26
  result.gt = files.gt?.replace(/\[locale\]/g, locale);
23
27
  return result;
24
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parseFilesConfig.js","names":[],"sources":["../../../src/fs/config/parseFilesConfig.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n FilesOptions,\n IncludePattern,\n RequiresReviewConfig,\n ResolvedFiles,\n Settings,\n TransformFormats,\n TransformFiles,\n TransformOption,\n} from '../../types/index.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport fg from 'fast-glob';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { logger } from '../../console/logger.js';\nimport chalk from 'chalk';\nimport micromatch from 'micromatch';\nimport { ParseFlagsByFileType } from '../../types/parsing.js';\nimport {\n BASE_PARSING_FLAGS_DEFAULT,\n GT_PARSING_FLAGS_DEFAULT,\n} from '../../config/defaults.js';\nimport { resolveTransformationFormat } from '../../formats/files/transformFormat.js';\n\n/**\n * Resolves the files from the files object\n * Replaces [locale] with the actual locale in the files\n *\n * @param files - The files object\n * @param locale - The locale to replace [locale] with\n * @returns The resolved files\n */\nexport function resolveLocaleFiles(\n files: ResolvedFiles,\n locale: string\n): ResolvedFiles {\n const result: ResolvedFiles = {};\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n result[fileType] = files[fileType]?.map((filepath) =>\n filepath.replace(/\\[locale\\]/g, locale)\n );\n }\n\n // Replace [locale] with locale in all paths\n result.gt = files.gt?.replace(/\\[locale\\]/g, locale);\n\n return result;\n}\n/**\n * Normalizes include patterns into plain path strings and tracks which\n * patterns have explicit publish flags.\n */\nexport function normalizeIncludePatterns(patterns: IncludePattern[]): {\n paths: string[];\n publishPatterns: string[];\n unpublishPatterns: string[];\n} {\n const paths: string[] = [];\n const publishPatterns: string[] = [];\n const unpublishPatterns: string[] = [];\n\n for (const pattern of patterns) {\n if (typeof pattern === 'string') {\n paths.push(pattern);\n } else {\n paths.push(pattern.pattern);\n if (pattern.publish === true) {\n publishPatterns.push(pattern.pattern);\n } else if (pattern.publish === false) {\n unpublishPatterns.push(pattern.pattern);\n }\n }\n }\n\n return { paths, publishPatterns, unpublishPatterns };\n}\n\n/**\n * Resolves the files from the files object.\n * Performs glob pattern expansion on the files.\n * Replaces [locale] with the actual locale in the files.\n *\n * @param files - The files object\n * @returns The resolved files\n */\nexport function resolveFiles(\n files: FilesOptions,\n locale: string,\n locales: string[],\n cwd: string,\n compositePatterns?: string[],\n requiresReviewDefault: boolean = false\n): Settings['files'] {\n // Initialize result object with empty arrays for each file type\n const resolvedPaths: ResolvedFiles = {};\n const placeholderResult: ResolvedFiles = {};\n const transformPaths: TransformFiles = {};\n // Output format transforms are tracked separately from path transforms.\n const transformFormats: TransformFormats = {};\n const publishPaths = new Set<string>();\n const unpublishPaths = new Set<string>();\n const requiresReviewPaths = new Set<string>();\n const parsingFlags: ParseFlagsByFileType = {};\n\n // Process GT files\n if (files.gt?.output) {\n placeholderResult.gt = path.resolve(cwd, files.gt.output);\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n // ==== TRANSFORMS ==== //\n const transform = files[fileType]?.transform;\n if (\n transform &&\n (typeof transform === 'string' ||\n typeof transform === 'object' ||\n Array.isArray(transform))\n ) {\n transformPaths[fileType] = transform;\n }\n // Validate source -> output format transforms during settings generation.\n const transformFormat = resolveTransformationFormat(\n fileType,\n files[fileType]?.transformationFormat\n );\n if (transformFormat) {\n transformFormats[fileType] = transformFormat;\n }\n // ==== PLACEHOLDERS ==== //\n if (files[fileType]?.include) {\n const { paths, publishPatterns, unpublishPatterns } =\n normalizeIncludePatterns(files[fileType].include);\n\n const filePaths = expandGlobPatterns(\n cwd,\n paths,\n files[fileType]?.exclude || [],\n locale,\n locales,\n transformPaths[fileType] || undefined,\n compositePatterns\n );\n resolvedPaths[fileType] = filePaths.resolvedPaths;\n placeholderResult[fileType] = filePaths.placeholderPaths;\n\n // Classify resolved paths into publish/unpublish sets\n classifyPublishPaths(\n filePaths.resolvedPaths,\n publishPatterns,\n unpublishPatterns,\n cwd,\n locale,\n publishPaths,\n unpublishPaths\n );\n\n // Classify resolved paths by effective requiresReview policy\n classifyRequiresReviewPaths(\n filePaths.resolvedPaths,\n validateRequiresReviewConfig(files[fileType]?.requiresReview, fileType),\n requiresReviewDefault,\n cwd,\n locale,\n requiresReviewPaths\n );\n }\n // ==== OTHER ==== //\n if (files[fileType]?.parsingFlags) {\n parsingFlags[fileType] = {\n ...BASE_PARSING_FLAGS_DEFAULT,\n ...files[fileType].parsingFlags,\n };\n }\n }\n\n return {\n resolvedPaths,\n placeholderPaths: placeholderResult,\n transformPaths: transformPaths,\n transformFormats,\n publishPaths,\n unpublishPaths,\n requiresReviewPaths,\n parsingFlags,\n gtJson: (() => {\n const rawGtFlags = (files.gt?.parsingFlags || {}) as Record<\n string,\n unknown\n >;\n return {\n publish: files.gt?.publish,\n parsingFlags: {\n ...GT_PARSING_FLAGS_DEFAULT,\n ...rawGtFlags,\n },\n };\n })(),\n };\n}\n\n// Helper function to expand glob patterns\nexport function expandGlobPatterns(\n cwd: string,\n includePatterns: string[],\n excludePatterns: string[],\n locale: string,\n locales: string[],\n transformPatterns?: TransformOption | string | TransformOption[],\n compositePatterns?: string[]\n): {\n resolvedPaths: string[];\n placeholderPaths: string[];\n} {\n // Expand glob patterns to include all matching files\n const resolvedPaths: string[] = [];\n const placeholderPaths: string[] = [];\n\n // Process include patterns\n for (const pattern of includePatterns) {\n // Track positions where [locale] appears in the original pattern\n // It must be included in the pattern, otherwise the CLI tool will not be able to find the correct output path\n // Warn if it's not included\n // Ignore if is composite pattern\n if (\n !pattern.includes('[locale]') &&\n !transformPatterns &&\n !compositePatterns?.includes(pattern)\n ) {\n logger.warn(\n chalk.yellow(\n `Pattern \"${pattern}\" does not include [locale], so the CLI tool may incorrectly save translated files.`\n )\n );\n }\n const localePositions: number[] = [];\n let searchIndex = 0;\n const localeTag = '[locale]';\n\n while (true) {\n const foundIndex = pattern.indexOf(localeTag, searchIndex);\n if (foundIndex === -1) break;\n localePositions.push(foundIndex);\n searchIndex = foundIndex + localeTag.length;\n }\n\n const expandedPattern = pattern.replace(/\\[locale\\]/g, locale);\n\n // Resolve the absolute pattern path\n const absolutePattern = path.resolve(cwd, expandedPattern);\n\n // Prepare exclude patterns with locale replaced\n const expandedExcludePatterns = Array.from(\n new Set(\n excludePatterns.flatMap((p) =>\n locales.map((targetLocale) =>\n path.resolve(\n cwd,\n p\n .replace(/\\[locale\\]/g, locale)\n .replace(/\\[locales\\]/g, targetLocale)\n )\n )\n )\n )\n );\n\n // Use fast-glob to find all matching files, excluding the patterns\n const matches = fg.sync(absolutePattern, {\n absolute: true,\n ignore: expandedExcludePatterns,\n });\n\n resolvedPaths.push(...matches);\n\n // For each match, create a version with [locale] in the correct positions\n matches.forEach((match) => {\n const absolutePath = path.resolve(cwd, match);\n const patternPath = path.resolve(cwd, pattern);\n let originalAbsolutePath = absolutePath;\n\n if (localePositions.length > 0) {\n const placeholderPath = buildPlaceholderPathFromPattern(\n patternPath,\n absolutePath,\n localeTag\n );\n originalAbsolutePath = placeholderPath;\n }\n\n placeholderPaths.push(originalAbsolutePath);\n });\n }\n\n return { resolvedPaths, placeholderPaths };\n}\n\nfunction buildPlaceholderPathFromPattern(\n patternPath: string,\n absolutePath: string,\n localeTag: string\n): string {\n if (!patternPath.includes(localeTag)) {\n return absolutePath;\n }\n\n const posixPattern = toPosixPath(patternPath);\n const posixPath = toPosixPath(absolutePath);\n\n const baseRegex = micromatch.makeRe(posixPattern, {\n literalBrackets: true,\n });\n const localeRegexSource = baseRegex.source.replace(\n /\\\\\\[locale\\\\\\]/g,\n '([^/]+)'\n );\n const flags = baseRegex.flags.includes('d')\n ? baseRegex.flags\n : `${baseRegex.flags}d`;\n const matcher = new RegExp(localeRegexSource, flags);\n const match = matcher.exec(posixPath);\n\n const matchWithIndices = match as RegExpExecArray & {\n indices?: Array<[number, number]>;\n };\n\n if (!match || !matchWithIndices.indices) {\n return absolutePath;\n }\n\n let placeholderPosixPath = posixPath;\n const indices = matchWithIndices.indices;\n\n for (let i = indices.length - 1; i >= 1; i--) {\n const [start, end] = indices[i];\n if (start === -1 || end === -1) continue;\n placeholderPosixPath =\n placeholderPosixPath.slice(0, start) +\n localeTag +\n placeholderPosixPath.slice(end);\n }\n\n return path.normalize(placeholderPosixPath);\n}\n\nfunction toPosixPath(value: string): string {\n return value.split(path.sep).join(path.posix.sep);\n}\n\n/**\n * Classifies resolved file paths into publish/unpublish sets by matching\n * them against the given glob patterns. Uses POSIX paths for micromatch\n * compatibility but stores platform-native paths in the output sets.\n */\nfunction classifyPublishPaths(\n resolvedPaths: string[],\n publishPatterns: string[],\n unpublishPatterns: string[],\n cwd: string,\n locale: string,\n publishPaths: Set<string>,\n unpublishPaths: Set<string>\n): void {\n if (publishPatterns.length === 0 && unpublishPatterns.length === 0) return;\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n\n for (const pattern of publishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n publishPaths.add(resolvedPaths[i]);\n }\n }\n }\n\n for (const pattern of unpublishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n unpublishPaths.add(resolvedPaths[i]);\n }\n }\n }\n}\n\n/**\n * Validates a file-type requiresReview config value. Only a boolean or an\n * object of include/exclude glob string arrays is accepted — notably not\n * string \"true\"/\"false\", so a misquoted boolean fails loudly instead of\n * silently changing review policy (and with it, version identity).\n */\nfunction validateRequiresReviewConfig(\n config: unknown,\n fileType: string\n): RequiresReviewConfig | undefined {\n if (config === undefined || typeof config === 'boolean') {\n return config;\n }\n const isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((item) => typeof item === 'string');\n if (\n config !== null &&\n typeof config === 'object' &&\n !Array.isArray(config) &&\n Object.keys(config).every((key) => key === 'include' || key === 'exclude')\n ) {\n const { include, exclude } = config as Record<string, unknown>;\n if (\n (include === undefined || isStringArray(include)) &&\n (exclude === undefined || isStringArray(exclude))\n ) {\n return config as RequiresReviewConfig;\n }\n }\n return logErrorAndExit(\n `files.${fileType}.requiresReview must be a boolean or an object of glob string arrays: { include?: string[], exclude?: string[] }`\n );\n}\n\n/**\n * Classifies resolved file paths by effective requiresReview policy and adds\n * paths whose policy is true to requiresReviewPaths. Precedence: file-type\n * include/exclude globs (exclude wins) > file-type boolean > top-level default.\n */\nfunction classifyRequiresReviewPaths(\n resolvedPaths: string[],\n config: RequiresReviewConfig | undefined,\n requiresReviewDefault: boolean,\n cwd: string,\n locale: string,\n requiresReviewPaths: Set<string>\n): void {\n if (typeof config === 'boolean' || config === undefined) {\n if (config ?? requiresReviewDefault) {\n for (const resolvedPath of resolvedPaths) {\n requiresReviewPaths.add(resolvedPath);\n }\n }\n return;\n }\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n const matchAny = (patterns: string[]) => {\n const matched = new Set<string>();\n for (const pattern of patterns) {\n for (const match of micromatch(posixPaths, toAbsoluteGlob(pattern))) {\n matched.add(match);\n }\n }\n return matched;\n };\n\n const included = matchAny(config.include ?? []);\n const excluded = matchAny(config.exclude ?? []);\n\n for (let i = 0; i < posixPaths.length; i++) {\n const requiresReview = excluded.has(posixPaths[i])\n ? false\n : included.has(posixPaths[i]) || requiresReviewDefault;\n if (requiresReview) {\n requiresReviewPaths.add(resolvedPaths[i]);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parseFilesConfig.js","names":[],"sources":["../../../src/fs/config/parseFilesConfig.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n FilesOptions,\n IncludePattern,\n RequiresReviewConfig,\n ResolvedFiles,\n Settings,\n TransformFormats,\n TransformFiles,\n TransformOption,\n} from '../../types/index.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport fg from 'fast-glob';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { logger } from '../../console/logger.js';\nimport chalk from 'chalk';\nimport micromatch from 'micromatch';\nimport { ParseFlagsByFileType } from '../../types/parsing.js';\nimport {\n BASE_PARSING_FLAGS_DEFAULT,\n GT_PARSING_FLAGS_DEFAULT,\n} from '../../config/defaults.js';\nimport { resolveTransformationFormat } from '../../formats/files/transformFormat.js';\nimport { localeForFilePath } from '../../formats/files/localePath.js';\n\n/**\n * Resolves the files from the files object\n * Replaces [locale] with the actual locale in the files\n *\n * @param files - The files object\n * @param locale - The locale to replace [locale] with\n * @returns The resolved files\n */\nexport function resolveLocaleFiles(\n files: ResolvedFiles,\n locale: string\n): ResolvedFiles {\n const result: ResolvedFiles = {};\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n const replacement = localeForFilePath(fileType, locale);\n result[fileType] = files[fileType]?.map((filepath) =>\n filepath.replace(/\\[locale\\]/g, replacement)\n );\n }\n\n // Replace [locale] with locale in all paths\n result.gt = files.gt?.replace(/\\[locale\\]/g, locale);\n\n return result;\n}\n/**\n * Normalizes include patterns into plain path strings and tracks which\n * patterns have explicit publish flags.\n */\nexport function normalizeIncludePatterns(patterns: IncludePattern[]): {\n paths: string[];\n publishPatterns: string[];\n unpublishPatterns: string[];\n} {\n const paths: string[] = [];\n const publishPatterns: string[] = [];\n const unpublishPatterns: string[] = [];\n\n for (const pattern of patterns) {\n if (typeof pattern === 'string') {\n paths.push(pattern);\n } else {\n paths.push(pattern.pattern);\n if (pattern.publish === true) {\n publishPatterns.push(pattern.pattern);\n } else if (pattern.publish === false) {\n unpublishPatterns.push(pattern.pattern);\n }\n }\n }\n\n return { paths, publishPatterns, unpublishPatterns };\n}\n\n/**\n * Resolves the files from the files object.\n * Performs glob pattern expansion on the files.\n * Replaces [locale] with the actual locale in the files.\n *\n * @param files - The files object\n * @returns The resolved files\n */\nexport function resolveFiles(\n files: FilesOptions,\n locale: string,\n locales: string[],\n cwd: string,\n compositePatterns?: string[],\n requiresReviewDefault: boolean = false\n): Settings['files'] {\n // Initialize result object with empty arrays for each file type\n const resolvedPaths: ResolvedFiles = {};\n const placeholderResult: ResolvedFiles = {};\n const transformPaths: TransformFiles = {};\n // Output format transforms are tracked separately from path transforms.\n const transformFormats: TransformFormats = {};\n const publishPaths = new Set<string>();\n const unpublishPaths = new Set<string>();\n const requiresReviewPaths = new Set<string>();\n const parsingFlags: ParseFlagsByFileType = {};\n\n // Process GT files\n if (files.gt?.output) {\n placeholderResult.gt = path.resolve(cwd, files.gt.output);\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n // ==== TRANSFORMS ==== //\n const transform = files[fileType]?.transform;\n if (\n transform &&\n (typeof transform === 'string' ||\n typeof transform === 'object' ||\n Array.isArray(transform))\n ) {\n transformPaths[fileType] = transform;\n }\n // Validate source -> output format transforms during settings generation.\n const transformFormat = resolveTransformationFormat(\n fileType,\n files[fileType]?.transformationFormat\n );\n if (transformFormat) {\n transformFormats[fileType] = transformFormat;\n }\n // ==== PLACEHOLDERS ==== //\n if (files[fileType]?.include) {\n const { paths, publishPatterns, unpublishPatterns } =\n normalizeIncludePatterns(files[fileType].include);\n\n const filePaths = expandGlobPatterns(\n cwd,\n paths,\n files[fileType]?.exclude || [],\n locale,\n locales,\n transformPaths[fileType] || undefined,\n compositePatterns\n );\n resolvedPaths[fileType] = filePaths.resolvedPaths;\n placeholderResult[fileType] = filePaths.placeholderPaths;\n\n // Classify resolved paths into publish/unpublish sets\n classifyPublishPaths(\n filePaths.resolvedPaths,\n publishPatterns,\n unpublishPatterns,\n cwd,\n locale,\n publishPaths,\n unpublishPaths\n );\n\n // Classify resolved paths by effective requiresReview policy\n classifyRequiresReviewPaths(\n filePaths.resolvedPaths,\n validateRequiresReviewConfig(files[fileType]?.requiresReview, fileType),\n requiresReviewDefault,\n cwd,\n locale,\n requiresReviewPaths\n );\n }\n // ==== OTHER ==== //\n if (files[fileType]?.parsingFlags) {\n parsingFlags[fileType] = {\n ...BASE_PARSING_FLAGS_DEFAULT,\n ...files[fileType].parsingFlags,\n };\n }\n }\n\n return {\n resolvedPaths,\n placeholderPaths: placeholderResult,\n transformPaths: transformPaths,\n transformFormats,\n publishPaths,\n unpublishPaths,\n requiresReviewPaths,\n parsingFlags,\n gtJson: (() => {\n const rawGtFlags = (files.gt?.parsingFlags || {}) as Record<\n string,\n unknown\n >;\n return {\n publish: files.gt?.publish,\n parsingFlags: {\n ...GT_PARSING_FLAGS_DEFAULT,\n ...rawGtFlags,\n },\n };\n })(),\n };\n}\n\n// Helper function to expand glob patterns\nexport function expandGlobPatterns(\n cwd: string,\n includePatterns: string[],\n excludePatterns: string[],\n locale: string,\n locales: string[],\n transformPatterns?: TransformOption | string | TransformOption[],\n compositePatterns?: string[]\n): {\n resolvedPaths: string[];\n placeholderPaths: string[];\n} {\n // Expand glob patterns to include all matching files\n const resolvedPaths: string[] = [];\n const placeholderPaths: string[] = [];\n\n // Process include patterns\n for (const pattern of includePatterns) {\n // Track positions where [locale] appears in the original pattern\n // It must be included in the pattern, otherwise the CLI tool will not be able to find the correct output path\n // Warn if it's not included\n // Ignore if is composite pattern\n if (\n !pattern.includes('[locale]') &&\n !transformPatterns &&\n !compositePatterns?.includes(pattern)\n ) {\n logger.warn(\n chalk.yellow(\n `Pattern \"${pattern}\" does not include [locale], so the CLI tool may incorrectly save translated files.`\n )\n );\n }\n const localePositions: number[] = [];\n let searchIndex = 0;\n const localeTag = '[locale]';\n\n while (true) {\n const foundIndex = pattern.indexOf(localeTag, searchIndex);\n if (foundIndex === -1) break;\n localePositions.push(foundIndex);\n searchIndex = foundIndex + localeTag.length;\n }\n\n const expandedPattern = pattern.replace(/\\[locale\\]/g, locale);\n\n // Resolve the absolute pattern path\n const absolutePattern = path.resolve(cwd, expandedPattern);\n\n // Prepare exclude patterns with locale replaced\n const expandedExcludePatterns = Array.from(\n new Set(\n excludePatterns.flatMap((p) =>\n locales.map((targetLocale) =>\n path.resolve(\n cwd,\n p\n .replace(/\\[locale\\]/g, locale)\n .replace(/\\[locales\\]/g, targetLocale)\n )\n )\n )\n )\n );\n\n // Use fast-glob to find all matching files, excluding the patterns\n const matches = fg.sync(absolutePattern, {\n absolute: true,\n ignore: expandedExcludePatterns,\n });\n\n resolvedPaths.push(...matches);\n\n // For each match, create a version with [locale] in the correct positions\n matches.forEach((match) => {\n const absolutePath = path.resolve(cwd, match);\n const patternPath = path.resolve(cwd, pattern);\n let originalAbsolutePath = absolutePath;\n\n if (localePositions.length > 0) {\n const placeholderPath = buildPlaceholderPathFromPattern(\n patternPath,\n absolutePath,\n localeTag\n );\n originalAbsolutePath = placeholderPath;\n }\n\n placeholderPaths.push(originalAbsolutePath);\n });\n }\n\n return { resolvedPaths, placeholderPaths };\n}\n\nfunction buildPlaceholderPathFromPattern(\n patternPath: string,\n absolutePath: string,\n localeTag: string\n): string {\n if (!patternPath.includes(localeTag)) {\n return absolutePath;\n }\n\n const posixPattern = toPosixPath(patternPath);\n const posixPath = toPosixPath(absolutePath);\n\n const baseRegex = micromatch.makeRe(posixPattern, {\n literalBrackets: true,\n });\n const localeRegexSource = baseRegex.source.replace(\n /\\\\\\[locale\\\\\\]/g,\n '([^/]+)'\n );\n const flags = baseRegex.flags.includes('d')\n ? baseRegex.flags\n : `${baseRegex.flags}d`;\n const matcher = new RegExp(localeRegexSource, flags);\n const match = matcher.exec(posixPath);\n\n const matchWithIndices = match as RegExpExecArray & {\n indices?: Array<[number, number]>;\n };\n\n if (!match || !matchWithIndices.indices) {\n return absolutePath;\n }\n\n let placeholderPosixPath = posixPath;\n const indices = matchWithIndices.indices;\n\n for (let i = indices.length - 1; i >= 1; i--) {\n const [start, end] = indices[i];\n if (start === -1 || end === -1) continue;\n placeholderPosixPath =\n placeholderPosixPath.slice(0, start) +\n localeTag +\n placeholderPosixPath.slice(end);\n }\n\n return path.normalize(placeholderPosixPath);\n}\n\nfunction toPosixPath(value: string): string {\n return value.split(path.sep).join(path.posix.sep);\n}\n\n/**\n * Classifies resolved file paths into publish/unpublish sets by matching\n * them against the given glob patterns. Uses POSIX paths for micromatch\n * compatibility but stores platform-native paths in the output sets.\n */\nfunction classifyPublishPaths(\n resolvedPaths: string[],\n publishPatterns: string[],\n unpublishPatterns: string[],\n cwd: string,\n locale: string,\n publishPaths: Set<string>,\n unpublishPaths: Set<string>\n): void {\n if (publishPatterns.length === 0 && unpublishPatterns.length === 0) return;\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n\n for (const pattern of publishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n publishPaths.add(resolvedPaths[i]);\n }\n }\n }\n\n for (const pattern of unpublishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n unpublishPaths.add(resolvedPaths[i]);\n }\n }\n }\n}\n\n/**\n * Validates a file-type requiresReview config value. Only a boolean or an\n * object of include/exclude glob string arrays is accepted — notably not\n * string \"true\"/\"false\", so a misquoted boolean fails loudly instead of\n * silently changing review policy (and with it, version identity).\n */\nfunction validateRequiresReviewConfig(\n config: unknown,\n fileType: string\n): RequiresReviewConfig | undefined {\n if (config === undefined || typeof config === 'boolean') {\n return config;\n }\n const isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((item) => typeof item === 'string');\n if (\n config !== null &&\n typeof config === 'object' &&\n !Array.isArray(config) &&\n Object.keys(config).every((key) => key === 'include' || key === 'exclude')\n ) {\n const { include, exclude } = config as Record<string, unknown>;\n if (\n (include === undefined || isStringArray(include)) &&\n (exclude === undefined || isStringArray(exclude))\n ) {\n return config as RequiresReviewConfig;\n }\n }\n return logErrorAndExit(\n `files.${fileType}.requiresReview must be a boolean or an object of glob string arrays: { include?: string[], exclude?: string[] }`\n );\n}\n\n/**\n * Classifies resolved file paths by effective requiresReview policy and adds\n * paths whose policy is true to requiresReviewPaths. Precedence: file-type\n * include/exclude globs (exclude wins) > file-type boolean > top-level default.\n */\nfunction classifyRequiresReviewPaths(\n resolvedPaths: string[],\n config: RequiresReviewConfig | undefined,\n requiresReviewDefault: boolean,\n cwd: string,\n locale: string,\n requiresReviewPaths: Set<string>\n): void {\n if (typeof config === 'boolean' || config === undefined) {\n if (config ?? requiresReviewDefault) {\n for (const resolvedPath of resolvedPaths) {\n requiresReviewPaths.add(resolvedPath);\n }\n }\n return;\n }\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n const matchAny = (patterns: string[]) => {\n const matched = new Set<string>();\n for (const pattern of patterns) {\n for (const match of micromatch(posixPaths, toAbsoluteGlob(pattern))) {\n matched.add(match);\n }\n }\n return matched;\n };\n\n const included = matchAny(config.include ?? []);\n const excluded = matchAny(config.exclude ?? []);\n\n for (let i = 0; i < posixPaths.length; i++) {\n const requiresReview = excluded.has(posixPaths[i])\n ? false\n : included.has(posixPaths[i]) || requiresReviewDefault;\n if (requiresReview) {\n requiresReviewPaths.add(resolvedPaths[i]);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { type FileFormat } from 'generaltranslation/types';
2
+ /**
3
+ * How file content crosses the disk boundary.
4
+ *
5
+ * This is the only module in the CLI that knows a file may be stored in
6
+ * something other than UTF-8. `readFileContent` is the single ingress point and
7
+ * `encodeFileContent` the single egress point; in between, content is a UTF-8
8
+ * string and nothing else has to reason about encodings.
9
+ */
10
+ /**
11
+ * On-disk byte layouts Foundation accepts for `.strings` and `.stringsdict`.
12
+ *
13
+ * A byte order mark is the only signal. Foundation does not sniff UTF-16 from
14
+ * interleaved NUL bytes, so an unmarked file is UTF-8 even when its bytes are
15
+ * plainly UTF-16 — `plutil` rejects those files, and guessing would translate
16
+ * content Foundation itself cannot read.
17
+ */
18
+ export type FileEncoding = 'utf8' | 'utf8-bom' | 'utf16le' | 'utf16be' | 'utf32le' | 'utf32be';
19
+ /**
20
+ * Ingress. Reads a file as the content the pipeline carries for its format:
21
+ * base64 for binary formats, otherwise UTF-8 text decoded from whatever
22
+ * encoding the file happens to be stored in.
23
+ *
24
+ * A file that cannot be read throws rather than reading as empty, which the
25
+ * pipeline cannot tell apart from a file that is genuinely empty and would
26
+ * upload as a deletion of every string in it.
27
+ * @param {string} filePath - The path to the file to read.
28
+ * @param {FileFormat} fileFormat - The format the file is configured as.
29
+ * @returns {string} - The content.
30
+ */
31
+ export declare function readFileContent(filePath: string, fileFormat: FileFormat): string;
32
+ /**
33
+ * Egress. Renders translated content for disk in the encoding `sourcePath` is
34
+ * stored in, so a repository of UTF-16 files stays UTF-16 rather than showing
35
+ * every one of them as rewritten.
36
+ *
37
+ * The source file is the record of that encoding, which is why it is re-read
38
+ * here rather than carried from ingress: the download that writes this file is
39
+ * usually a different process run from the upload that read it, so there is no
40
+ * value to carry. Nothing has to be persisted in the lockfile or on the server.
41
+ *
42
+ * A source moved or deleted since it was uploaded leaves the translation
43
+ * already on disk as the only remaining record, so `outputPath` is read next.
44
+ * Defaulting straight to UTF-8 would rewrite every UTF-16 file and, for
45
+ * `.stringsdict`, strand a UTF-16 XML declaration over unmarked UTF-8 bytes —
46
+ * the one combination Foundation rejects.
47
+ * @param {string} content - The translated content, as UTF-8 text.
48
+ * @param {FileFormat} fileFormat - The format being written.
49
+ * @param {string} sourcePath - The source file this translation came from.
50
+ * @param {string} [outputPath] - Where the translation is about to be written.
51
+ * @returns The value to hand to `writeFile`.
52
+ */
53
+ export declare function encodeFileContent(content: string, fileFormat: FileFormat | undefined, sourcePath: string, outputPath?: string): string | Buffer;
54
+ /** Reads the byte order mark, defaulting to UTF-8 when there is none. */
55
+ export declare function detectFileEncoding(bytes: Buffer): FileEncoding;
56
+ /**
57
+ * Decodes a `.strings` or `.stringsdict` file to text, dropping the byte order
58
+ * mark. The encoding is returned so the writer can restore both on the way out.
59
+ */
60
+ export declare function decodeFileText(bytes: Buffer): {
61
+ text: string;
62
+ encoding: FileEncoding;
63
+ };
64
+ /** Re-encodes text into the given layout, byte order mark included. */
65
+ export declare function encodeFileText(text: string, encoding: FileEncoding): Buffer;
@@ -0,0 +1,171 @@
1
+ import fs from "node:fs";
2
+ import { isBinaryFileFormat } from "generaltranslation/types";
3
+ //#region src/fs/fileContent.ts
4
+ /**
5
+ * Formats whose bytes on disk may carry an encoding other than UTF-8, so reads
6
+ * decode by byte order mark and writes restore it. Older Xcode wrote both of
7
+ * Apple's localization formats as UTF-16.
8
+ */
9
+ const BYTE_ORDER_MARK_FORMATS = new Set(["DOT_STRINGS", "DOT_STRINGSDICT"]);
10
+ /**
11
+ * Ingress. Reads a file as the content the pipeline carries for its format:
12
+ * base64 for binary formats, otherwise UTF-8 text decoded from whatever
13
+ * encoding the file happens to be stored in.
14
+ *
15
+ * A file that cannot be read throws rather than reading as empty, which the
16
+ * pipeline cannot tell apart from a file that is genuinely empty and would
17
+ * upload as a deletion of every string in it.
18
+ * @param {string} filePath - The path to the file to read.
19
+ * @param {FileFormat} fileFormat - The format the file is configured as.
20
+ * @returns {string} - The content.
21
+ */
22
+ function readFileContent(filePath, fileFormat) {
23
+ if (isBinaryFileFormat(fileFormat)) return fs.readFileSync(filePath).toString("base64");
24
+ if (BYTE_ORDER_MARK_FORMATS.has(fileFormat)) return decodeFileText(fs.readFileSync(filePath)).text;
25
+ return fs.readFileSync(filePath, "utf8");
26
+ }
27
+ /**
28
+ * Egress. Renders translated content for disk in the encoding `sourcePath` is
29
+ * stored in, so a repository of UTF-16 files stays UTF-16 rather than showing
30
+ * every one of them as rewritten.
31
+ *
32
+ * The source file is the record of that encoding, which is why it is re-read
33
+ * here rather than carried from ingress: the download that writes this file is
34
+ * usually a different process run from the upload that read it, so there is no
35
+ * value to carry. Nothing has to be persisted in the lockfile or on the server.
36
+ *
37
+ * A source moved or deleted since it was uploaded leaves the translation
38
+ * already on disk as the only remaining record, so `outputPath` is read next.
39
+ * Defaulting straight to UTF-8 would rewrite every UTF-16 file and, for
40
+ * `.stringsdict`, strand a UTF-16 XML declaration over unmarked UTF-8 bytes —
41
+ * the one combination Foundation rejects.
42
+ * @param {string} content - The translated content, as UTF-8 text.
43
+ * @param {FileFormat} fileFormat - The format being written.
44
+ * @param {string} sourcePath - The source file this translation came from.
45
+ * @param {string} [outputPath] - Where the translation is about to be written.
46
+ * @returns The value to hand to `writeFile`.
47
+ */
48
+ function encodeFileContent(content, fileFormat, sourcePath, outputPath) {
49
+ if (!fileFormat || !BYTE_ORDER_MARK_FORMATS.has(fileFormat)) return content;
50
+ const encoding = readFileEncoding(sourcePath) ?? (outputPath ? readFileEncoding(outputPath) : void 0) ?? "utf8";
51
+ return encoding === "utf8" ? content : encodeFileText(content, encoding);
52
+ }
53
+ /**
54
+ * Reads a file's byte order mark without reading the rest of it. Undefined
55
+ * separates "no such file" from a file that is genuinely unmarked UTF-8.
56
+ */
57
+ function readFileEncoding(filePath) {
58
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return;
59
+ const handle = fs.openSync(filePath, "r");
60
+ try {
61
+ const mark = Buffer.alloc(LONGEST_BYTE_ORDER_MARK);
62
+ const read = fs.readSync(handle, mark, 0, mark.length, 0);
63
+ return detectFileEncoding(mark.subarray(0, read));
64
+ } finally {
65
+ fs.closeSync(handle);
66
+ }
67
+ }
68
+ const BYTE_ORDER_MARKS = [
69
+ ["utf32le", [
70
+ 255,
71
+ 254,
72
+ 0,
73
+ 0
74
+ ]],
75
+ ["utf32be", [
76
+ 0,
77
+ 0,
78
+ 254,
79
+ 255
80
+ ]],
81
+ ["utf16le", [255, 254]],
82
+ ["utf16be", [254, 255]],
83
+ ["utf8-bom", [
84
+ 239,
85
+ 187,
86
+ 191
87
+ ]]
88
+ ];
89
+ const BYTE_ORDER_MARK_BY_ENCODING = new Map(BYTE_ORDER_MARKS.map(([encoding, mark]) => [encoding, Buffer.from(mark)]));
90
+ /** Bytes that must be in hand before a byte order mark can be ruled out. */
91
+ const LONGEST_BYTE_ORDER_MARK = Math.max(...BYTE_ORDER_MARKS.map(([, mark]) => mark.length));
92
+ /** Reads the byte order mark, defaulting to UTF-8 when there is none. */
93
+ function detectFileEncoding(bytes) {
94
+ for (const [encoding, mark] of BYTE_ORDER_MARKS) if (bytes.length >= mark.length && mark.every((b, i) => bytes[i] === b)) return encoding;
95
+ return "utf8";
96
+ }
97
+ /**
98
+ * Decodes a `.strings` or `.stringsdict` file to text, dropping the byte order
99
+ * mark. The encoding is returned so the writer can restore both on the way out.
100
+ */
101
+ function decodeFileText(bytes) {
102
+ const encoding = detectFileEncoding(bytes);
103
+ const body = bytes.subarray(BYTE_ORDER_MARK_BY_ENCODING.get(encoding)?.length ?? 0);
104
+ switch (encoding) {
105
+ case "utf16le": return {
106
+ text: decodeUtf16(body, true),
107
+ encoding
108
+ };
109
+ case "utf16be": return {
110
+ text: decodeUtf16(body, false),
111
+ encoding
112
+ };
113
+ case "utf32le": return {
114
+ text: decodeUtf32(body, true),
115
+ encoding
116
+ };
117
+ case "utf32be": return {
118
+ text: decodeUtf32(body, false),
119
+ encoding
120
+ };
121
+ default: return {
122
+ text: body.toString("utf8"),
123
+ encoding
124
+ };
125
+ }
126
+ }
127
+ /** Re-encodes text into the given layout, byte order mark included. */
128
+ function encodeFileText(text, encoding) {
129
+ const mark = BYTE_ORDER_MARK_BY_ENCODING.get(encoding);
130
+ const body = (() => {
131
+ switch (encoding) {
132
+ case "utf16le": return Buffer.from(text, "utf16le");
133
+ case "utf16be": return Buffer.from(text, "utf16le").swap16();
134
+ case "utf32le": return encodeUtf32(text, true);
135
+ case "utf32be": return encodeUtf32(text, false);
136
+ default: return Buffer.from(text, "utf8");
137
+ }
138
+ })();
139
+ return mark ? Buffer.concat([mark, body]) : body;
140
+ }
141
+ function decodeUtf16(body, littleEndian) {
142
+ if (body.length % 2 !== 0) throw new Error("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");
143
+ if (littleEndian) return body.toString("utf16le");
144
+ return Buffer.from(body).swap16().toString("utf16le");
145
+ }
146
+ const CODE_POINTS_PER_CALL = 4096;
147
+ function decodeUtf32(body, littleEndian) {
148
+ if (body.length % 4 !== 0) throw new Error("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");
149
+ const codePoints = Array.from({ length: body.length / 4 }, (_, i) => littleEndian ? body.readUInt32LE(i * 4) : body.readUInt32BE(i * 4));
150
+ let text = "";
151
+ for (let i = 0; i < codePoints.length; i += CODE_POINTS_PER_CALL) try {
152
+ text += String.fromCodePoint(...codePoints.slice(i, i + CODE_POINTS_PER_CALL));
153
+ } catch {
154
+ throw new Error("it has a UTF-32 byte order mark but contains a value that is not a Unicode code point");
155
+ }
156
+ return text;
157
+ }
158
+ function encodeUtf32(text, littleEndian) {
159
+ const characters = Array.from(text);
160
+ const body = Buffer.alloc(characters.length * 4);
161
+ characters.forEach((character, index) => {
162
+ const codePoint = character.codePointAt(0) ?? 0;
163
+ if (littleEndian) body.writeUInt32LE(codePoint, index * 4);
164
+ else body.writeUInt32BE(codePoint, index * 4);
165
+ });
166
+ return body;
167
+ }
168
+ //#endregion
169
+ export { decodeFileText, detectFileEncoding, encodeFileContent, encodeFileText, readFileContent };
170
+
171
+ //# sourceMappingURL=fileContent.js.map
@@ -0,0 +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 +1 @@
1
- export declare const PACKAGE_VERSION = "2.18.1";
1
+ export declare const PACKAGE_VERSION = "2.20.0";
@@ -1,5 +1,5 @@
1
1
  //#region src/generated/version.ts
2
- const PACKAGE_VERSION = "2.18.1";
2
+ const PACKAGE_VERSION = "2.20.0";
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.18.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.20.0';\n"],"mappings":";AACA,MAAa,kBAAkB"}
@@ -1,8 +1,10 @@
1
- type DownloadMeta = {
1
+ import type { FileFormat } from 'generaltranslation/types';
2
+ export type DownloadMeta = {
2
3
  branchId: string;
3
4
  fileId: string;
4
5
  versionId: string;
5
6
  locale: string;
7
+ fileFormat: FileFormat;
6
8
  inputPath?: string;
7
9
  };
8
10
  export declare function recordDownloaded(filePath: string, meta?: DownloadMeta): void;
@@ -16,4 +18,3 @@ export declare function getDownloaded(): Set<string>;
16
18
  export declare function getNeedsPostprocessing(): Set<string>;
17
19
  export declare function getDownloadedMeta(): Map<string, DownloadMeta>;
18
20
  export declare function clearDownloaded(): void;
19
- export {};
@@ -1 +1 @@
1
- {"version":3,"file":"recentDownloads.js","names":[],"sources":["../../src/state/recentDownloads.ts"],"sourcesContent":["type DownloadMeta = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n inputPath?: string;\n};\n\nconst recent = new Set<string>();\nconst recentMeta = new Map<string, DownloadMeta>();\nconst remerged = new Set<string>();\n\nexport function recordDownloaded(filePath: string, meta?: DownloadMeta) {\n recent.add(filePath);\n if (meta) {\n recentMeta.set(filePath, meta);\n }\n}\n\n/**\n * Track a file that was re-merged with the source\n * so that postprocessing still runs on it.\n */\nexport function recordRemerged(filePath: string) {\n remerged.add(filePath);\n}\n\nexport function getDownloaded(): Set<string> {\n return recent;\n}\n\n/** Files that need postprocessing: downloaded OR re-merged */\nexport function getNeedsPostprocessing(): Set<string> {\n return new Set([...recent, ...remerged]);\n}\n\nexport function getDownloadedMeta(): Map<string, DownloadMeta> {\n return recentMeta;\n}\n\nexport function clearDownloaded() {\n recent.clear();\n recentMeta.clear();\n remerged.clear();\n}\n"],"mappings":";AAQA,MAAM,yBAAS,IAAI,KAAa;AAChC,MAAM,6BAAa,IAAI,KAA2B;AAClD,MAAM,2BAAW,IAAI,KAAa;AAElC,SAAgB,iBAAiB,UAAkB,MAAqB;AACtE,QAAO,IAAI,SAAS;AACpB,KAAI,KACF,YAAW,IAAI,UAAU,KAAK;;;;;;AAQlC,SAAgB,eAAe,UAAkB;AAC/C,UAAS,IAAI,SAAS;;AAGxB,SAAgB,gBAA6B;AAC3C,QAAO;;;AAIT,SAAgB,yBAAsC;AACpD,QAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;;AAG1C,SAAgB,oBAA+C;AAC7D,QAAO;;AAGT,SAAgB,kBAAkB;AAChC,QAAO,OAAO;AACd,YAAW,OAAO;AAClB,UAAS,OAAO"}
1
+ {"version":3,"file":"recentDownloads.js","names":[],"sources":["../../src/state/recentDownloads.ts"],"sourcesContent":["import type { FileFormat } from 'generaltranslation/types';\n\nexport type DownloadMeta = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n // The format the file was written as, so it can be read back the same way.\n fileFormat: FileFormat;\n inputPath?: string;\n};\n\nconst recent = new Set<string>();\nconst recentMeta = new Map<string, DownloadMeta>();\nconst remerged = new Set<string>();\n\nexport function recordDownloaded(filePath: string, meta?: DownloadMeta) {\n recent.add(filePath);\n if (meta) {\n recentMeta.set(filePath, meta);\n }\n}\n\n/**\n * Track a file that was re-merged with the source\n * so that postprocessing still runs on it.\n */\nexport function recordRemerged(filePath: string) {\n remerged.add(filePath);\n}\n\nexport function getDownloaded(): Set<string> {\n return recent;\n}\n\n/** Files that need postprocessing: downloaded OR re-merged */\nexport function getNeedsPostprocessing(): Set<string> {\n return new Set([...recent, ...remerged]);\n}\n\nexport function getDownloadedMeta(): Map<string, DownloadMeta> {\n return recentMeta;\n}\n\nexport function clearDownloaded() {\n recent.clear();\n recentMeta.clear();\n remerged.clear();\n}\n"],"mappings":";AAYA,MAAM,yBAAS,IAAI,KAAa;AAChC,MAAM,6BAAa,IAAI,KAA2B;AAClD,MAAM,2BAAW,IAAI,KAAa;AAElC,SAAgB,iBAAiB,UAAkB,MAAqB;AACtE,QAAO,IAAI,SAAS;AACpB,KAAI,KACF,YAAW,IAAI,UAAU,KAAK;;;;;;AAQlC,SAAgB,eAAe,UAAkB;AAC/C,UAAS,IAAI,SAAS;;AAGxB,SAAgB,gBAA6B;AAC3C,QAAO;;;AAIT,SAAgB,yBAAsC;AACpD,QAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;;AAG1C,SAAgB,oBAA+C;AAC7D,QAAO;;AAGT,SAAgB,kBAAkB;AAChC,QAAO,OAAO;AACd,YAAW,OAAO;AAClB,UAAS,OAAO"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../console/logger.js";
2
- import { gt } from "../utils/gt.js";
2
+ import { api } from "../utils/api.js";
3
3
  import chalk from "chalk";
4
4
  import path from "node:path";
5
5
  //#region src/translation/reviewSetupWarning.ts
@@ -20,12 +20,12 @@ const PROJECT_INFO_TIMEOUT_MS = 1e4;
20
20
  async function warnManualReviewSetup(settings, files) {
21
21
  const requiresReviewPaths = settings.files?.requiresReviewPaths;
22
22
  if (!files.some((file) => {
23
- if (file.fileFormat === "GTJSON") return Object.values(file.formatMetadata ?? {}).some((metadata) => metadata?.requires_review === true);
23
+ if (file.fileFormat === "GTJSON") return Object.values(file.formatMetadata ?? {}).some((metadata) => typeof metadata === "object" && metadata !== null && "requires_review" in metadata && metadata.requires_review === true);
24
24
  return requiresReviewPaths?.has(path.resolve(process.cwd(), file.fileName));
25
25
  })) return;
26
26
  let autoApprove;
27
27
  try {
28
- autoApprove = (await gt.getProjectInfo({ timeout: PROJECT_INFO_TIMEOUT_MS })).autoApprove;
28
+ autoApprove = (await api.getProjectInfo(PROJECT_INFO_TIMEOUT_MS)).autoApprove;
29
29
  } catch {}
30
30
  if (autoApprove === false) return;
31
31
  const settingsUrl = settings.projectId ? `${settings.dashboardUrl}/project/${settings.projectId}/settings` : settings.dashboardUrl;