gt 2.14.57 → 2.14.58
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 +11 -0
- package/dist/api/downloadFileBatch.js +29 -0
- package/dist/api/downloadFileBatch.js.map +1 -1
- package/dist/cli/commands/enqueue.js +2 -0
- package/dist/cli/commands/enqueue.js.map +1 -1
- package/dist/cli/commands/stage.js +2 -0
- package/dist/cli/commands/stage.js.map +1 -1
- package/dist/config/generateSettings.js +4 -1
- package/dist/config/generateSettings.js.map +1 -1
- package/dist/console/displayTranslateSummary.js +2 -0
- package/dist/console/displayTranslateSummary.js.map +1 -1
- package/dist/console/index.d.ts +1 -0
- package/dist/console/index.js +2 -1
- package/dist/console/index.js.map +1 -1
- package/dist/extraction/postProcess.js +1 -0
- package/dist/extraction/postProcess.js.map +1 -1
- package/dist/formats/files/aggregateFiles.js +6 -5
- package/dist/formats/files/aggregateFiles.js.map +1 -1
- package/dist/formats/files/collectFiles.js +7 -1
- package/dist/formats/files/collectFiles.js.map +1 -1
- package/dist/formats/files/convertToFileTranslationData.js +12 -1
- package/dist/formats/files/convertToFileTranslationData.js.map +1 -1
- package/dist/fs/config/parseFilesConfig.d.ts +1 -1
- package/dist/fs/config/parseFilesConfig.js +41 -1
- package/dist/fs/config/parseFilesConfig.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/react/jsx/utils/constants.d.ts +2 -2
- package/dist/react/jsx/utils/constants.js +3 -1
- package/dist/react/jsx/utils/constants.js.map +1 -1
- package/dist/react/jsx/utils/jsxParsing/parseTProps.js +14 -1
- package/dist/react/jsx/utils/jsxParsing/parseTProps.js.map +1 -1
- package/dist/react/jsx/utils/mapAttributeName.d.ts +1 -1
- package/dist/react/jsx/utils/mapAttributeName.js +1 -0
- package/dist/react/jsx/utils/mapAttributeName.js.map +1 -1
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.d.ts +1 -0
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.js +4 -2
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.js.map +1 -1
- package/dist/state/translateWarnings.d.ts +1 -1
- package/dist/state/translateWarnings.js.map +1 -1
- package/dist/translation/reviewSetupWarning.d.ts +16 -0
- package/dist/translation/reviewSetupWarning.js +52 -0
- package/dist/translation/reviewSetupWarning.js.map +1 -0
- package/dist/types/files.d.ts +2 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/utils/hash.d.ts +8 -0
- package/dist/utils/hash.js +11 -1
- package/dist/utils/hash.js.map +1 -1
- package/dist/workflows/download.d.ts +2 -0
- package/dist/workflows/download.js +2 -1
- package/dist/workflows/download.js.map +1 -1
- package/dist/workflows/steps/DownloadStep.js +20 -0
- package/dist/workflows/steps/DownloadStep.js.map +1 -1
- package/dist/workflows/steps/EnqueueStep.js +0 -1
- package/dist/workflows/steps/EnqueueStep.js.map +1 -1
- package/package.json +4 -4
package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractStringEntryMetadata.js","names":["pathModule"],"sources":["../../../../../../src/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.ts"],"sourcesContent":["import * as t from '@babel/types';\nimport { ParsingConfig } from '../types.js';\nimport { ParsingOutput } from '../types.js';\nimport { isStaticExpression } from '../../../evaluateJsx.js';\nimport { warnInvalidMaxCharsSync } from '../../../../../console/index.js';\nimport { warnInvalidFormatSync } from '../../../../../console/index.js';\nimport { warnNonStaticExpressionSync } from '../../../../../console/index.js';\nimport { GT_ATTRIBUTES_WITH_SUGAR } from '../../constants.js';\nimport { containsDeriveCall } from '../derivation/containsDeriveCall.js';\nimport generateModule from '@babel/generator';\nimport { mapAttributeName } from '../../mapAttributeName.js';\nimport pathModule from 'node:path';\nimport { isNumberLiteral } from '../../isNumberLiteral.js';\nimport { extractSourceCode } from '../../extractSourceCode.js';\nimport type { SourceCode } from '../../extractSourceCode.js';\nimport { SURROUNDING_LINE_COUNT } from '../../../../../utils/constants.js';\n\n// Handle CommonJS/ESM interop\nconst generate = generateModule.default || generateModule;\n\n/**\n * Metadata record type\n */\nexport type InlineMetadata = {\n maxChars?: number;\n context?: string;\n id?: string;\n hash?: string;\n format?: string;\n filePaths?: string[];\n sourceCode?: Record<string, SourceCode[]>;\n contextDeriveExpr?: t.Expression;\n};\n\n/**\n * Extracts inline metadata from a string entry\n * @param metadata - The metadata record to inject the data into\n * @param entry - The string entry to extract the metadata from\n * @param output - The output to use\n * @param config - The configuration to use\n * @returns The inline metadata\n *\n * @note - this function does not automatically append the index to the id, this must be done manually in the caller.\n *\n */\nexport function extractStringEntryMetadata({\n options,\n output,\n config,\n nodeLoc,\n surroundingLineCount = SURROUNDING_LINE_COUNT,\n}: {\n options?: t.CallExpression['arguments'][number];\n output: ParsingOutput;\n config: ParsingConfig;\n nodeLoc?: {\n start?: { line: number } | null;\n end?: { line: number } | null;\n } | null;\n surroundingLineCount?: number;\n}): InlineMetadata {\n // extract filepath for entry\n const relativeFilepath = pathModule.relative(process.cwd(), config.file);\n\n // extract inline metadata\n const inlineMetadata = extractInlineMetadata({\n options,\n output,\n config,\n });\n\n // extract surrounding lines from source file\n let sourceCode: Record<string, SourceCode[]> | undefined;\n if (\n config.includeSourceCodeContext &&\n nodeLoc?.start?.line &&\n nodeLoc?.end?.line\n ) {\n const entry = extractSourceCode(\n config.file,\n nodeLoc.start.line,\n nodeLoc.end.line,\n surroundingLineCount\n );\n if (entry && relativeFilepath) {\n sourceCode = { [relativeFilepath]: [entry] };\n }\n }\n\n return {\n ...inlineMetadata,\n filePaths: relativeFilepath ? [relativeFilepath] : undefined,\n ...(sourceCode && { sourceCode }),\n };\n}\n\n// ----- Helper Functions ----- //\n\n/**\n * Extracts inline metadata specified e.g. gt('Hello', { $context: 'greeting' })\n * @param options - The options to extract the metadata from\n * @param output - The output to use\n * @param config - The configuration to use\n * @param index - The index of the string entry\n * @returns The inline metadata\n */\nfunction extractInlineMetadata({\n options,\n output,\n config,\n}: {\n options?: t.CallExpression['arguments'][number];\n output: ParsingOutput;\n config: ParsingConfig;\n}): InlineMetadata {\n const metadata: Record<string, string | number | string[]> = {};\n let contextDeriveExpr: t.Expression | undefined;\n if (options && options.type === 'ObjectExpression') {\n options.properties.forEach((prop) => {\n if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {\n const attribute = prop.key.name;\n if (\n GT_ATTRIBUTES_WITH_SUGAR.includes(\n attribute as (typeof GT_ATTRIBUTES_WITH_SUGAR)[number]\n ) &&\n t.isExpression(prop.value)\n ) {\n const result = isStaticExpression(prop.value);\n if (!result.isStatic) {\n const mappedKey = mapAttributeName(attribute);\n if (mappedKey === 'context' && containsDeriveCall(prop.value)) {\n contextDeriveExpr = prop.value;\n } else {\n output.errors.push(\n warnNonStaticExpressionSync(\n config.file,\n attribute,\n generate(prop.value).code,\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n }\n }\n if (\n result.isStatic &&\n result.value != null &&\n !config.ignoreInlineMetadata\n ) {\n const mappedKey = mapAttributeName(attribute);\n if (mappedKey === 'maxChars') {\n // Handle maxChars attribute\n if (\n (typeof result.value === 'string' &&\n (isNaN(Number(result.value)) ||\n !isNumberLiteral(prop.value))) ||\n !Number.isInteger(Number(result.value))\n ) {\n output.errors.push(\n warnInvalidMaxCharsSync(\n config.file,\n generate(prop).code,\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n } else if (typeof result.value === 'string') {\n // Add the maxChars value to the metadata\n metadata[mappedKey] = Math.abs(Number(result.value));\n }\n } else if (mappedKey === 'format') {\n // Handle format attribute - validate allowed values\n const validFormats = ['ICU', 'STRING', 'I18NEXT'];\n if (\n typeof result.value === 'string' &&\n validFormats.includes(result.value)\n ) {\n metadata[mappedKey] = result.value;\n } else {\n output.warnings.add(\n warnInvalidFormatSync(\n config.file,\n String(result.value),\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n }\n } else {\n // Add the $context or $id or other attributes value to the metadata\n // TODO: why are we including everything? arent we only interested in relevant inline metadata?\n metadata[mappedKey] = result.value;\n }\n }\n }\n }\n });\n }\n\n return {\n ...metadata,\n ...(contextDeriveExpr && { contextDeriveExpr }),\n };\n}\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"extractStringEntryMetadata.js","names":["pathModule"],"sources":["../../../../../../src/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.ts"],"sourcesContent":["import * as t from '@babel/types';\nimport { ParsingConfig } from '../types.js';\nimport { ParsingOutput } from '../types.js';\nimport { isStaticExpression } from '../../../evaluateJsx.js';\nimport { warnInvalidMaxCharsSync } from '../../../../../console/index.js';\nimport { warnInvalidFormatSync } from '../../../../../console/index.js';\nimport { warnInvalidRequiresReviewSync } from '../../../../../console/index.js';\nimport { warnNonStaticExpressionSync } from '../../../../../console/index.js';\nimport { GT_ATTRIBUTES_WITH_SUGAR } from '../../constants.js';\nimport { containsDeriveCall } from '../derivation/containsDeriveCall.js';\nimport generateModule from '@babel/generator';\nimport { mapAttributeName } from '../../mapAttributeName.js';\nimport pathModule from 'node:path';\nimport { isNumberLiteral } from '../../isNumberLiteral.js';\nimport { extractSourceCode } from '../../extractSourceCode.js';\nimport type { SourceCode } from '../../extractSourceCode.js';\nimport { SURROUNDING_LINE_COUNT } from '../../../../../utils/constants.js';\n\n// Handle CommonJS/ESM interop\nconst generate = generateModule.default || generateModule;\n\n/**\n * Metadata record type\n */\nexport type InlineMetadata = {\n maxChars?: number;\n context?: string;\n id?: string;\n hash?: string;\n format?: string;\n requiresReview?: boolean;\n filePaths?: string[];\n sourceCode?: Record<string, SourceCode[]>;\n contextDeriveExpr?: t.Expression;\n};\n\n/**\n * Extracts inline metadata from a string entry\n * @param metadata - The metadata record to inject the data into\n * @param entry - The string entry to extract the metadata from\n * @param output - The output to use\n * @param config - The configuration to use\n * @returns The inline metadata\n *\n * @note - this function does not automatically append the index to the id, this must be done manually in the caller.\n *\n */\nexport function extractStringEntryMetadata({\n options,\n output,\n config,\n nodeLoc,\n surroundingLineCount = SURROUNDING_LINE_COUNT,\n}: {\n options?: t.CallExpression['arguments'][number];\n output: ParsingOutput;\n config: ParsingConfig;\n nodeLoc?: {\n start?: { line: number } | null;\n end?: { line: number } | null;\n } | null;\n surroundingLineCount?: number;\n}): InlineMetadata {\n // extract filepath for entry\n const relativeFilepath = pathModule.relative(process.cwd(), config.file);\n\n // extract inline metadata\n const inlineMetadata = extractInlineMetadata({\n options,\n output,\n config,\n });\n\n // extract surrounding lines from source file\n let sourceCode: Record<string, SourceCode[]> | undefined;\n if (\n config.includeSourceCodeContext &&\n nodeLoc?.start?.line &&\n nodeLoc?.end?.line\n ) {\n const entry = extractSourceCode(\n config.file,\n nodeLoc.start.line,\n nodeLoc.end.line,\n surroundingLineCount\n );\n if (entry && relativeFilepath) {\n sourceCode = { [relativeFilepath]: [entry] };\n }\n }\n\n return {\n ...inlineMetadata,\n filePaths: relativeFilepath ? [relativeFilepath] : undefined,\n ...(sourceCode && { sourceCode }),\n };\n}\n\n// ----- Helper Functions ----- //\n\n/**\n * Extracts inline metadata specified e.g. gt('Hello', { $context: 'greeting' })\n * @param options - The options to extract the metadata from\n * @param output - The output to use\n * @param config - The configuration to use\n * @param index - The index of the string entry\n * @returns The inline metadata\n */\nfunction extractInlineMetadata({\n options,\n output,\n config,\n}: {\n options?: t.CallExpression['arguments'][number];\n output: ParsingOutput;\n config: ParsingConfig;\n}): InlineMetadata {\n const metadata: Record<string, string | number | boolean | string[]> = {};\n let contextDeriveExpr: t.Expression | undefined;\n if (options && options.type === 'ObjectExpression') {\n options.properties.forEach((prop) => {\n if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {\n const attribute = prop.key.name;\n if (\n GT_ATTRIBUTES_WITH_SUGAR.includes(\n attribute as (typeof GT_ATTRIBUTES_WITH_SUGAR)[number]\n ) &&\n t.isExpression(prop.value)\n ) {\n const result = isStaticExpression(prop.value);\n if (!result.isStatic) {\n const mappedKey = mapAttributeName(attribute);\n if (mappedKey === 'context' && containsDeriveCall(prop.value)) {\n contextDeriveExpr = prop.value;\n } else {\n output.errors.push(\n warnNonStaticExpressionSync(\n config.file,\n attribute,\n generate(prop.value).code,\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n }\n }\n if (\n result.isStatic &&\n result.value != null &&\n !config.ignoreInlineMetadata\n ) {\n const mappedKey = mapAttributeName(attribute);\n if (mappedKey === 'maxChars') {\n // Handle maxChars attribute\n if (\n (typeof result.value === 'string' &&\n (isNaN(Number(result.value)) ||\n !isNumberLiteral(prop.value))) ||\n !Number.isInteger(Number(result.value))\n ) {\n output.errors.push(\n warnInvalidMaxCharsSync(\n config.file,\n generate(prop).code,\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n } else if (typeof result.value === 'string') {\n // Add the maxChars value to the metadata\n metadata[mappedKey] = Math.abs(Number(result.value));\n }\n } else if (mappedKey === 'requiresReview') {\n // Handle $requiresReview attribute - boolean literal only,\n // string \"true\"/\"false\" is rejected because the value is\n // hash-changing and coercion would be too easy to get wrong\n if (t.isBooleanLiteral(prop.value)) {\n metadata[mappedKey] = prop.value.value;\n } else {\n output.errors.push(\n warnInvalidRequiresReviewSync(\n config.file,\n generate(prop).code,\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n }\n } else if (mappedKey === 'format') {\n // Handle format attribute - validate allowed values\n const validFormats = ['ICU', 'STRING', 'I18NEXT'];\n if (\n typeof result.value === 'string' &&\n validFormats.includes(result.value)\n ) {\n metadata[mappedKey] = result.value;\n } else {\n output.warnings.add(\n warnInvalidFormatSync(\n config.file,\n String(result.value),\n `${prop.loc?.start?.line}:${prop.loc?.start?.column}`\n )\n );\n }\n } else {\n // Add the $context or $id or other attributes value to the metadata\n // TODO: why are we including everything? arent we only interested in relevant inline metadata?\n metadata[mappedKey] = result.value;\n }\n }\n }\n }\n });\n }\n\n return {\n ...metadata,\n ...(contextDeriveExpr && { contextDeriveExpr }),\n };\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAM,WAAW,eAAe,WAAW;;;;;;;;;;;;AA4B3C,SAAgB,2BAA2B,EACzC,SACA,QACA,QACA,SACA,uBAAA,KAUiB;CAEjB,MAAM,mBAAmBA,KAAW,SAAS,QAAQ,KAAK,EAAE,OAAO,KAAK;CAGxE,MAAM,iBAAiB,sBAAsB;EAC3C;EACA;EACA;EACD,CAAC;CAGF,IAAI;AACJ,KACE,OAAO,4BACP,SAAS,OAAO,QAChB,SAAS,KAAK,MACd;EACA,MAAM,QAAQ,kBACZ,OAAO,MACP,QAAQ,MAAM,MACd,QAAQ,IAAI,MACZ,qBACD;AACD,MAAI,SAAS,iBACX,cAAa,GAAG,mBAAmB,CAAC,MAAM,EAAE;;AAIhD,QAAO;EACL,GAAG;EACH,WAAW,mBAAmB,CAAC,iBAAiB,GAAG,KAAA;EACnD,GAAI,cAAc,EAAE,YAAY;EACjC;;;;;;;;;;AAaH,SAAS,sBAAsB,EAC7B,SACA,QACA,UAKiB;CACjB,MAAM,WAAiE,EAAE;CACzE,IAAI;AACJ,KAAI,WAAW,QAAQ,SAAS,mBAC9B,SAAQ,WAAW,SAAS,SAAS;AACnC,MAAI,KAAK,SAAS,oBAAoB,KAAK,IAAI,SAAS,cAAc;GACpE,MAAM,YAAY,KAAK,IAAI;AAC3B,OACE,yBAAyB,SACvB,UACD,IACD,EAAE,aAAa,KAAK,MAAM,EAC1B;IACA,MAAM,SAAS,mBAAmB,KAAK,MAAM;AAC7C,QAAI,CAAC,OAAO,SAEV,KADkB,iBAAiB,UACtB,KAAK,aAAa,mBAAmB,KAAK,MAAM,CAC3D,qBAAoB,KAAK;QAEzB,QAAO,OAAO,KACZ,4BACE,OAAO,MACP,WACA,SAAS,KAAK,MAAM,CAAC,MACrB,GAAG,KAAK,KAAK,OAAO,KAAK,GAAG,KAAK,KAAK,OAAO,SAC9C,CACF;AAGL,QACE,OAAO,YACP,OAAO,SAAS,QAChB,CAAC,OAAO,sBACR;KACA,MAAM,YAAY,iBAAiB,UAAU;AAC7C,SAAI,cAAc;UAGb,OAAO,OAAO,UAAU,aACtB,MAAM,OAAO,OAAO,MAAM,CAAC,IAC1B,CAAC,gBAAgB,KAAK,MAAM,KAChC,CAAC,OAAO,UAAU,OAAO,OAAO,MAAM,CAAC,CAEvC,QAAO,OAAO,KACZ,wBACE,OAAO,MACP,SAAS,KAAK,CAAC,MACf,GAAG,KAAK,KAAK,OAAO,KAAK,GAAG,KAAK,KAAK,OAAO,SAC9C,CACF;eACQ,OAAO,OAAO,UAAU,SAEjC,UAAS,aAAa,KAAK,IAAI,OAAO,OAAO,MAAM,CAAC;gBAE7C,cAAc,iBAIvB,KAAI,EAAE,iBAAiB,KAAK,MAAM,CAChC,UAAS,aAAa,KAAK,MAAM;SAEjC,QAAO,OAAO,KACZ,8BACE,OAAO,MACP,SAAS,KAAK,CAAC,MACf,GAAG,KAAK,KAAK,OAAO,KAAK,GAAG,KAAK,KAAK,OAAO,SAC9C,CACF;cAEM,cAAc,SAGvB,KACE,OAAO,OAAO,UAAU,YACxB;MAHoB;MAAO;MAAU;MAGzB,CAAC,SAAS,OAAO,MAAM,CAEnC,UAAS,aAAa,OAAO;SAE7B,QAAO,SAAS,IACd,sBACE,OAAO,MACP,OAAO,OAAO,MAAM,EACpB,GAAG,KAAK,KAAK,OAAO,KAAK,GAAG,KAAK,KAAK,OAAO,SAC9C,CACF;SAKH,UAAS,aAAa,OAAO;;;;GAKrC;AAGJ,QAAO;EACL,GAAG;EACH,GAAI,qBAAqB,EAAE,mBAAmB;EAC/C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WarningCategory = 'skipped_file' | 'failed_move' | 'failed_translation' | 'failed_download';
|
|
1
|
+
export type WarningCategory = 'skipped_file' | 'pending_review' | 'failed_move' | 'failed_translation' | 'failed_download';
|
|
2
2
|
export type TranslateWarning = {
|
|
3
3
|
category: WarningCategory;
|
|
4
4
|
fileName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translateWarnings.js","names":[],"sources":["../../src/state/translateWarnings.ts"],"sourcesContent":["export type WarningCategory =\n | 'skipped_file'\n | 'failed_move'\n | 'failed_translation'\n | 'failed_download';\n\nexport type TranslateWarning = {\n category: WarningCategory;\n fileName: string;\n reason: string;\n};\n\nconst warnings: TranslateWarning[] = [];\n\nexport function recordWarning(\n category: WarningCategory,\n fileName: string,\n reason: string\n) {\n warnings.push({ category, fileName, reason });\n}\n\nexport function getWarnings(): TranslateWarning[] {\n return warnings;\n}\n\nexport function hasWarnings(): boolean {\n return warnings.length > 0;\n}\n\nexport function clearWarnings() {\n warnings.length = 0;\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"translateWarnings.js","names":[],"sources":["../../src/state/translateWarnings.ts"],"sourcesContent":["export type WarningCategory =\n | 'skipped_file'\n | 'pending_review'\n | 'failed_move'\n | 'failed_translation'\n | 'failed_download';\n\nexport type TranslateWarning = {\n category: WarningCategory;\n fileName: string;\n reason: string;\n};\n\nconst warnings: TranslateWarning[] = [];\n\nexport function recordWarning(\n category: WarningCategory,\n fileName: string,\n reason: string\n) {\n warnings.push({ category, fileName, reason });\n}\n\nexport function getWarnings(): TranslateWarning[] {\n return warnings;\n}\n\nexport function hasWarnings(): boolean {\n return warnings.length > 0;\n}\n\nexport function clearWarnings() {\n warnings.length = 0;\n}\n"],"mappings":";AAaA,MAAM,WAA+B,EAAE;AAEvC,SAAgB,cACd,UACA,UACA,QACA;AACA,UAAS,KAAK;EAAE;EAAU;EAAU;EAAQ,CAAC;;AAG/C,SAAgB,cAAkC;AAChD,QAAO;;AAGT,SAAgB,cAAuB;AACrC,QAAO,SAAS,SAAS;;AAG3B,SAAgB,gBAAgB;AAC9B,UAAS,SAAS"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FileToUpload } from 'generaltranslation/types';
|
|
2
|
+
import { Settings } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Warns when an upload contains review-gated content but the project
|
|
5
|
+
* auto-approves new translations, pointing the user at the dashboard project
|
|
6
|
+
* settings page. The CLI cannot change the auto-approve setting itself —
|
|
7
|
+
* review setup happens in the dashboard.
|
|
8
|
+
*
|
|
9
|
+
* The project's auto-approve setting is fetched lazily (only when the upload
|
|
10
|
+
* actually contains review-gated content):
|
|
11
|
+
* - false: manual review is already the project workflow — no warning
|
|
12
|
+
* - true: requiresReview config has no gating effect — warn
|
|
13
|
+
* - unavailable (older API, dry runs without credentials): warn with
|
|
14
|
+
* conditional wording, since auto-approve is the platform default
|
|
15
|
+
*/
|
|
16
|
+
export declare function warnManualReviewSetup(settings: Settings, files: FileToUpload[]): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { logger } from "../console/logger.js";
|
|
2
|
+
import { gt } from "../utils/gt.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/translation/reviewSetupWarning.ts
|
|
6
|
+
const PROJECT_INFO_TIMEOUT_MS = 1e4;
|
|
7
|
+
/**
|
|
8
|
+
* Warns when an upload contains review-gated content but the project
|
|
9
|
+
* auto-approves new translations, pointing the user at the dashboard project
|
|
10
|
+
* settings page. The CLI cannot change the auto-approve setting itself —
|
|
11
|
+
* review setup happens in the dashboard.
|
|
12
|
+
*
|
|
13
|
+
* The project's auto-approve setting is fetched lazily (only when the upload
|
|
14
|
+
* actually contains review-gated content):
|
|
15
|
+
* - false: manual review is already the project workflow — no warning
|
|
16
|
+
* - true: requiresReview config has no gating effect — warn
|
|
17
|
+
* - unavailable (older API, dry runs without credentials): warn with
|
|
18
|
+
* conditional wording, since auto-approve is the platform default
|
|
19
|
+
*/
|
|
20
|
+
async function warnManualReviewSetup(settings, files) {
|
|
21
|
+
const requiresReviewPaths = settings.files?.requiresReviewPaths;
|
|
22
|
+
if (!files.some((file) => {
|
|
23
|
+
if (file.fileFormat === "GTJSON") return Object.values(file.formatMetadata ?? {}).some((metadata) => metadata?.requires_review === true);
|
|
24
|
+
return requiresReviewPaths?.has(path.resolve(process.cwd(), file.fileName));
|
|
25
|
+
})) return;
|
|
26
|
+
let autoApprove;
|
|
27
|
+
try {
|
|
28
|
+
autoApprove = (await gt.getProjectInfo({ timeout: PROJECT_INFO_TIMEOUT_MS })).autoApprove;
|
|
29
|
+
} catch {}
|
|
30
|
+
if (autoApprove === false) return;
|
|
31
|
+
const settingsUrl = settings.projectId ? `${settings.dashboardUrl}/project/${settings.projectId}/settings` : settings.dashboardUrl;
|
|
32
|
+
const message = `Some of your content requires review (requiresReview), ${autoApprove === true ? "but this project approves new translations automatically." : "but new translations are approved automatically unless auto-approval is turned off for this project."} To review translations before they are used, disable "Auto approve translations" in your project settings:`;
|
|
33
|
+
logger.warn(chalk.yellow([...wrapWords(message, 70), chalk.cyan(settingsUrl)].join("\n")));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Greedy word wrap so warning lines render at a consistent width instead of
|
|
37
|
+
* one terminal-width line followed by stubby fragments.
|
|
38
|
+
*/
|
|
39
|
+
function wrapWords(text, width) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
let line = "";
|
|
42
|
+
for (const word of text.split(" ")) if (line && line.length + 1 + word.length > width) {
|
|
43
|
+
lines.push(line);
|
|
44
|
+
line = word;
|
|
45
|
+
} else line = line ? `${line} ${word}` : word;
|
|
46
|
+
if (line) lines.push(line);
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { warnManualReviewSetup };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=reviewSetupWarning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewSetupWarning.js","names":[],"sources":["../../src/translation/reviewSetupWarning.ts"],"sourcesContent":["import path from 'node:path';\nimport chalk from 'chalk';\nimport type { FileToUpload } from 'generaltranslation/types';\nimport { logger } from '../console/logger.js';\nimport { gt } from '../utils/gt.js';\nimport { Settings } from '../types/index.js';\n\nconst PROJECT_INFO_TIMEOUT_MS = 10_000;\n\n/**\n * Warns when an upload contains review-gated content but the project\n * auto-approves new translations, pointing the user at the dashboard project\n * settings page. The CLI cannot change the auto-approve setting itself —\n * review setup happens in the dashboard.\n *\n * The project's auto-approve setting is fetched lazily (only when the upload\n * actually contains review-gated content):\n * - false: manual review is already the project workflow — no warning\n * - true: requiresReview config has no gating effect — warn\n * - unavailable (older API, dry runs without credentials): warn with\n * conditional wording, since auto-approve is the platform default\n */\nexport async function warnManualReviewSetup(\n settings: Settings,\n files: FileToUpload[]\n): Promise<void> {\n const requiresReviewPaths = settings.files?.requiresReviewPaths;\n\n const hasReviewGatedFile = files.some((file) => {\n if (file.fileFormat === 'GTJSON') {\n // Uploaded GTJSON metadata carries the platform's snake_case key\n return Object.values(file.formatMetadata ?? {}).some(\n (metadata) =>\n (metadata as { requires_review?: boolean })?.requires_review === true\n );\n }\n return requiresReviewPaths?.has(path.resolve(process.cwd(), file.fileName));\n });\n if (!hasReviewGatedFile) return;\n\n let autoApprove: boolean | undefined;\n try {\n autoApprove = (\n await gt.getProjectInfo({ timeout: PROJECT_INFO_TIMEOUT_MS })\n ).autoApprove;\n } catch {\n // Setting unavailable — fall through to the conditional wording\n }\n if (autoApprove === false) return;\n\n const settingsUrl = settings.projectId\n ? `${settings.dashboardUrl}/project/${settings.projectId}/settings`\n : settings.dashboardUrl;\n const autoApproveClause =\n autoApprove === true\n ? 'but this project approves new translations automatically.'\n : 'but new translations are approved automatically unless auto-approval is turned off for this project.';\n // \"Auto approve translations\" matches the dashboard setting title exactly.\n const message =\n `Some of your content requires review (requiresReview), ${autoApproveClause} ` +\n `To review translations before they are used, disable \"Auto approve translations\" in your project settings:`;\n logger.warn(\n chalk.yellow(\n [...wrapWords(message, 70), chalk.cyan(settingsUrl)].join('\\n')\n )\n );\n}\n\n/**\n * Greedy word wrap so warning lines render at a consistent width instead of\n * one terminal-width line followed by stubby fragments.\n */\nfunction wrapWords(text: string, width: number): string[] {\n const lines: string[] = [];\n let line = '';\n for (const word of text.split(' ')) {\n if (line && line.length + 1 + word.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = line ? `${line} ${word}` : word;\n }\n }\n if (line) lines.push(line);\n return lines;\n}\n"],"mappings":";;;;;AAOA,MAAM,0BAA0B;;;;;;;;;;;;;;AAehC,eAAsB,sBACpB,UACA,OACe;CACf,MAAM,sBAAsB,SAAS,OAAO;AAY5C,KAAI,CAVuB,MAAM,MAAM,SAAS;AAC9C,MAAI,KAAK,eAAe,SAEtB,QAAO,OAAO,OAAO,KAAK,kBAAkB,EAAE,CAAC,CAAC,MAC7C,aACE,UAA4C,oBAAoB,KACpE;AAEH,SAAO,qBAAqB,IAAI,KAAK,QAAQ,QAAQ,KAAK,EAAE,KAAK,SAAS,CAAC;GAEtD,CAAE;CAEzB,IAAI;AACJ,KAAI;AACF,iBACE,MAAM,GAAG,eAAe,EAAE,SAAS,yBAAyB,CAAC,EAC7D;SACI;AAGR,KAAI,gBAAgB,MAAO;CAE3B,MAAM,cAAc,SAAS,YACzB,GAAG,SAAS,aAAa,WAAW,SAAS,UAAU,aACvD,SAAS;CAMb,MAAM,UACJ,0DALA,gBAAgB,OACZ,8DACA,uGAGwE;AAE9E,QAAO,KACL,MAAM,OACJ,CAAC,GAAG,UAAU,SAAS,GAAG,EAAE,MAAM,KAAK,YAAY,CAAC,CAAC,KAAK,KAAK,CAChE,CACF;;;;;;AAOH,SAAS,UAAU,MAAc,OAAyB;CACxD,MAAM,QAAkB,EAAE;CAC1B,IAAI,OAAO;AACX,MAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,CAChC,KAAI,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS,OAAO;AACjD,QAAM,KAAK,KAAK;AAChB,SAAO;OAEP,QAAO,OAAO,GAAG,KAAK,GAAG,SAAS;AAGtC,KAAI,KAAM,OAAM,KAAK,KAAK;AAC1B,QAAO"}
|
package/dist/types/files.d.ts
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -143,6 +143,10 @@ export type IncludePattern = string | {
|
|
|
143
143
|
pattern: string;
|
|
144
144
|
publish?: boolean;
|
|
145
145
|
};
|
|
146
|
+
export type RequiresReviewConfig = boolean | {
|
|
147
|
+
include?: string[];
|
|
148
|
+
exclude?: string[];
|
|
149
|
+
};
|
|
146
150
|
export type FilesOptions = {
|
|
147
151
|
[K in SupportedFileExtension]?: {
|
|
148
152
|
include: IncludePattern[];
|
|
@@ -150,6 +154,7 @@ export type FilesOptions = {
|
|
|
150
154
|
transform?: string | TransformOption | TransformOption[];
|
|
151
155
|
transformationFormat?: FileFormat;
|
|
152
156
|
parsingFlags?: BaseParsingFlags;
|
|
157
|
+
requiresReview?: RequiresReviewConfig;
|
|
153
158
|
};
|
|
154
159
|
} & {
|
|
155
160
|
gt?: {
|
|
@@ -180,6 +185,7 @@ export type Settings = {
|
|
|
180
185
|
transformFormats: TransformFormats;
|
|
181
186
|
publishPaths: Set<string>;
|
|
182
187
|
unpublishPaths: Set<string>;
|
|
188
|
+
requiresReviewPaths: Set<string>;
|
|
183
189
|
parsingFlags: ParseFlagsByFileType;
|
|
184
190
|
gtJson: {
|
|
185
191
|
publish?: boolean;
|
|
@@ -192,6 +198,7 @@ export type Settings = {
|
|
|
192
198
|
};
|
|
193
199
|
};
|
|
194
200
|
stageTranslations: boolean;
|
|
201
|
+
requiresReview?: boolean;
|
|
195
202
|
publish?: boolean;
|
|
196
203
|
_versionId?: string;
|
|
197
204
|
_branchId?: string;
|
package/dist/utils/hash.d.ts
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
4
4
|
* @returns {string} The hashed string.
|
|
5
5
|
*/
|
|
6
6
|
export declare function hashStringSync(string: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Computes a file content version hash. When the file's effective
|
|
9
|
+
* requiresReview policy is true, the policy is mixed into the hash so
|
|
10
|
+
* review-policy changes produce a new version identity even for unchanged
|
|
11
|
+
* content. A false policy hashes identically to the plain content hash,
|
|
12
|
+
* preserving pre-requiresReview version IDs.
|
|
13
|
+
*/
|
|
14
|
+
export declare function hashVersionId(content: string, requiresReview: boolean): string;
|
package/dist/utils/hash.js
CHANGED
|
@@ -10,7 +10,17 @@ function hashStringSync(string) {
|
|
|
10
10
|
hash.update(string);
|
|
11
11
|
return hash.digest("hex");
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Computes a file content version hash. When the file's effective
|
|
15
|
+
* requiresReview policy is true, the policy is mixed into the hash so
|
|
16
|
+
* review-policy changes produce a new version identity even for unchanged
|
|
17
|
+
* content. A false policy hashes identically to the plain content hash,
|
|
18
|
+
* preserving pre-requiresReview version IDs.
|
|
19
|
+
*/
|
|
20
|
+
function hashVersionId(content, requiresReview) {
|
|
21
|
+
return requiresReview ? hashStringSync(content + "\0requiresReview:true") : hashStringSync(content);
|
|
22
|
+
}
|
|
13
23
|
//#endregion
|
|
14
|
-
export { hashStringSync };
|
|
24
|
+
export { hashStringSync, hashVersionId };
|
|
15
25
|
|
|
16
26
|
//# sourceMappingURL=hash.js.map
|
package/dist/utils/hash.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.js","names":[],"sources":["../../src/utils/hash.ts"],"sourcesContent":["import crypto from 'crypto';\n\n/**\n * Hashes a string using SHA-256 algorithm.\n * @param {string} string - The string to be hashed.\n * @returns {string} The hashed string.\n */\nexport function hashStringSync(string: string): string {\n const hash = crypto.createHash('sha256');\n hash.update(string);\n return hash.digest('hex');\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,eAAe,QAAwB;CACrD,MAAM,OAAO,OAAO,WAAW,SAAS;AACxC,MAAK,OAAO,OAAO;AACnB,QAAO,KAAK,OAAO,MAAM"}
|
|
1
|
+
{"version":3,"file":"hash.js","names":[],"sources":["../../src/utils/hash.ts"],"sourcesContent":["import crypto from 'crypto';\n\n/**\n * Hashes a string using SHA-256 algorithm.\n * @param {string} string - The string to be hashed.\n * @returns {string} The hashed string.\n */\nexport function hashStringSync(string: string): string {\n const hash = crypto.createHash('sha256');\n hash.update(string);\n return hash.digest('hex');\n}\n\n/**\n * Computes a file content version hash. When the file's effective\n * requiresReview policy is true, the policy is mixed into the hash so\n * review-policy changes produce a new version identity even for unchanged\n * content. A false policy hashes identically to the plain content hash,\n * preserving pre-requiresReview version IDs.\n */\nexport function hashVersionId(\n content: string,\n requiresReview: boolean\n): string {\n return requiresReview\n ? hashStringSync(content + '\\u0000requiresReview:true')\n : hashStringSync(content);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,eAAe,QAAwB;CACrD,MAAM,OAAO,OAAO,WAAW,SAAS;AACxC,MAAK,OAAO,OAAO;AACnB,QAAO,KAAK,OAAO,MAAM;;;;;;;;;AAU3B,SAAgB,cACd,SACA,gBACQ;AACR,QAAO,iBACH,eAAe,UAAU,wBAA4B,GACrD,eAAe,QAAQ"}
|
|
@@ -99,7 +99,8 @@ function prepareFileQueryData(fileVersionData, locales, branchData) {
|
|
|
99
99
|
fileName: fileVersionData[fileId].fileName,
|
|
100
100
|
fileId,
|
|
101
101
|
locale,
|
|
102
|
-
branchId: branchData.currentBranch.id
|
|
102
|
+
branchId: branchData.currentBranch.id,
|
|
103
|
+
componentCount: fileVersionData[fileId].componentCount
|
|
103
104
|
});
|
|
104
105
|
return fileQueryData;
|
|
105
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.js","names":[],"sources":["../../src/workflows/download.ts"],"sourcesContent":["import path from 'node:path';\nimport { Settings } from '../types/index.js';\nimport { gt } from '../utils/gt.js';\nimport { EnqueueFilesResult } from 'generaltranslation/types';\nimport { clearLocaleDirs } from '../fs/clearLocaleDirs.js';\nimport {\n FileStatusTracker,\n PollTranslationJobsStep,\n} from './steps/PollJobsStep.js';\nimport { DownloadTranslationsStep } from './steps/DownloadStep.js';\nimport { BranchData } from '../types/branch.js';\nimport { branchResolutionError } from '../console/index.js';\nimport { logErrorAndExit } from '../console/logging.js';\nimport { logger } from '../console/logger.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport { BranchStep } from './steps/BranchStep.js';\nimport { FileProperties } from '../types/files.js';\nimport chalk from 'chalk';\n\nexport type FileTranslationData = {\n [fileId: string]: {\n versionId: string;\n fileName: string;\n };\n};\n\n/**\n * Checks the status of translations and downloads them using a workflow pattern\n * @param fileVersionData - Mapping of file IDs to their version and name information\n * @param jobData - Optional job data from enqueue operation\n * @param locales - The locales to wait for\n * @param timeoutDuration - The timeout duration for the wait in seconds\n * @param resolveOutputPath - Function to resolve the output path for a given source path and locale\n * @param options - Settings configuration\n * @param forceRetranslation - Whether to force retranslation\n * @param forceDownload - Whether to force download even if file exists\n * @returns True if all translations are downloaded successfully, false otherwise\n */\nexport async function runDownloadWorkflow({\n fileVersionData,\n jobData,\n branchData,\n locales,\n timeoutDuration,\n resolveOutputPath,\n options,\n forceRetranslation,\n forceDownload,\n}: {\n fileVersionData: FileTranslationData;\n jobData: EnqueueFilesResult | undefined;\n branchData: BranchData | undefined;\n locales: string[];\n timeoutDuration: number;\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n options: Settings;\n forceRetranslation?: boolean;\n forceDownload?: boolean;\n}): Promise<boolean> {\n if (!branchData) {\n // Run the branch step\n const branchStep = new BranchStep(gt, options);\n const branchResult = await branchStep.run();\n await branchStep.wait();\n if (!branchResult) {\n return logErrorAndExit(branchResolutionError);\n }\n branchData = branchResult;\n }\n // readLockfile uses _branchId to select branch-specific lockfile entries.\n // Keep it on a scoped copy so the caller's settings object is not mutated.\n const settingsForBranch: Settings = {\n ...options,\n _branchId: branchData.currentBranch.id,\n };\n // Prepare the query data\n const fileQueryData = prepareFileQueryData(\n fileVersionData,\n locales,\n branchData\n );\n\n // Initialize download status\n const fileTracker: FileStatusTracker = {\n completed: new Map<string, FileProperties>(),\n inProgress: new Map<string, FileProperties>(),\n failed: new Map<string, FileProperties>(),\n skipped: new Map<string, FileProperties>(),\n };\n\n // Step 1: Poll translation jobs if jobData exists\n let pollTimedOut = false;\n if (jobData) {\n const pollStep = new PollTranslationJobsStep(gt);\n const pollResult = await pollStep.run({\n fileTracker,\n fileQueryData,\n jobData,\n timeoutDuration,\n forceRetranslation,\n });\n await pollStep.wait();\n\n if (pollResult.fileTracker.failed.size > 0) {\n logger.error(\n `${chalk.red(`${pollResult.fileTracker.failed.size} file(s) failed to translate:`)}\\n${Array.from(\n pollResult.fileTracker.failed.entries()\n )\n .map(([, value]) => `- ${value.fileName}`)\n .join('\\n')}`\n );\n for (const [, value] of pollResult.fileTracker.failed) {\n recordWarning(\n 'failed_translation',\n value.fileName,\n `Failed to translate for locale ${value.locale}`\n );\n }\n\n // If all files failed translation, exit early\n if (pollResult.fileTracker.completed.size === 0) {\n return false;\n }\n }\n\n // Even if polling timed out, still download whatever completed successfully\n if (!pollResult.success) {\n pollTimedOut = true;\n if (pollResult.fileTracker.completed.size > 0) {\n logger.warn(\n chalk.yellow(\n `Timed out, but ${pollResult.fileTracker.completed.size} translation(s) completed successfully. Downloading completed files...`\n )\n );\n } else {\n return false;\n }\n }\n } else {\n for (const file of fileQueryData) {\n // Staging - assume all files are completed\n fileTracker.completed.set(\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file\n );\n }\n }\n\n await clearCompletedLocaleDirs({\n fileTracker,\n locales,\n resolveOutputPath,\n options,\n });\n\n // Step 2: Download translations\n const downloadStep = new DownloadTranslationsStep(gt, settingsForBranch);\n const downloadResult = await downloadStep.run({\n fileTracker,\n resolveOutputPath,\n forceDownload,\n });\n await downloadStep.wait();\n\n // If polling timed out, report failure even though we downloaded what we could\n if (pollTimedOut) {\n return false;\n }\n\n return downloadResult;\n}\n\nasync function clearCompletedLocaleDirs({\n fileTracker,\n locales,\n resolveOutputPath,\n options,\n}: {\n fileTracker: FileStatusTracker;\n locales: string[];\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n options: Settings;\n}): Promise<void> {\n if (\n options.options?.experimentalClearLocaleDirs !== true ||\n fileTracker.completed.size === 0\n ) {\n return;\n }\n\n const translatedFiles = new Set(\n Array.from(fileTracker.completed.values())\n .map((file) => {\n const outputPath = resolveOutputPath(file.fileName, file.locale);\n // Only clear if the output path is different from the source.\n return outputPath !== null && outputPath !== file.fileName\n ? outputPath\n : null;\n })\n .filter((filePath): filePath is string => filePath !== null)\n );\n\n if (translatedFiles.size === 0) return;\n\n const cwd = path.dirname(options.config);\n\n await clearLocaleDirs(\n translatedFiles,\n locales,\n options.options?.clearLocaleDirsExclude,\n cwd\n );\n}\n\n/**\n * Prepares the file query data from input data and locales\n */\nfunction prepareFileQueryData(\n fileVersionData: FileTranslationData,\n locales: string[],\n branchData: BranchData\n): FileProperties[] {\n const fileQueryData: FileProperties[] = [];\n\n for (const fileId in fileVersionData) {\n for (const locale of locales) {\n fileQueryData.push({\n versionId: fileVersionData[fileId].versionId,\n fileName: fileVersionData[fileId].fileName,\n fileId,\n locale,\n branchId: branchData.currentBranch.id,\n });\n }\n }\n\n return fileQueryData;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"download.js","names":[],"sources":["../../src/workflows/download.ts"],"sourcesContent":["import path from 'node:path';\nimport { Settings } from '../types/index.js';\nimport { gt } from '../utils/gt.js';\nimport { EnqueueFilesResult } from 'generaltranslation/types';\nimport { clearLocaleDirs } from '../fs/clearLocaleDirs.js';\nimport {\n FileStatusTracker,\n PollTranslationJobsStep,\n} from './steps/PollJobsStep.js';\nimport { DownloadTranslationsStep } from './steps/DownloadStep.js';\nimport { BranchData } from '../types/branch.js';\nimport { branchResolutionError } from '../console/index.js';\nimport { logErrorAndExit } from '../console/logging.js';\nimport { logger } from '../console/logger.js';\nimport { recordWarning } from '../state/translateWarnings.js';\nimport { BranchStep } from './steps/BranchStep.js';\nimport { FileProperties } from '../types/files.js';\nimport chalk from 'chalk';\n\nexport type FileTranslationData = {\n [fileId: string]: {\n versionId: string;\n fileName: string;\n /** Source component count for GTJSON files */\n componentCount?: number;\n };\n};\n\n/**\n * Checks the status of translations and downloads them using a workflow pattern\n * @param fileVersionData - Mapping of file IDs to their version and name information\n * @param jobData - Optional job data from enqueue operation\n * @param locales - The locales to wait for\n * @param timeoutDuration - The timeout duration for the wait in seconds\n * @param resolveOutputPath - Function to resolve the output path for a given source path and locale\n * @param options - Settings configuration\n * @param forceRetranslation - Whether to force retranslation\n * @param forceDownload - Whether to force download even if file exists\n * @returns True if all translations are downloaded successfully, false otherwise\n */\nexport async function runDownloadWorkflow({\n fileVersionData,\n jobData,\n branchData,\n locales,\n timeoutDuration,\n resolveOutputPath,\n options,\n forceRetranslation,\n forceDownload,\n}: {\n fileVersionData: FileTranslationData;\n jobData: EnqueueFilesResult | undefined;\n branchData: BranchData | undefined;\n locales: string[];\n timeoutDuration: number;\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n options: Settings;\n forceRetranslation?: boolean;\n forceDownload?: boolean;\n}): Promise<boolean> {\n if (!branchData) {\n // Run the branch step\n const branchStep = new BranchStep(gt, options);\n const branchResult = await branchStep.run();\n await branchStep.wait();\n if (!branchResult) {\n return logErrorAndExit(branchResolutionError);\n }\n branchData = branchResult;\n }\n // readLockfile uses _branchId to select branch-specific lockfile entries.\n // Keep it on a scoped copy so the caller's settings object is not mutated.\n const settingsForBranch: Settings = {\n ...options,\n _branchId: branchData.currentBranch.id,\n };\n // Prepare the query data\n const fileQueryData = prepareFileQueryData(\n fileVersionData,\n locales,\n branchData\n );\n\n // Initialize download status\n const fileTracker: FileStatusTracker = {\n completed: new Map<string, FileProperties>(),\n inProgress: new Map<string, FileProperties>(),\n failed: new Map<string, FileProperties>(),\n skipped: new Map<string, FileProperties>(),\n };\n\n // Step 1: Poll translation jobs if jobData exists\n let pollTimedOut = false;\n if (jobData) {\n const pollStep = new PollTranslationJobsStep(gt);\n const pollResult = await pollStep.run({\n fileTracker,\n fileQueryData,\n jobData,\n timeoutDuration,\n forceRetranslation,\n });\n await pollStep.wait();\n\n if (pollResult.fileTracker.failed.size > 0) {\n logger.error(\n `${chalk.red(`${pollResult.fileTracker.failed.size} file(s) failed to translate:`)}\\n${Array.from(\n pollResult.fileTracker.failed.entries()\n )\n .map(([, value]) => `- ${value.fileName}`)\n .join('\\n')}`\n );\n for (const [, value] of pollResult.fileTracker.failed) {\n recordWarning(\n 'failed_translation',\n value.fileName,\n `Failed to translate for locale ${value.locale}`\n );\n }\n\n // If all files failed translation, exit early\n if (pollResult.fileTracker.completed.size === 0) {\n return false;\n }\n }\n\n // Even if polling timed out, still download whatever completed successfully\n if (!pollResult.success) {\n pollTimedOut = true;\n if (pollResult.fileTracker.completed.size > 0) {\n logger.warn(\n chalk.yellow(\n `Timed out, but ${pollResult.fileTracker.completed.size} translation(s) completed successfully. Downloading completed files...`\n )\n );\n } else {\n return false;\n }\n }\n } else {\n for (const file of fileQueryData) {\n // Staging - assume all files are completed\n fileTracker.completed.set(\n `${file.branchId}:${file.fileId}:${file.versionId}:${file.locale}`,\n file\n );\n }\n }\n\n await clearCompletedLocaleDirs({\n fileTracker,\n locales,\n resolveOutputPath,\n options,\n });\n\n // Step 2: Download translations\n const downloadStep = new DownloadTranslationsStep(gt, settingsForBranch);\n const downloadResult = await downloadStep.run({\n fileTracker,\n resolveOutputPath,\n forceDownload,\n });\n await downloadStep.wait();\n\n // If polling timed out, report failure even though we downloaded what we could\n if (pollTimedOut) {\n return false;\n }\n\n return downloadResult;\n}\n\nasync function clearCompletedLocaleDirs({\n fileTracker,\n locales,\n resolveOutputPath,\n options,\n}: {\n fileTracker: FileStatusTracker;\n locales: string[];\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n options: Settings;\n}): Promise<void> {\n if (\n options.options?.experimentalClearLocaleDirs !== true ||\n fileTracker.completed.size === 0\n ) {\n return;\n }\n\n const translatedFiles = new Set(\n Array.from(fileTracker.completed.values())\n .map((file) => {\n const outputPath = resolveOutputPath(file.fileName, file.locale);\n // Only clear if the output path is different from the source.\n return outputPath !== null && outputPath !== file.fileName\n ? outputPath\n : null;\n })\n .filter((filePath): filePath is string => filePath !== null)\n );\n\n if (translatedFiles.size === 0) return;\n\n const cwd = path.dirname(options.config);\n\n await clearLocaleDirs(\n translatedFiles,\n locales,\n options.options?.clearLocaleDirsExclude,\n cwd\n );\n}\n\n/**\n * Prepares the file query data from input data and locales\n */\nfunction prepareFileQueryData(\n fileVersionData: FileTranslationData,\n locales: string[],\n branchData: BranchData\n): FileProperties[] {\n const fileQueryData: FileProperties[] = [];\n\n for (const fileId in fileVersionData) {\n for (const locale of locales) {\n fileQueryData.push({\n versionId: fileVersionData[fileId].versionId,\n fileName: fileVersionData[fileId].fileName,\n fileId,\n locale,\n branchId: branchData.currentBranch.id,\n componentCount: fileVersionData[fileId].componentCount,\n });\n }\n }\n\n return fileQueryData;\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { logger } from "../../console/logger.js";
|
|
2
|
+
import "../../utils/constants.js";
|
|
2
3
|
import { WorkflowStep } from "./WorkflowStep.js";
|
|
3
4
|
import { recordWarning } from "../../state/translateWarnings.js";
|
|
4
5
|
import { downloadFileBatch } from "../../api/downloadFileBatch.js";
|
|
5
6
|
import chalk from "chalk";
|
|
7
|
+
import path from "node:path";
|
|
6
8
|
//#region src/workflows/steps/DownloadStep.ts
|
|
7
9
|
var DownloadTranslationsStep = class extends WorkflowStep {
|
|
8
10
|
spinner = null;
|
|
@@ -37,6 +39,24 @@ var DownloadTranslationsStep = class extends WorkflowStep {
|
|
|
37
39
|
logger.warn(`Failed to download ${missing.length} file(s):\n${missing.map((f) => `- ${f.fileName} (${f.locale})`).join("\n")}`);
|
|
38
40
|
for (const f of missing) recordWarning("failed_download", f.fileName, `Failed to download for locale ${f.locale}`);
|
|
39
41
|
}
|
|
42
|
+
const requiresReviewPaths = this.settings.files?.requiresReviewPaths ?? /* @__PURE__ */ new Set();
|
|
43
|
+
const reviewGatedKeys = /* @__PURE__ */ new Set();
|
|
44
|
+
if (requiresReviewPaths.size > 0) {
|
|
45
|
+
for (const translation of readyTranslations) {
|
|
46
|
+
if (translation.approvedAt !== null) continue;
|
|
47
|
+
const fileKey = `${translation.branchId}:${translation.fileId}:${translation.versionId}:${translation.locale}`;
|
|
48
|
+
const fileProperties = fileTracker.completed.get(fileKey);
|
|
49
|
+
if (!fileProperties) continue;
|
|
50
|
+
if (fileProperties.fileName === "__INTERNAL_GT_TEMPLATE_NAME__") continue;
|
|
51
|
+
const absolutePath = path.resolve(process.cwd(), fileProperties.fileName);
|
|
52
|
+
if (!requiresReviewPaths.has(absolutePath)) continue;
|
|
53
|
+
reviewGatedKeys.add(fileKey);
|
|
54
|
+
fileTracker.completed.delete(fileKey);
|
|
55
|
+
fileTracker.skipped.set(fileKey, fileProperties);
|
|
56
|
+
recordWarning("pending_review", fileProperties.fileName, `Translation for locale ${translation.locale} requires review and is not approved yet`);
|
|
57
|
+
}
|
|
58
|
+
if (reviewGatedKeys.size > 0) logger.info(`Skipped ${reviewGatedKeys.size} file(s) awaiting review approval. They will download once approved.`);
|
|
59
|
+
}
|
|
40
60
|
const batchFiles = readyTranslations.map((translation) => {
|
|
41
61
|
const fileKey = `${translation.branchId}:${translation.fileId}:${translation.versionId}:${translation.locale}`;
|
|
42
62
|
const fileProperties = fileTracker.completed.get(fileKey);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DownloadStep.js","names":[],"sources":["../../../src/workflows/steps/DownloadStep.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { WorkflowStep } from './WorkflowStep.js';\nimport { logger } from '../../console/logger.js';\nimport {\n BatchedFiles,\n downloadFileBatch,\n DownloadFileBatchResult,\n} from '../../api/downloadFileBatch.js';\nimport { GT } from 'generaltranslation';\nimport { Settings } from '../../types/index.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\nimport { FileStatusTracker } from './PollJobsStep.js';\n\nexport type DownloadTranslationsInput = {\n fileTracker: FileStatusTracker;\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n forceDownload?: boolean;\n};\n\nexport class DownloadTranslationsStep extends WorkflowStep<\n DownloadTranslationsInput,\n boolean\n> {\n private spinner: ReturnType<typeof logger.createProgressBar> | null = null;\n\n constructor(\n private gt: GT,\n private settings: Settings\n ) {\n super();\n }\n\n async run({\n fileTracker,\n resolveOutputPath,\n forceDownload,\n }: DownloadTranslationsInput): Promise<boolean> {\n this.spinner = logger.createProgressBar(fileTracker.completed.size);\n this.spinner.start('Downloading files...');\n\n return this.downloadFiles(fileTracker, resolveOutputPath, forceDownload);\n }\n\n private async downloadFiles(\n fileTracker: FileStatusTracker,\n resolveOutputPath: (sourcePath: string, locale: string) => string | null,\n forceDownload?: boolean\n ): Promise<boolean> {\n try {\n // Only download files that are marked as completed\n const currentQueryData = Array.from(fileTracker.completed.values());\n\n // If no files to download, we're done\n if (currentQueryData.length === 0) {\n this.spinner?.stop(chalk.green('No files to download'));\n return true;\n }\n\n // Check for translations\n const responseData = await this.gt.queryFileData({\n translatedFiles: currentQueryData.map((item) => ({\n fileId: item.fileId,\n versionId: item.versionId,\n branchId: item.branchId,\n locale: item.locale,\n })),\n });\n const translatedFiles = responseData.translatedFiles || [];\n\n // Filter for ready translations\n const readyTranslations = translatedFiles.filter(\n (file) => file.completedAt !== null\n );\n let missingCount = 0;\n\n if (readyTranslations.length < currentQueryData.length) {\n const readyKeys = new Set(\n readyTranslations.map(\n (t) => `${t.branchId}:${t.fileId}:${t.versionId}:${t.locale}`\n )\n );\n const missing = currentQueryData.filter(\n (item) =>\n !readyKeys.has(\n `${item.branchId}:${item.fileId}:${item.versionId}:${item.locale}`\n )\n );\n missingCount = missing.length;\n logger.warn(\n `Failed to download ${missing.length} file(s):\\n${missing.map((f) => `- ${f.fileName} (${f.locale})`).join('\\n')}`\n );\n for (const f of missing) {\n recordWarning(\n 'failed_download',\n f.fileName,\n `Failed to download for locale ${f.locale}`\n );\n }\n }\n\n // Prepare batch download data\n const batchFiles: BatchedFiles = readyTranslations\n .map((translation) => {\n const fileKey = `${translation.branchId}:${translation.fileId}:${translation.versionId}:${translation.locale}`;\n\n const fileProperties = fileTracker.completed.get(fileKey);\n if (!fileProperties) {\n return null;\n }\n const outputPath = resolveOutputPath(\n fileProperties.fileName,\n translation.locale\n );\n\n // Skip downloading GTJSON files that are not in the files configuration\n if (outputPath === null) {\n fileTracker.completed.delete(fileKey);\n fileTracker.skipped.set(fileKey, fileProperties);\n return null;\n }\n return {\n branchId: translation.branchId,\n fileId: translation.fileId,\n versionId: translation.versionId,\n locale: translation.locale,\n inputPath: fileProperties.fileName,\n outputPath,\n };\n })\n .filter((file) => file !== null) as BatchedFiles;\n\n if (batchFiles.length > 0) {\n const batchResult = await this.downloadFilesWithRetry(\n fileTracker,\n batchFiles,\n forceDownload\n );\n this.spinner?.stop(\n chalk.green(\n `Downloaded ${batchResult.successful.length} files${batchResult.skipped.length > 0 ? `, skipped ${batchResult.skipped.length} files` : ''}`\n )\n );\n if (batchResult.failed.length > 0) {\n logger.warn(\n `Failed to download ${batchResult.failed.length} files: ${batchResult.failed.map((f) => f.inputPath).join('\\n')}`\n );\n for (const f of batchResult.failed) {\n recordWarning(\n 'failed_download',\n f.inputPath,\n `Failed to download for locale ${f.locale}`\n );\n }\n }\n } else if (missingCount > 0) {\n this.spinner?.stop(\n chalk.yellow('No files downloaded - see warnings above')\n );\n } else {\n this.spinner?.stop(chalk.green('No files to download'));\n }\n\n return true;\n } catch (error) {\n this.spinner?.stop(\n chalk.red('An error occurred while downloading translations')\n );\n logger.error(chalk.red('Error: ') + error);\n return false;\n }\n }\n\n private async downloadFilesWithRetry(\n fileTracker: FileStatusTracker,\n files: BatchedFiles,\n forceDownload?: boolean,\n maxRetries: number = 3,\n initialDelay: number = 1000\n ): Promise<DownloadFileBatchResult> {\n let remainingFiles = files;\n let allSuccessful: BatchedFiles = [];\n let retryCount = 0;\n let allSkipped: BatchedFiles = [];\n while (remainingFiles.length > 0 && retryCount <= maxRetries) {\n const batchResult = await downloadFileBatch(\n fileTracker,\n remainingFiles,\n this.settings,\n forceDownload\n );\n\n allSuccessful = [...allSuccessful, ...batchResult.successful];\n allSkipped = [...allSkipped, ...batchResult.skipped];\n\n this.spinner?.advance(\n batchResult.successful.length +\n batchResult.skipped.length +\n batchResult.failed.length\n );\n\n // If no failures or we've exhausted retries, we're done\n if (batchResult.failed.length === 0 || retryCount === maxRetries) {\n return {\n successful: allSuccessful,\n failed: batchResult.failed,\n skipped: allSkipped,\n };\n }\n\n // Calculate exponential backoff delay\n const delay = initialDelay * Math.pow(2, retryCount);\n logger.error(\n chalk.yellow(\n `Retrying ${batchResult.failed.length} failed file(s) in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`\n )\n );\n\n // Wait before retrying\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n remainingFiles = batchResult.failed;\n retryCount++;\n }\n\n return {\n successful: allSuccessful,\n failed: remainingFiles,\n skipped: allSkipped,\n };\n }\n\n async wait(): Promise<void> {\n return;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DownloadStep.js","names":[],"sources":["../../../src/workflows/steps/DownloadStep.ts"],"sourcesContent":["import chalk from 'chalk';\nimport path from 'node:path';\nimport { WorkflowStep } from './WorkflowStep.js';\nimport { logger } from '../../console/logger.js';\nimport {\n BatchedFiles,\n downloadFileBatch,\n DownloadFileBatchResult,\n} from '../../api/downloadFileBatch.js';\nimport { GT } from 'generaltranslation';\nimport { Settings } from '../../types/index.js';\nimport { recordWarning } from '../../state/translateWarnings.js';\nimport { FileStatusTracker } from './PollJobsStep.js';\nimport { TEMPLATE_FILE_NAME } from '../../utils/constants.js';\n\nexport type DownloadTranslationsInput = {\n fileTracker: FileStatusTracker;\n resolveOutputPath: (sourcePath: string, locale: string) => string | null;\n forceDownload?: boolean;\n};\n\nexport class DownloadTranslationsStep extends WorkflowStep<\n DownloadTranslationsInput,\n boolean\n> {\n private spinner: ReturnType<typeof logger.createProgressBar> | null = null;\n\n constructor(\n private gt: GT,\n private settings: Settings\n ) {\n super();\n }\n\n async run({\n fileTracker,\n resolveOutputPath,\n forceDownload,\n }: DownloadTranslationsInput): Promise<boolean> {\n this.spinner = logger.createProgressBar(fileTracker.completed.size);\n this.spinner.start('Downloading files...');\n\n return this.downloadFiles(fileTracker, resolveOutputPath, forceDownload);\n }\n\n private async downloadFiles(\n fileTracker: FileStatusTracker,\n resolveOutputPath: (sourcePath: string, locale: string) => string | null,\n forceDownload?: boolean\n ): Promise<boolean> {\n try {\n // Only download files that are marked as completed\n const currentQueryData = Array.from(fileTracker.completed.values());\n\n // If no files to download, we're done\n if (currentQueryData.length === 0) {\n this.spinner?.stop(chalk.green('No files to download'));\n return true;\n }\n\n // Check for translations\n const responseData = await this.gt.queryFileData({\n translatedFiles: currentQueryData.map((item) => ({\n fileId: item.fileId,\n versionId: item.versionId,\n branchId: item.branchId,\n locale: item.locale,\n })),\n });\n const translatedFiles = responseData.translatedFiles || [];\n\n // Filter for ready translations\n const readyTranslations = translatedFiles.filter(\n (file) => file.completedAt !== null\n );\n let missingCount = 0;\n\n if (readyTranslations.length < currentQueryData.length) {\n const readyKeys = new Set(\n readyTranslations.map(\n (t) => `${t.branchId}:${t.fileId}:${t.versionId}:${t.locale}`\n )\n );\n const missing = currentQueryData.filter(\n (item) =>\n !readyKeys.has(\n `${item.branchId}:${item.fileId}:${item.versionId}:${item.locale}`\n )\n );\n missingCount = missing.length;\n logger.warn(\n `Failed to download ${missing.length} file(s):\\n${missing.map((f) => `- ${f.fileName} (${f.locale})`).join('\\n')}`\n );\n for (const f of missing) {\n recordWarning(\n 'failed_download',\n f.fileName,\n `Failed to download for locale ${f.locale}`\n );\n }\n }\n\n // Review gating: skip completed translations for normal files whose\n // effective requiresReview policy is true but that are not approved\n // yet. Skips are intentional, not failures — a review-gated file is\n // simply absent locally and falls back to source behavior.\n // GTJSON is exempt: it is always requested, and the platform filters\n // unapproved components out of the served content itself.\n const requiresReviewPaths =\n this.settings.files?.requiresReviewPaths ?? new Set<string>();\n const reviewGatedKeys = new Set<string>();\n if (requiresReviewPaths.size > 0) {\n for (const translation of readyTranslations) {\n if (translation.approvedAt !== null) continue;\n const fileKey = `${translation.branchId}:${translation.fileId}:${translation.versionId}:${translation.locale}`;\n const fileProperties = fileTracker.completed.get(fileKey);\n if (!fileProperties) continue;\n if (fileProperties.fileName === TEMPLATE_FILE_NAME) continue;\n const absolutePath = path.resolve(\n process.cwd(),\n fileProperties.fileName\n );\n if (!requiresReviewPaths.has(absolutePath)) continue;\n reviewGatedKeys.add(fileKey);\n fileTracker.completed.delete(fileKey);\n fileTracker.skipped.set(fileKey, fileProperties);\n recordWarning(\n 'pending_review',\n fileProperties.fileName,\n `Translation for locale ${translation.locale} requires review and is not approved yet`\n );\n }\n if (reviewGatedKeys.size > 0) {\n logger.info(\n `Skipped ${reviewGatedKeys.size} file(s) awaiting review approval. They will download once approved.`\n );\n }\n }\n\n // Prepare batch download data\n const batchFiles: BatchedFiles = readyTranslations\n .map((translation) => {\n const fileKey = `${translation.branchId}:${translation.fileId}:${translation.versionId}:${translation.locale}`;\n\n const fileProperties = fileTracker.completed.get(fileKey);\n if (!fileProperties) {\n return null;\n }\n const outputPath = resolveOutputPath(\n fileProperties.fileName,\n translation.locale\n );\n\n // Skip downloading GTJSON files that are not in the files configuration\n if (outputPath === null) {\n fileTracker.completed.delete(fileKey);\n fileTracker.skipped.set(fileKey, fileProperties);\n return null;\n }\n return {\n branchId: translation.branchId,\n fileId: translation.fileId,\n versionId: translation.versionId,\n locale: translation.locale,\n inputPath: fileProperties.fileName,\n outputPath,\n };\n })\n .filter((file) => file !== null) as BatchedFiles;\n\n if (batchFiles.length > 0) {\n const batchResult = await this.downloadFilesWithRetry(\n fileTracker,\n batchFiles,\n forceDownload\n );\n this.spinner?.stop(\n chalk.green(\n `Downloaded ${batchResult.successful.length} files${batchResult.skipped.length > 0 ? `, skipped ${batchResult.skipped.length} files` : ''}`\n )\n );\n if (batchResult.failed.length > 0) {\n logger.warn(\n `Failed to download ${batchResult.failed.length} files: ${batchResult.failed.map((f) => f.inputPath).join('\\n')}`\n );\n for (const f of batchResult.failed) {\n recordWarning(\n 'failed_download',\n f.inputPath,\n `Failed to download for locale ${f.locale}`\n );\n }\n }\n } else if (missingCount > 0) {\n this.spinner?.stop(\n chalk.yellow('No files downloaded - see warnings above')\n );\n } else {\n this.spinner?.stop(chalk.green('No files to download'));\n }\n\n return true;\n } catch (error) {\n this.spinner?.stop(\n chalk.red('An error occurred while downloading translations')\n );\n logger.error(chalk.red('Error: ') + error);\n return false;\n }\n }\n\n private async downloadFilesWithRetry(\n fileTracker: FileStatusTracker,\n files: BatchedFiles,\n forceDownload?: boolean,\n maxRetries: number = 3,\n initialDelay: number = 1000\n ): Promise<DownloadFileBatchResult> {\n let remainingFiles = files;\n let allSuccessful: BatchedFiles = [];\n let retryCount = 0;\n let allSkipped: BatchedFiles = [];\n while (remainingFiles.length > 0 && retryCount <= maxRetries) {\n const batchResult = await downloadFileBatch(\n fileTracker,\n remainingFiles,\n this.settings,\n forceDownload\n );\n\n allSuccessful = [...allSuccessful, ...batchResult.successful];\n allSkipped = [...allSkipped, ...batchResult.skipped];\n\n this.spinner?.advance(\n batchResult.successful.length +\n batchResult.skipped.length +\n batchResult.failed.length\n );\n\n // If no failures or we've exhausted retries, we're done\n if (batchResult.failed.length === 0 || retryCount === maxRetries) {\n return {\n successful: allSuccessful,\n failed: batchResult.failed,\n skipped: allSkipped,\n };\n }\n\n // Calculate exponential backoff delay\n const delay = initialDelay * Math.pow(2, retryCount);\n logger.error(\n chalk.yellow(\n `Retrying ${batchResult.failed.length} failed file(s) in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`\n )\n );\n\n // Wait before retrying\n await new Promise((resolve) => setTimeout(resolve, delay));\n\n remainingFiles = batchResult.failed;\n retryCount++;\n }\n\n return {\n successful: allSuccessful,\n failed: remainingFiles,\n skipped: allSkipped,\n };\n }\n\n async wait(): Promise<void> {\n return;\n }\n}\n"],"mappings":"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"}
|
|
@@ -26,7 +26,6 @@ var EnqueueStep = class extends WorkflowStep {
|
|
|
26
26
|
this.result = await this.gt.enqueueFiles(files, {
|
|
27
27
|
sourceLocale: this.settings.defaultLocale,
|
|
28
28
|
targetLocales: this.settings.locales,
|
|
29
|
-
requireApproval: this.settings.stageTranslations,
|
|
30
29
|
modelProvider: this.settings.modelProvider,
|
|
31
30
|
force: this.force
|
|
32
31
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnqueueStep.js","names":[],"sources":["../../../src/workflows/steps/EnqueueStep.ts"],"sourcesContent":["import type { EnqueueFilesResult } from 'generaltranslation/types';\nimport { WorkflowStep } from './WorkflowStep.js';\nimport { logger } from '../../console/logger.js';\nimport { GT } from 'generaltranslation';\nimport { Settings } from '../../types/index.js';\nimport type { FileReference } from 'generaltranslation/types';\nimport chalk from 'chalk';\n\nexport class EnqueueStep extends WorkflowStep<\n FileReference[],\n EnqueueFilesResult\n> {\n private spinner = logger.createSpinner('dots');\n private result: EnqueueFilesResult | null = null;\n private spinnerStarted = false;\n\n constructor(\n private gt: GT,\n private settings: Settings,\n private force?: boolean\n ) {\n super();\n }\n\n async run(files: FileReference[]): Promise<EnqueueFilesResult> {\n if (files.length === 0) {\n this.result = {\n jobData: {},\n locales: this.settings.locales,\n message: 'No files need to be enqueued',\n };\n return this.result;\n }\n\n this.spinner.start('Enqueuing translations...');\n this.spinnerStarted = true;\n\n this.result = await this.gt.enqueueFiles(files, {\n sourceLocale: this.settings.defaultLocale,\n targetLocales: this.settings.locales,\n
|
|
1
|
+
{"version":3,"file":"EnqueueStep.js","names":[],"sources":["../../../src/workflows/steps/EnqueueStep.ts"],"sourcesContent":["import type { EnqueueFilesResult } from 'generaltranslation/types';\nimport { WorkflowStep } from './WorkflowStep.js';\nimport { logger } from '../../console/logger.js';\nimport { GT } from 'generaltranslation';\nimport { Settings } from '../../types/index.js';\nimport type { FileReference } from 'generaltranslation/types';\nimport chalk from 'chalk';\n\nexport class EnqueueStep extends WorkflowStep<\n FileReference[],\n EnqueueFilesResult\n> {\n private spinner = logger.createSpinner('dots');\n private result: EnqueueFilesResult | null = null;\n private spinnerStarted = false;\n\n constructor(\n private gt: GT,\n private settings: Settings,\n private force?: boolean\n ) {\n super();\n }\n\n async run(files: FileReference[]): Promise<EnqueueFilesResult> {\n if (files.length === 0) {\n this.result = {\n jobData: {},\n locales: this.settings.locales,\n message: 'No files need to be enqueued',\n };\n return this.result;\n }\n\n this.spinner.start('Enqueuing translations...');\n this.spinnerStarted = true;\n\n this.result = await this.gt.enqueueFiles(files, {\n sourceLocale: this.settings.defaultLocale,\n targetLocales: this.settings.locales,\n modelProvider: this.settings.modelProvider,\n force: this.force,\n });\n\n return this.result;\n }\n\n async wait(): Promise<void> {\n if (this.result && this.spinnerStarted) {\n this.spinner.stop(chalk.green('Translations enqueued successfully'));\n }\n }\n}\n"],"mappings":";;;;AAQA,IAAa,cAAb,cAAiC,aAG/B;CACA,UAAkB,OAAO,cAAc,OAAO;CAC9C,SAA4C;CAC5C,iBAAyB;CAEzB,YACE,IACA,UACA,OACA;AACA,SAAO;AAJC,OAAA,KAAA;AACA,OAAA,WAAA;AACA,OAAA,QAAA;;CAKV,MAAM,IAAI,OAAqD;AAC7D,MAAI,MAAM,WAAW,GAAG;AACtB,QAAK,SAAS;IACZ,SAAS,EAAE;IACX,SAAS,KAAK,SAAS;IACvB,SAAS;IACV;AACD,UAAO,KAAK;;AAGd,OAAK,QAAQ,MAAM,4BAA4B;AAC/C,OAAK,iBAAiB;AAEtB,OAAK,SAAS,MAAM,KAAK,GAAG,aAAa,OAAO;GAC9C,cAAc,KAAK,SAAS;GAC5B,eAAe,KAAK,SAAS;GAC7B,eAAe,KAAK,SAAS;GAC7B,OAAO,KAAK;GACb,CAAC;AAEF,SAAO,KAAK;;CAGd,MAAM,OAAsB;AAC1B,MAAI,KAAK,UAAU,KAAK,eACtB,MAAK,QAAQ,KAAK,MAAM,MAAM,qCAAqC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gt",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.58",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "bin/main.js",
|
|
6
6
|
"files": [
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
"unist-util-visit": "^5.0.0",
|
|
115
115
|
"yaml": "^2.8.0",
|
|
116
116
|
"@generaltranslation/format": "0.1.1",
|
|
117
|
-
"@generaltranslation/python-extractor": "0.2.
|
|
118
|
-
"@generaltranslation/supported-locales": "2.1.
|
|
119
|
-
"generaltranslation": "8.2.
|
|
117
|
+
"@generaltranslation/python-extractor": "0.2.25",
|
|
118
|
+
"@generaltranslation/supported-locales": "2.1.5",
|
|
119
|
+
"generaltranslation": "8.2.19",
|
|
120
120
|
"gt-remark": "1.0.10"
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|