gt 2.14.21 → 2.14.22

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,15 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.14.22
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1248](https://github.com/generaltranslation/gt/pull/1248) [`b12d57d`](https://github.com/generaltranslation/gt/commit/b12d57dab1d5cb1f602c5ac24a702b48cda7f11e) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - Add PO/POT file format support and transformFormat plumbing for API uploads and CLI file downloads.
8
+
9
+ - Updated dependencies [[`b12d57d`](https://github.com/generaltranslation/gt/commit/b12d57dab1d5cb1f602c5ac24a702b48cda7f11e)]:
10
+ - generaltranslation@8.2.8
11
+ - @generaltranslation/python-extractor@0.2.13
12
+
3
13
  ## 2.14.21
4
14
 
5
15
  ### Patch Changes
@@ -27,8 +27,8 @@ const findLatestDownloadedVersion = (entryMap, fileId, locale) => {
27
27
  export async function collectAndSendUserEditDiffs(files, settings) {
28
28
  if (!settings.files)
29
29
  return false;
30
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
31
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
30
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
31
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
32
32
  const { entryMap } = readLockfile(settings);
33
33
  const tempDir = path.join(os.tmpdir(), randomUUID());
34
34
  if (!fs.existsSync(tempDir))
@@ -13,10 +13,16 @@ import { readLockfile, writeLockfile, findOrCreateEntry, } from '../fs/config/do
13
13
  import { recordDownloaded, recordRemerged } from '../state/recentDownloads.js';
14
14
  import { recordWarning } from '../state/translateWarnings.js';
15
15
  import stringify from 'fast-json-stable-stringify';
16
+ import { SUPPORTED_FILE_EXTENSIONS } from '../formats/files/supportedFiles.js';
17
+ import { hasNonIdentityFileFormatTransformForType } from '../formats/files/transformFormat.js';
18
+ import { getRelative } from '../fs/findFilepath.js';
16
19
  /**
17
20
  * Merges translated content with the current source file for schema-based formats.
18
21
  */
19
22
  function mergeWithSource(translatedContent, locale, inputPath, options) {
23
+ if (shouldSkipSourceFormatMerge(inputPath, options)) {
24
+ return translatedContent;
25
+ }
20
26
  if (!options.options)
21
27
  return translatedContent;
22
28
  const jsonSchema = options.options.jsonSchema
@@ -49,6 +55,23 @@ function mergeWithSource(translatedContent, locale, inputPath, options) {
49
55
  return mergeYaml(sourceContent, inputPath, options.options, [{ translatedContent, targetLocale: locale }], options.defaultLocale)[0];
50
56
  }
51
57
  }
58
+ /**
59
+ * Determines whether a source file should be skipped for schema re-merging.
60
+ * @param inputPath - The path of the source file
61
+ * @param options - The settings for the project
62
+ * @returns True if the source file should be skipped for schema re-merging, false otherwise
63
+ */
64
+ function shouldSkipSourceFormatMerge(inputPath, options) {
65
+ for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
66
+ if (!hasNonIdentityFileFormatTransformForType(options, fileType))
67
+ continue;
68
+ const transformedSourcePaths = options.files.resolvedPaths[fileType] || [];
69
+ if (transformedSourcePaths.some((sourcePath) => getRelative(sourcePath) === inputPath)) {
70
+ return true;
71
+ }
72
+ }
73
+ return false;
74
+ }
52
75
  /**
53
76
  * Downloads multiple translation files in a single batch request
54
77
  * @param files - Array of files to download with their output paths
@@ -25,8 +25,8 @@ export async function handleDownload(options, settings, library) {
25
25
  return logErrorAndExit(noFilesError);
26
26
  }
27
27
  // Files
28
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
29
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
28
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
29
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
30
30
  // Collect the hashes for all files we need to download
31
31
  let fileVersionData;
32
32
  if (settings.stageTranslations) {
@@ -10,11 +10,14 @@ import localizeStaticImports from '../../utils/localizeStaticImports.js';
10
10
  import { getDownloadedMeta } from '../../state/recentDownloads.js';
11
11
  import { persistPostProcessHashes } from '../../utils/persistPostprocessHashes.js';
12
12
  import { runPublishWorkflow } from '../../workflows/publish.js';
13
+ import { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';
14
+ import { hasNonIdentityFileFormatTransformForType } from '../../formats/files/transformFormat.js';
15
+ import { getRelative } from '../../fs/findFilepath.js';
13
16
  // Downloads translations that were completed
14
17
  export async function handleTranslate(options, settings, fileVersionData, jobData, branchData, publishMap) {
15
18
  if (fileVersionData) {
16
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
17
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
19
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats, } = settings.files;
20
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
18
21
  // Check for remaining translations
19
22
  await runDownloadWorkflow({
20
23
  fileVersionData: fileVersionData,
@@ -39,21 +42,24 @@ export async function handleTranslate(options, settings, fileVersionData, jobDat
39
42
  }
40
43
  }
41
44
  export async function postProcessTranslations(settings, includeFiles) {
45
+ const postProcessIncludes = filterPostProcessIncludesForFormatTransforms(settings, includeFiles);
46
+ if (includeFiles && postProcessIncludes?.size === 0)
47
+ return;
42
48
  // Mintlify OpenAPI localization (spec routing + validation)
43
- await processOpenApi(settings, includeFiles);
49
+ await processOpenApi(settings, postProcessIncludes);
44
50
  // Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales
45
51
  // Default locale is processed earlier in the flow in base.ts
46
52
  if (settings.options?.experimentalLocalizeStaticUrls) {
47
53
  const nonDefaultLocales = settings.locales.filter((locale) => locale !== settings.defaultLocale);
48
54
  if (nonDefaultLocales.length > 0) {
49
- await localizeStaticUrls(settings, nonDefaultLocales, includeFiles);
55
+ await localizeStaticUrls(settings, nonDefaultLocales, postProcessIncludes);
50
56
  }
51
57
  }
52
58
  // Rewrite relative asset URLs in translated md/mdx files
53
59
  if (settings.options?.experimentalLocalizeRelativeAssets) {
54
60
  const nonDefaultLocales = settings.locales.filter((locale) => locale !== settings.defaultLocale);
55
61
  if (nonDefaultLocales.length > 0) {
56
- await localizeRelativeAssets(settings, nonDefaultLocales, includeFiles);
62
+ await localizeRelativeAssets(settings, nonDefaultLocales, postProcessIncludes);
57
63
  }
58
64
  }
59
65
  const shouldProcessAnchorIds = settings.options?.experimentalLocalizeStaticUrls ||
@@ -61,20 +67,51 @@ export async function postProcessTranslations(settings, includeFiles) {
61
67
  // Add explicit anchor IDs to translated MDX/MD files to preserve navigation
62
68
  // Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'
63
69
  if (shouldProcessAnchorIds) {
64
- await processAnchorIds(settings, includeFiles);
70
+ await processAnchorIds(settings, postProcessIncludes);
65
71
  }
66
72
  // Localize static imports (import Snippet from /snippets/file.mdx -> import Snippet from /snippets/[locale]/file.mdx)
67
73
  if (settings.options?.experimentalLocalizeStaticImports) {
68
- await localizeStaticImports(settings, includeFiles);
74
+ await localizeStaticImports(settings, postProcessIncludes);
69
75
  }
70
76
  // Flatten json files into a single file
71
77
  if (settings.options?.experimentalFlattenJsonFiles) {
72
- await flattenJsonFiles(settings, includeFiles);
78
+ await flattenJsonFiles(settings, postProcessIncludes);
73
79
  }
74
80
  // Copy files to the target locale
75
81
  if (settings.options?.copyFiles) {
76
82
  await copyFile(settings);
77
83
  }
78
84
  // Record postprocessed content hashes for newly downloaded files
79
- persistPostProcessHashes(settings, includeFiles, getDownloadedMeta());
85
+ persistPostProcessHashes(settings, postProcessIncludes, getDownloadedMeta());
86
+ }
87
+ /**
88
+ * Exclude only outputs whose source file was translated into a different format.
89
+ * @param settings - The settings for the project
90
+ * @param includeFiles - The files to include in the post-processing
91
+ * @returns The files to exclude in the post-processing
92
+ */
93
+ function filterPostProcessIncludesForFormatTransforms(settings, includeFiles) {
94
+ if (!includeFiles)
95
+ return includeFiles;
96
+ const transformedSourcePaths = new Set();
97
+ for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
98
+ if (!hasNonIdentityFileFormatTransformForType(settings, fileType))
99
+ continue;
100
+ for (const sourcePath of settings.files.resolvedPaths[fileType] || []) {
101
+ transformedSourcePaths.add(getRelative(sourcePath));
102
+ }
103
+ }
104
+ if (transformedSourcePaths.size === 0)
105
+ return includeFiles;
106
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
107
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
108
+ const transformedOutputPaths = new Set();
109
+ for (const localeMapping of Object.values(fileMapping)) {
110
+ for (const [sourcePath, outputPath] of Object.entries(localeMapping)) {
111
+ if (transformedSourcePaths.has(sourcePath)) {
112
+ transformedOutputPaths.add(outputPath);
113
+ }
114
+ }
115
+ }
116
+ return new Set([...includeFiles].filter((filePath) => !transformedOutputPaths.has(filePath)));
80
117
  }
@@ -16,6 +16,7 @@ import { hashStringSync } from '../../utils/hash.js';
16
16
  import { hasValidCredentials } from './utils/validation.js';
17
17
  import { buildPublishMap } from '../../utils/resolvePublish.js';
18
18
  import { runPublishWorkflow } from '../../workflows/publish.js';
19
+ import { getTransformFormatProperty } from '../../formats/files/transformFormat.js';
19
20
  const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
20
21
  /**
21
22
  * Sends multiple files to the API for translation
@@ -60,6 +61,7 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
60
61
  content: parsedJson,
61
62
  fileName: relativePath,
62
63
  fileFormat: 'JSON',
64
+ ...getTransformFormatProperty(settings, 'json'),
63
65
  dataFormat,
64
66
  locale: settings.defaultLocale,
65
67
  fileId: hashStringSync(relativePath),
@@ -81,6 +83,7 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
81
83
  content: parsedYaml,
82
84
  fileName: relativePath,
83
85
  fileFormat,
86
+ ...getTransformFormatProperty(settings, 'yaml'),
84
87
  dataFormat,
85
88
  locale: settings.defaultLocale,
86
89
  fileId: hashStringSync(relativePath),
@@ -99,6 +102,7 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
99
102
  content: parsedJson,
100
103
  fileName: relativePath,
101
104
  fileFormat: 'TWILIO_CONTENT_JSON',
105
+ ...getTransformFormatProperty(settings, 'twilioContentJson'),
102
106
  dataFormat: 'STRING',
103
107
  locale: settings.defaultLocale,
104
108
  fileId: hashStringSync(relativePath),
@@ -121,6 +125,7 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
121
125
  content: sanitizedContent,
122
126
  fileName: relativePath,
123
127
  fileFormat: fileType.toUpperCase(),
128
+ ...getTransformFormatProperty(settings, fileType),
124
129
  dataFormat,
125
130
  locale: settings.defaultLocale,
126
131
  fileId: hashStringSync(relativePath),
@@ -141,13 +146,14 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
141
146
  return exitSync(1);
142
147
  const locales = settings.locales || [];
143
148
  // Create file mapping for all file types
144
- const fileMapping = createFileMapping(filePaths, placeholderPaths, transformPaths, locales, settings.defaultLocale);
149
+ const fileMapping = createFileMapping(filePaths, placeholderPaths, transformPaths, settings.files?.transformFormats || {}, locales, settings.defaultLocale);
145
150
  // construct object
146
151
  const uploadData = allFiles.map((file) => {
147
152
  const sourceFile = {
148
153
  content: file.content,
149
154
  fileName: file.fileName,
150
155
  fileFormat: file.fileFormat,
156
+ transformFormat: file.transformFormat,
151
157
  dataFormat: file.dataFormat,
152
158
  locale: file.locale,
153
159
  fileId: file.fileId,
@@ -163,7 +169,7 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
163
169
  translations.push({
164
170
  content: extracted,
165
171
  fileName: file.fileName,
166
- fileFormat: file.fileFormat,
172
+ fileFormat: file.transformFormat ?? file.fileFormat,
167
173
  dataFormat: file.dataFormat,
168
174
  locale,
169
175
  fileId: file.fileId,
@@ -178,8 +184,8 @@ export async function upload(filePaths, placeholderPaths, transformPaths, dataFo
178
184
  const translatedContent = readFileSync(translatedFileName, 'utf8');
179
185
  translations.push({
180
186
  content: translatedContent,
181
- fileName: file.fileName,
182
- fileFormat: file.fileFormat,
187
+ fileName: translatedFileName,
188
+ fileFormat: file.transformFormat ?? file.fileFormat,
183
189
  dataFormat: file.dataFormat,
184
190
  locale,
185
191
  fileId: file.fileId,
@@ -13,6 +13,7 @@ import { resolveConfig } from './resolveConfig.js';
13
13
  import { gt } from '../utils/gt.js';
14
14
  import { generatePreset } from './optionPresets.js';
15
15
  import { GT_PARSING_FLAGS_DEFAULT } from './defaults.js';
16
+ import { normalizeFilesOptions } from '../formats/files/transformFormat.js';
16
17
  export const DEFAULT_SRC_PATTERNS = [
17
18
  'src/**/*.{js,jsx,ts,tsx}',
18
19
  'app/**/*.{js,jsx,ts,tsx}',
@@ -147,11 +148,12 @@ export async function generateSettings(flags, cwd = process.cwd(), options) {
147
148
  .filter(([, schema]) => schema.composite)
148
149
  .map(([key]) => key);
149
150
  mergedOptions.files = mergedOptions.files
150
- ? resolveFiles(mergedOptions.files, mergedOptions.defaultLocale, mergedOptions.locales, cwd, compositePatterns)
151
+ ? resolveFiles(normalizeFilesOptions(mergedOptions.files), mergedOptions.defaultLocale, mergedOptions.locales, cwd, compositePatterns)
151
152
  : {
152
153
  resolvedPaths: {},
153
154
  placeholderPaths: {},
154
155
  transformPaths: {},
156
+ transformFormats: {},
155
157
  publishPaths: new Set(),
156
158
  unpublishPaths: new Set(),
157
159
  parsingFlags: {},
@@ -14,6 +14,7 @@ import { hashStringSync } from '../../utils/hash.js';
14
14
  import { preprocessContent } from './preprocessContent.js';
15
15
  import { parseKeyedMetadata, } from '../parseKeyedMetadata.js';
16
16
  import { buildPublishMap } from '../../utils/resolvePublish.js';
17
+ import { getTransformFormatProperty } from './transformFormat.js';
17
18
  /**
18
19
  * Checks if a file path is a metadata companion file (e.g. foo.metadata.json)
19
20
  * AND its corresponding source file (e.g. foo.json) exists in the file list.
@@ -123,6 +124,7 @@ export async function aggregateFiles(settings) {
123
124
  content: parsedJson,
124
125
  fileName: relativePath,
125
126
  fileFormat: 'JSON',
127
+ ...getTransformFormatProperty(settings, 'json'),
126
128
  dataFormat,
127
129
  locale: settings.defaultLocale,
128
130
  ...(keyedMetadata && {
@@ -193,6 +195,7 @@ export async function aggregateFiles(settings) {
193
195
  content: parsedYaml,
194
196
  fileName: relativePath,
195
197
  fileFormat,
198
+ ...getTransformFormatProperty(settings, 'yaml'),
196
199
  fileId: hashStringSync(relativePath),
197
200
  versionId: hashStringSync(parsedYaml),
198
201
  locale: settings.defaultLocale,
@@ -235,6 +238,7 @@ export async function aggregateFiles(settings) {
235
238
  content: parsedJson,
236
239
  fileName: relativePath,
237
240
  fileFormat: 'TWILIO_CONTENT_JSON',
241
+ ...getTransformFormatProperty(settings, 'twilioContentJson'),
238
242
  dataFormat: 'STRING',
239
243
  locale: settings.defaultLocale,
240
244
  };
@@ -271,6 +275,7 @@ export async function aggregateFiles(settings) {
271
275
  content: processed,
272
276
  fileName: relativePath,
273
277
  fileFormat: fileType.toUpperCase(),
278
+ ...getTransformFormatProperty(settings, fileType),
274
279
  fileId: hashStringSync(relativePath),
275
280
  versionId: hashStringSync(processed),
276
281
  locale: settings.defaultLocale,
@@ -1,11 +1,12 @@
1
- import { ResolvedFiles, TransformFiles } from '../../types/index.js';
1
+ import { ResolvedFiles, TransformFiles, TransformFormats } from '../../types/index.js';
2
2
  import { FileMapping } from '../../types/files.js';
3
3
  /**
4
4
  * Creates a mapping between source files and their translated counterparts for each locale
5
5
  * @param filePaths - Resolved file paths for different file types
6
6
  * @param placeholderPaths - Placeholder paths for translated files
7
7
  * @param transformPaths - Transform paths for file naming
8
+ * @param transformFormats - Output file format transforms for translated files
8
9
  * @param locales - List of locales to create a mapping for
9
10
  * @returns A mapping between source files and their translated counterparts for each locale, in the form of relative paths
10
11
  */
11
- export declare function createFileMapping(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, targetLocales: string[], defaultLocale: string): FileMapping;
12
+ export declare function createFileMapping(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, transformFormats: TransformFormats, targetLocales: string[], defaultLocale: string): FileMapping;
@@ -5,15 +5,17 @@ import { getRelative } from '../../fs/findFilepath.js';
5
5
  import { getLocaleProperties } from 'generaltranslation';
6
6
  import { replaceLocalePlaceholders } from '../utils.js';
7
7
  import { TEMPLATE_FILE_NAME } from '../../utils/constants.js';
8
+ import { replaceFileExtensionForFormat } from './transformFormat.js';
8
9
  /**
9
10
  * Creates a mapping between source files and their translated counterparts for each locale
10
11
  * @param filePaths - Resolved file paths for different file types
11
12
  * @param placeholderPaths - Placeholder paths for translated files
12
13
  * @param transformPaths - Transform paths for file naming
14
+ * @param transformFormats - Output file format transforms for translated files
13
15
  * @param locales - List of locales to create a mapping for
14
16
  * @returns A mapping between source files and their translated counterparts for each locale, in the form of relative paths
15
17
  */
16
- export function createFileMapping(filePaths, placeholderPaths, transformPaths, targetLocales, defaultLocale) {
18
+ export function createFileMapping(filePaths, placeholderPaths, transformPaths, transformFormats, targetLocales, defaultLocale) {
17
19
  const fileMapping = {};
18
20
  for (const locale of targetLocales) {
19
21
  const translatedPaths = resolveLocaleFiles(placeholderPaths, locale);
@@ -32,6 +34,7 @@ export function createFileMapping(filePaths, placeholderPaths, transformPaths, t
32
34
  if (!translatedFiles)
33
35
  continue;
34
36
  const transformPath = transformPaths[typeIndex];
37
+ const transformFormat = transformFormats?.[typeIndex];
35
38
  if (transformPath) {
36
39
  if (typeof transformPath === 'string') {
37
40
  translatedFiles = translatedFiles.map((filePath) => {
@@ -105,7 +108,10 @@ export function createFileMapping(filePaths, placeholderPaths, transformPaths, t
105
108
  }
106
109
  for (let i = 0; i < sourcePaths.length; i++) {
107
110
  const sourceFile = getRelative(sourcePaths[i]);
108
- const translatedFile = getRelative(translatedFiles[i]);
111
+ // Format transforms keep the mapped path but rewrite the output suffix.
112
+ const translatedFile = getRelative(transformFormat
113
+ ? replaceFileExtensionForFormat(translatedFiles[i], transformFormat)
114
+ : translatedFiles[i]);
109
115
  localeMapping[sourceFile] = translatedFile;
110
116
  }
111
117
  }
@@ -1,6 +1,7 @@
1
- export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson"];
1
+ export declare const SUPPORTED_FILE_EXTENSIONS: readonly ["json", "pot", "mdx", "md", "ts", "js", "yaml", "html", "txt", "twilioContentJson"];
2
2
  export declare const FILE_EXT_TO_EXT_LABEL: {
3
3
  json: string;
4
+ pot: string;
4
5
  mdx: string;
5
6
  md: string;
6
7
  ts: string;
@@ -1,5 +1,6 @@
1
1
  export const SUPPORTED_FILE_EXTENSIONS = [
2
2
  'json',
3
+ 'pot',
3
4
  'mdx',
4
5
  'md',
5
6
  'ts',
@@ -11,6 +12,7 @@ export const SUPPORTED_FILE_EXTENSIONS = [
11
12
  ];
12
13
  export const FILE_EXT_TO_EXT_LABEL = {
13
14
  json: 'JSON',
15
+ pot: 'POT',
14
16
  mdx: 'MDX',
15
17
  md: 'Markdown',
16
18
  ts: 'TypeScript',
@@ -0,0 +1,68 @@
1
+ import type { FileFormat } from '../../types/data.js';
2
+ import type { FilesOptions, Settings, SupportedFileExtension } from '../../types/index.js';
3
+ /**
4
+ * Maps CLI config file keys to API file format enum values.
5
+ */
6
+ export declare const CONFIG_FILE_TYPE_TO_FILE_FORMAT: {
7
+ readonly json: "JSON";
8
+ readonly pot: "POT";
9
+ readonly mdx: "MDX";
10
+ readonly md: "MD";
11
+ readonly ts: "TS";
12
+ readonly js: "JS";
13
+ readonly yaml: "YAML";
14
+ readonly html: "HTML";
15
+ readonly txt: "TXT";
16
+ readonly twilioContentJson: "TWILIO_CONTENT_JSON";
17
+ };
18
+ /**
19
+ * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
20
+ */
21
+ export declare const FILE_FORMAT_TO_CONFIG_FILE_TYPE: {
22
+ readonly JSON: "json";
23
+ readonly POT: "pot";
24
+ readonly MDX: "mdx";
25
+ readonly MD: "md";
26
+ readonly TS: "ts";
27
+ readonly JS: "js";
28
+ readonly YAML: "yaml";
29
+ readonly HTML: "html";
30
+ readonly TXT: "txt";
31
+ readonly TWILIO_CONTENT_JSON: "twilioContentJson";
32
+ };
33
+ /**
34
+ * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
35
+ *
36
+ * This lets users write either `files.POT` or `files.pot` while keeping the
37
+ * rest of the CLI on its existing lowercase file-type convention.
38
+ */
39
+ export declare function normalizeFilesOptions(files: FilesOptions): FilesOptions;
40
+ /**
41
+ * Validates and resolves a configured output format for a source file type.
42
+ *
43
+ * Throws when the requested source -> output format is not supported by
44
+ * `generaltranslation/internal`.
45
+ */
46
+ export declare function resolveTransformationFormat(fileType: SupportedFileExtension, transformationFormat: string | undefined): FileFormat | undefined;
47
+ /**
48
+ * Returns the API upload/enqueue property for a file type when one is configured.
49
+ */
50
+ export declare function getTransformFormatProperty(settings: Settings, fileType: SupportedFileExtension): {
51
+ transformFormat?: FileFormat;
52
+ };
53
+ /**
54
+ * Returns the preferred file extension for a translated file format.
55
+ */
56
+ export declare function getFileExtensionForFormat(format: FileFormat): string;
57
+ /**
58
+ * Rewrites a path's extension to match the translated file format.
59
+ */
60
+ export declare function replaceFileExtensionForFormat(filePath: string, format: FileFormat): string;
61
+ /**
62
+ * Detects whether any configured format transform changes the output file type.
63
+ */
64
+ export declare function hasNonIdentityFileFormatTransform(settings: Settings): boolean;
65
+ /**
66
+ * Returns true when the configured transform for a file type changes its format.
67
+ */
68
+ export declare function hasNonIdentityFileFormatTransformForType(settings: Settings, fileType: SupportedFileExtension): boolean;
@@ -0,0 +1,122 @@
1
+ import { validateFileFormatTransforms } from 'generaltranslation/internal';
2
+ /**
3
+ * Maps CLI config file keys to API file format enum values.
4
+ */
5
+ export const CONFIG_FILE_TYPE_TO_FILE_FORMAT = {
6
+ json: 'JSON',
7
+ pot: 'POT',
8
+ mdx: 'MDX',
9
+ md: 'MD',
10
+ ts: 'TS',
11
+ js: 'JS',
12
+ yaml: 'YAML',
13
+ html: 'HTML',
14
+ txt: 'TXT',
15
+ twilioContentJson: 'TWILIO_CONTENT_JSON',
16
+ };
17
+ /**
18
+ * Maps uppercase config aliases to the CLI's canonical lowercase file keys.
19
+ */
20
+ export const FILE_FORMAT_TO_CONFIG_FILE_TYPE = {
21
+ JSON: 'json',
22
+ POT: 'pot',
23
+ MDX: 'mdx',
24
+ MD: 'md',
25
+ TS: 'ts',
26
+ JS: 'js',
27
+ YAML: 'yaml',
28
+ HTML: 'html',
29
+ TXT: 'txt',
30
+ TWILIO_CONTENT_JSON: 'twilioContentJson',
31
+ };
32
+ /**
33
+ * Maps API file format enum values to the extension the CLI should write.
34
+ */
35
+ const FILE_FORMAT_EXTENSIONS = {
36
+ GTJSON: 'json',
37
+ JSON: 'json',
38
+ PO: 'po',
39
+ POT: 'pot',
40
+ YAML: 'yaml',
41
+ MDX: 'mdx',
42
+ MD: 'md',
43
+ TS: 'ts',
44
+ JS: 'js',
45
+ HTML: 'html',
46
+ TXT: 'txt',
47
+ TWILIO_CONTENT_JSON: 'json',
48
+ };
49
+ /**
50
+ * Converts uppercase file format config keys into the CLI's canonical lowercase keys.
51
+ *
52
+ * This lets users write either `files.POT` or `files.pot` while keeping the
53
+ * rest of the CLI on its existing lowercase file-type convention.
54
+ */
55
+ export function normalizeFilesOptions(files) {
56
+ const normalized = { ...files };
57
+ for (const [fileFormat, fileType] of Object.entries(FILE_FORMAT_TO_CONFIG_FILE_TYPE)) {
58
+ const uppercaseConfig = normalized[fileFormat];
59
+ if (!normalized[fileType] && uppercaseConfig) {
60
+ normalized[fileType] = uppercaseConfig;
61
+ }
62
+ delete normalized[fileFormat];
63
+ }
64
+ return normalized;
65
+ }
66
+ /**
67
+ * Validates and resolves a configured output format for a source file type.
68
+ *
69
+ * Throws when the requested source -> output format is not supported by
70
+ * `generaltranslation/internal`.
71
+ */
72
+ export function resolveTransformationFormat(fileType, transformationFormat) {
73
+ if (!transformationFormat)
74
+ return undefined;
75
+ const fileFormat = CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType];
76
+ validateFileFormatTransforms([
77
+ {
78
+ fileFormat,
79
+ transformFormat: transformationFormat,
80
+ fileName: `files.${fileType}`,
81
+ },
82
+ ]);
83
+ return transformationFormat;
84
+ }
85
+ /**
86
+ * Returns the API upload/enqueue property for a file type when one is configured.
87
+ */
88
+ export function getTransformFormatProperty(settings, fileType) {
89
+ const transformFormat = settings.files?.transformFormats?.[fileType];
90
+ return transformFormat ? { transformFormat } : {};
91
+ }
92
+ /**
93
+ * Returns the preferred file extension for a translated file format.
94
+ */
95
+ export function getFileExtensionForFormat(format) {
96
+ return FILE_FORMAT_EXTENSIONS[format];
97
+ }
98
+ /**
99
+ * Rewrites a path's extension to match the translated file format.
100
+ */
101
+ export function replaceFileExtensionForFormat(filePath, format) {
102
+ const extension = getFileExtensionForFormat(format);
103
+ return /\.[^/.]+$/.test(filePath)
104
+ ? filePath.replace(/\.[^/.]+$/, `.${extension}`)
105
+ : `${filePath}.${extension}`;
106
+ }
107
+ /**
108
+ * Detects whether any configured format transform changes the output file type.
109
+ */
110
+ export function hasNonIdentityFileFormatTransform(settings) {
111
+ return Object.entries(settings.files?.transformFormats || {}).some(([fileType, transformFormat]) => transformFormat &&
112
+ CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !==
113
+ transformFormat);
114
+ }
115
+ /**
116
+ * Returns true when the configured transform for a file type changes its format.
117
+ */
118
+ export function hasNonIdentityFileFormatTransformForType(settings, fileType) {
119
+ const transformFormat = settings.files?.transformFormats?.[fileType];
120
+ return !!(transformFormat &&
121
+ CONFIG_FILE_TYPE_TO_FILE_FORMAT[fileType] !== transformFormat);
122
+ }
@@ -5,6 +5,7 @@ import { logger } from '../../console/logger.js';
5
5
  import chalk from 'chalk';
6
6
  import micromatch from 'micromatch';
7
7
  import { BASE_PARSING_FLAGS_DEFAULT, GT_PARSING_FLAGS_DEFAULT, } from '../../config/defaults.js';
8
+ import { resolveTransformationFormat } from '../../formats/files/transformFormat.js';
8
9
  /**
9
10
  * Resolves the files from the files object
10
11
  * Replaces [locale] with the actual locale in the files
@@ -59,6 +60,8 @@ export function resolveFiles(files, locale, locales, cwd, compositePatterns) {
59
60
  const resolvedPaths = {};
60
61
  const placeholderResult = {};
61
62
  const transformPaths = {};
63
+ // Output format transforms are tracked separately from path transforms.
64
+ const transformFormats = {};
62
65
  const publishPaths = new Set();
63
66
  const unpublishPaths = new Set();
64
67
  const parsingFlags = {};
@@ -75,6 +78,11 @@ export function resolveFiles(files, locale, locales, cwd, compositePatterns) {
75
78
  Array.isArray(transform))) {
76
79
  transformPaths[fileType] = transform;
77
80
  }
81
+ // Validate source -> output format transforms during settings generation.
82
+ const transformFormat = resolveTransformationFormat(fileType, files[fileType]?.transformationFormat);
83
+ if (transformFormat) {
84
+ transformFormats[fileType] = transformFormat;
85
+ }
78
86
  // ==== PLACEHOLDERS ==== //
79
87
  if (files[fileType]?.include) {
80
88
  const { paths, publishPatterns, unpublishPatterns } = normalizeIncludePatterns(files[fileType].include);
@@ -96,6 +104,7 @@ export function resolveFiles(files, locale, locales, cwd, compositePatterns) {
96
104
  resolvedPaths,
97
105
  placeholderPaths: placeholderResult,
98
106
  transformPaths: transformPaths,
107
+ transformFormats,
99
108
  publishPaths,
100
109
  unpublishPaths,
101
110
  parsingFlags,
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.14.21";
1
+ export declare const PACKAGE_VERSION = "2.14.22";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.14.21';
2
+ export const PACKAGE_VERSION = '2.14.22';
@@ -1,4 +1,4 @@
1
- import { CustomMapping } from 'generaltranslation/types';
1
+ import { CustomMapping, FileFormat } from 'generaltranslation/types';
2
2
  import { SUPPORTED_FILE_EXTENSIONS } from '../formats/files/supportedFiles.js';
3
3
  import { ParsingConfigOptions, GTParsingFlags, BaseParsingFlags, ParseFlagsByFileType } from './parsing.js';
4
4
  import { Libraries, InlineLibrary } from './libraries.js';
@@ -135,6 +135,9 @@ export type TransformOption = {
135
135
  export type TransformFiles = {
136
136
  [K in SupportedFileExtension]?: TransformOption | string | TransformOption[];
137
137
  };
138
+ export type TransformFormats = {
139
+ [K in SupportedFileExtension]?: FileFormat;
140
+ };
138
141
  export type IncludePattern = string | {
139
142
  pattern: string;
140
143
  publish?: boolean;
@@ -144,6 +147,7 @@ export type FilesOptions = {
144
147
  include: IncludePattern[];
145
148
  exclude?: string[];
146
149
  transform?: string | TransformOption | TransformOption[];
150
+ transformationFormat?: FileFormat;
147
151
  parsingFlags?: BaseParsingFlags;
148
152
  };
149
153
  } & {
@@ -172,6 +176,7 @@ export type Settings = {
172
176
  resolvedPaths: ResolvedFiles;
173
177
  placeholderPaths: ResolvedFiles;
174
178
  transformPaths: TransformFiles;
179
+ transformFormats: TransformFormats;
175
180
  publishPaths: Set<string>;
176
181
  unpublishPaths: Set<string>;
177
182
  parsingFlags: ParseFlagsByFileType;
@@ -7,7 +7,7 @@ export default async function flattenJsonFiles(settings, includeFiles) {
7
7
  return;
8
8
  }
9
9
  const { resolvedPaths: sourceFiles } = settings.files;
10
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
10
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.files.transformFormats, settings.locales, settings.defaultLocale);
11
11
  await Promise.all(Object.values(fileMapping).map(async (filesMap) => {
12
12
  const targetFiles = Object.values(filesMap).filter((p) => p.endsWith('.json') && (!includeFiles || includeFiles.has(p)));
13
13
  await Promise.all(targetFiles.map(async (file) => {
@@ -136,7 +136,7 @@ export default async function localizeRelativeAssets(settings, targetLocales, in
136
136
  }
137
137
  const { resolvedPaths: sourceFiles } = settings.files;
138
138
  const locales = targetLocales || settings.locales;
139
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
139
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.files.transformFormats, settings.locales, settings.defaultLocale);
140
140
  const cwd = process.cwd();
141
141
  const processPromises = Object.entries(fileMapping)
142
142
  .filter(([locale]) => locales.includes(locale))
@@ -28,7 +28,7 @@ export default async function localizeStaticImports(settings, includeFiles) {
28
28
  return;
29
29
  }
30
30
  const { resolvedPaths: sourceFiles } = settings.files;
31
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
31
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.files.transformFormats, settings.locales, settings.defaultLocale);
32
32
  // Process all file types at once with a single call
33
33
  const processPromises = [];
34
34
  // First, process default locale files (from source files)
@@ -31,7 +31,7 @@ export default async function localizeStaticUrls(settings, targetLocales, includ
31
31
  const { resolvedPaths: sourceFiles } = settings.files;
32
32
  // Use filtered locales if provided, otherwise use all locales
33
33
  const locales = targetLocales || settings.locales;
34
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, // Always use all locales for mapping, filter later
34
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.files.transformFormats, settings.locales, // Always use all locales for mapping, filter later
35
35
  settings.defaultLocale);
36
36
  // Process all file types at once with a single call
37
37
  const processPromises = [];
@@ -9,8 +9,8 @@ import * as fs from 'fs';
9
9
  export default async function processAnchorIds(settings, includeFiles) {
10
10
  if (!settings.files)
11
11
  return;
12
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
13
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
12
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
13
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
14
14
  const sourceTypeByPath = new Map();
15
15
  if (resolvedPaths.md) {
16
16
  for (const filePath of resolvedPaths.md) {
@@ -34,8 +34,8 @@ export default async function processOpenApi(settings, includeFiles) {
34
34
  if (!specAnalyses.length)
35
35
  return;
36
36
  const warnings = new Set();
37
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
38
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
37
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
38
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, settings.locales, settings.defaultLocale);
39
39
  const fileMappingAbs = {};
40
40
  for (const [locale, mapping] of Object.entries(fileMapping)) {
41
41
  fileMappingAbs[locale] = {};
@@ -226,11 +226,11 @@ export async function mirrorAssetsToLocales(settings) {
226
226
  const assetPaths = resolveAssetPaths(include, cwd);
227
227
  if (assetPaths.size === 0)
228
228
  return;
229
- const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
229
+ const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } = settings.files;
230
230
  const targetLocales = settings.locales.filter((l) => l !== settings.defaultLocale);
231
231
  if (targetLocales.length === 0)
232
232
  return;
233
- const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, targetLocales, settings.defaultLocale);
233
+ const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, transformFormats, targetLocales, settings.defaultLocale);
234
234
  for (const locale of targetLocales) {
235
235
  const filesMap = fileMapping[locale];
236
236
  if (!filesMap)
@@ -110,7 +110,15 @@ export class UploadSourcesStep extends WorkflowStep {
110
110
  sourceLocale: this.settings.defaultLocale,
111
111
  modelProvider: this.settings.modelProvider,
112
112
  });
113
- this.result = response.uploadedFiles;
113
+ // The API may not echo transformFormat, so preserve it from local inputs.
114
+ const localFileMap = new Map(files.map((f) => [`${f.fileId}:${f.versionId}`, f]));
115
+ this.result = response.uploadedFiles.map((uploadedFile) => {
116
+ const localFile = localFileMap.get(`${uploadedFile.fileId}:${uploadedFile.versionId}`);
117
+ return {
118
+ ...uploadedFile,
119
+ transformFormat: localFile?.transformFormat ?? uploadedFile.transformFormat,
120
+ };
121
+ });
114
122
  // Merge files that were already uploaded into the result
115
123
  this.result.push(...filesToSkipUpload.map((f) => ({
116
124
  fileId: f.fileId,
@@ -118,6 +126,7 @@ export class UploadSourcesStep extends WorkflowStep {
118
126
  branchId: f.branchId ?? currentBranchId,
119
127
  fileName: f.fileName,
120
128
  fileFormat: f.fileFormat,
129
+ transformFormat: f.transformFormat,
121
130
  dataFormat: f.dataFormat,
122
131
  locale: f.locale,
123
132
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.14.21",
3
+ "version": "2.14.22",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -110,8 +110,8 @@
110
110
  "unified": "^11.0.5",
111
111
  "unist-util-visit": "^5.0.0",
112
112
  "yaml": "^2.8.0",
113
- "@generaltranslation/python-extractor": "0.2.12",
114
- "generaltranslation": "8.2.7",
113
+ "@generaltranslation/python-extractor": "0.2.13",
114
+ "generaltranslation": "8.2.8",
115
115
  "gt-remark": "1.0.7"
116
116
  },
117
117
  "devDependencies": {