From 4b90782ae00a2a2516a4be71592d3cb29b26341a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 20 Nov 2023 23:23:53 +0800 Subject: [PATCH] feat(cli): support sync array keys (#4903) * feat(cli): support sync array keys * refactor(cli): update translate sync concurrency to 5 * chore: add changeset --- .changeset/empty-buttons-ring.md | 7 ++ .../src/commands/translate/sync-keys/utils.ts | 103 ++++++++++++------ packages/cli/src/commands/translate/sync.ts | 2 +- 3 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 .changeset/empty-buttons-ring.md diff --git a/.changeset/empty-buttons-ring.md b/.changeset/empty-buttons-ring.md new file mode 100644 index 000000000..9299f4a92 --- /dev/null +++ b/.changeset/empty-buttons-ring.md @@ -0,0 +1,7 @@ +--- +"@logto/cli": minor +--- + +support sync phrases array keys and update concurrency to 5 + +as the new model works with more concurrency. diff --git a/packages/cli/src/commands/translate/sync-keys/utils.ts b/packages/cli/src/commands/translate/sync-keys/utils.ts index d3007b1fa..8644334e8 100644 --- a/packages/cli/src/commands/translate/sync-keys/utils.ts +++ b/packages/cli/src/commands/translate/sync-keys/utils.ts @@ -7,12 +7,42 @@ import ts from 'typescript'; import { consoleLog } from '../../../utils.js'; +const getValue = (property: ts.PropertyAssignment): string => { + if ( + ts.isStringLiteral(property.initializer) || + ts.isNoSubstitutionTemplateLiteral(property.initializer) + ) { + return property.initializer.getText(); + } + + consoleLog.fatal('Unsupported property:', property.getFullText()); +}; + +const hasLeadingUntranslatedComment = (fullText: string) => { + const commentRanges = ts.getLeadingCommentRanges(fullText, 0); + + if (commentRanges?.[0]) { + if (commentRanges.length > 1) { + consoleLog.fatal('Multiple comments found for property.'); + } + + const commentRange = commentRanges[0]; + const comment = fullText.slice(commentRange.pos, commentRange.end); + if (comment.includes(untranslatedMark)) { + return true; + } + consoleLog.warn('Unsupported comment:', comment); + } + + return false; +}; + type FileStructure = { - [key: string]: { filePath?: string; structure: FileStructure }; + [key: string | number]: { filePath?: string; structure: FileStructure }; }; type NestedPhraseObject = { - [key: string]: [phrase: string, isTranslated: boolean] | NestedPhraseObject; + [key: string | number]: [phrase: string, isTranslated: boolean] | NestedPhraseObject; }; const untranslatedMark = 'UNTRANSLATED'; @@ -118,35 +148,32 @@ export const parseLocaleFiles = (filePath: string): ParsedTuple => { nestedObject[key] = phrases; fileStructure[key] = { structure }; /* eslint-enable @silverhand/fp/no-mutation */ - } else if ( - ts.isStringLiteral(property.initializer) || - ts.isNoSubstitutionTemplateLiteral(property.initializer) - ) { - const value = property.initializer.getText(); - - const commentRanges = ts.getLeadingCommentRanges(property.getFullText(), 0); - - if (commentRanges?.[0]) { - if (commentRanges.length > 1) { - consoleLog.fatal('Multiple comments found for property:', property); + } else if (ts.isArrayLiteralExpression(property.initializer)) { + const elements = property.initializer.elements.map((element) => { + if (ts.isStringLiteralLike(element)) { + return [ + [element.getText(), !hasLeadingUntranslatedComment(element.getFullText())], + {}, + ]; } - const commentRange = commentRanges[0]; - const comment = property.getFullText().slice(commentRange.pos, commentRange.end); - if (comment.includes(untranslatedMark)) { - // eslint-disable-next-line @silverhand/fp/no-mutation - nestedObject[key] = [value, false]; - } else { - // eslint-disable-next-line @silverhand/fp/no-mutation - nestedObject[key] = [value, true]; - consoleLog.warn('Unsupported comment:', comment); - } - } else { - // eslint-disable-next-line @silverhand/fp/no-mutation - nestedObject[key] = [value, true]; - } + return traverseNode(element, {}, {}); + }); + + /* eslint-disable @silverhand/fp/no-mutation */ + nestedObject[key] = Object.fromEntries( + elements.map(([phrases], index) => [index, phrases]) + ); + fileStructure[key] = { + structure: Object.fromEntries( + elements.map(([, structure], index) => [index, { structure }]) + ), + }; + /* eslint-enable @silverhand/fp/no-mutation */ } else { - consoleLog.fatal('Unsupported property:', property); + const value = getValue(property); + // eslint-disable-next-line @silverhand/fp/no-mutation + nestedObject[key] = [value, !hasLeadingUntranslatedComment(property.getFullText())]; } } }; @@ -260,15 +287,18 @@ const traverseNode = async ( // Recursively traverse the nested object of phrases and the file structure // of the baseline language + // eslint-disable-next-line complexity const traverseObject = async ( baseline: ParsedTuple, targetObject: NestedPhraseObject, tabSize: number ) => { const [baselineObject, baselineStructure] = baseline; + const isBaselineArray = 0 in baselineObject; for (const [key, value] of Object.entries(baselineObject)) { const existingValue = targetObject[key]; + const keyOutput = isBaselineArray ? '' : `${key}: `; if (Array.isArray(value)) { const [phrase] = value; @@ -280,11 +310,11 @@ const traverseNode = async ( if (Array.isArray(existingValue) && existingValue[1]) { await fs.appendFile( targetFilePath, - `${' '.repeat(tabSize)}${key}: ${existingValue[0]},\n` + `${' '.repeat(tabSize)}${keyOutput}${existingValue[0]},\n` ); } else { await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}/** ${untranslatedMark} */\n`); - await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${phrase},\n`); + await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${keyOutput}${phrase},\n`); } } // Not a string, treat it as a nested object or an import @@ -301,15 +331,22 @@ const traverseNode = async ( path.join(targetDirectory, keyStructure.filePath) ); } - // Otherwise, treat it as a nested object. + // Otherwise, treat it as a nested object or array. else { - await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: {\n`); + const isValueArray = 0 in value; + await fs.appendFile( + targetFilePath, + `${' '.repeat(tabSize)}${keyOutput}${isValueArray ? '[' : '{'}\n` + ); await traverseObject( [value, keyStructure?.structure ?? {}], Array.isArray(existingValue) ? {} : existingValue ?? {}, tabSize + 2 ); - await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}},\n`); + await fs.appendFile( + targetFilePath, + `${' '.repeat(tabSize)}${isValueArray ? ']' : '}'},\n` + ); } } } diff --git a/packages/cli/src/commands/translate/sync.ts b/packages/cli/src/commands/translate/sync.ts index 9a65b8694..6ec819810 100644 --- a/packages/cli/src/commands/translate/sync.ts +++ b/packages/cli/src/commands/translate/sync.ts @@ -13,7 +13,7 @@ const sync: CommandModule<{ path?: string }, { path?: string }> = { describe: 'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.', handler: async ({ path: inputPath }) => { - const queue = new PQueue({ concurrency: 1 }); + const queue = new PQueue({ concurrency: 5 }); const instancePath = await inquireInstancePath(inputPath); for (const languageTag of Object.keys(languages)) {