mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(cli): support sync array keys (#4903)
* feat(cli): support sync array keys * refactor(cli): update translate sync concurrency to 5 * chore: add changeset
This commit is contained in:
parent
7379cfe4bd
commit
4b90782ae0
3 changed files with 78 additions and 34 deletions
7
.changeset/empty-buttons-ring.md
Normal file
7
.changeset/empty-buttons-ring.md
Normal file
|
@ -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.
|
|
@ -7,12 +7,42 @@ import ts from 'typescript';
|
||||||
|
|
||||||
import { consoleLog } from '../../../utils.js';
|
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 = {
|
type FileStructure = {
|
||||||
[key: string]: { filePath?: string; structure: FileStructure };
|
[key: string | number]: { filePath?: string; structure: FileStructure };
|
||||||
};
|
};
|
||||||
|
|
||||||
type NestedPhraseObject = {
|
type NestedPhraseObject = {
|
||||||
[key: string]: [phrase: string, isTranslated: boolean] | NestedPhraseObject;
|
[key: string | number]: [phrase: string, isTranslated: boolean] | NestedPhraseObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
const untranslatedMark = 'UNTRANSLATED';
|
const untranslatedMark = 'UNTRANSLATED';
|
||||||
|
@ -118,35 +148,32 @@ export const parseLocaleFiles = (filePath: string): ParsedTuple => {
|
||||||
nestedObject[key] = phrases;
|
nestedObject[key] = phrases;
|
||||||
fileStructure[key] = { structure };
|
fileStructure[key] = { structure };
|
||||||
/* eslint-enable @silverhand/fp/no-mutation */
|
/* eslint-enable @silverhand/fp/no-mutation */
|
||||||
} else if (
|
} else if (ts.isArrayLiteralExpression(property.initializer)) {
|
||||||
ts.isStringLiteral(property.initializer) ||
|
const elements = property.initializer.elements.map((element) => {
|
||||||
ts.isNoSubstitutionTemplateLiteral(property.initializer)
|
if (ts.isStringLiteralLike(element)) {
|
||||||
) {
|
return [
|
||||||
const value = property.initializer.getText();
|
[element.getText(), !hasLeadingUntranslatedComment(element.getFullText())],
|
||||||
|
{},
|
||||||
const commentRanges = ts.getLeadingCommentRanges(property.getFullText(), 0);
|
];
|
||||||
|
|
||||||
if (commentRanges?.[0]) {
|
|
||||||
if (commentRanges.length > 1) {
|
|
||||||
consoleLog.fatal('Multiple comments found for property:', property);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentRange = commentRanges[0];
|
return traverseNode(element, {}, {});
|
||||||
const comment = property.getFullText().slice(commentRange.pos, commentRange.end);
|
});
|
||||||
if (comment.includes(untranslatedMark)) {
|
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
/* eslint-disable @silverhand/fp/no-mutation */
|
||||||
nestedObject[key] = [value, false];
|
nestedObject[key] = Object.fromEntries(
|
||||||
} else {
|
elements.map(([phrases], index) => [index, phrases])
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
);
|
||||||
nestedObject[key] = [value, true];
|
fileStructure[key] = {
|
||||||
consoleLog.warn('Unsupported comment:', comment);
|
structure: Object.fromEntries(
|
||||||
}
|
elements.map(([, structure], index) => [index, { structure }])
|
||||||
} else {
|
),
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
};
|
||||||
nestedObject[key] = [value, true];
|
/* eslint-enable @silverhand/fp/no-mutation */
|
||||||
}
|
|
||||||
} else {
|
} 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
|
// Recursively traverse the nested object of phrases and the file structure
|
||||||
// of the baseline language
|
// of the baseline language
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
const traverseObject = async (
|
const traverseObject = async (
|
||||||
baseline: ParsedTuple,
|
baseline: ParsedTuple,
|
||||||
targetObject: NestedPhraseObject,
|
targetObject: NestedPhraseObject,
|
||||||
tabSize: number
|
tabSize: number
|
||||||
) => {
|
) => {
|
||||||
const [baselineObject, baselineStructure] = baseline;
|
const [baselineObject, baselineStructure] = baseline;
|
||||||
|
const isBaselineArray = 0 in baselineObject;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(baselineObject)) {
|
for (const [key, value] of Object.entries(baselineObject)) {
|
||||||
const existingValue = targetObject[key];
|
const existingValue = targetObject[key];
|
||||||
|
const keyOutput = isBaselineArray ? '' : `${key}: `;
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const [phrase] = value;
|
const [phrase] = value;
|
||||||
|
@ -280,11 +310,11 @@ const traverseNode = async (
|
||||||
if (Array.isArray(existingValue) && existingValue[1]) {
|
if (Array.isArray(existingValue) && existingValue[1]) {
|
||||||
await fs.appendFile(
|
await fs.appendFile(
|
||||||
targetFilePath,
|
targetFilePath,
|
||||||
`${' '.repeat(tabSize)}${key}: ${existingValue[0]},\n`
|
`${' '.repeat(tabSize)}${keyOutput}${existingValue[0]},\n`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}/** ${untranslatedMark} */\n`);
|
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
|
// Not a string, treat it as a nested object or an import
|
||||||
|
@ -301,15 +331,22 @@ const traverseNode = async (
|
||||||
path.join(targetDirectory, keyStructure.filePath)
|
path.join(targetDirectory, keyStructure.filePath)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Otherwise, treat it as a nested object.
|
// Otherwise, treat it as a nested object or array.
|
||||||
else {
|
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(
|
await traverseObject(
|
||||||
[value, keyStructure?.structure ?? {}],
|
[value, keyStructure?.structure ?? {}],
|
||||||
Array.isArray(existingValue) ? {} : existingValue ?? {},
|
Array.isArray(existingValue) ? {} : existingValue ?? {},
|
||||||
tabSize + 2
|
tabSize + 2
|
||||||
);
|
);
|
||||||
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}},\n`);
|
await fs.appendFile(
|
||||||
|
targetFilePath,
|
||||||
|
`${' '.repeat(tabSize)}${isValueArray ? ']' : '}'},\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ const sync: CommandModule<{ path?: string }, { path?: string }> = {
|
||||||
describe:
|
describe:
|
||||||
'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
||||||
handler: async ({ path: inputPath }) => {
|
handler: async ({ path: inputPath }) => {
|
||||||
const queue = new PQueue({ concurrency: 1 });
|
const queue = new PQueue({ concurrency: 5 });
|
||||||
const instancePath = await inquireInstancePath(inputPath);
|
const instancePath = await inquireInstancePath(inputPath);
|
||||||
|
|
||||||
for (const languageTag of Object.keys(languages)) {
|
for (const languageTag of Object.keys(languages)) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue