0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(cli): translate sync-keys command (#4265)

* feat(cli): `translate sync-keys` command

* refactor(cli): use promise and add comments

* refactor(phrases): fix lint errors

* refactor(cli): fix bugs and add changeset
This commit is contained in:
Gao Sun 2023-07-31 12:15:45 +08:00 committed by GitHub
parent fe54bbafe2
commit fde330a8b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 570 additions and 135 deletions

View file

@ -0,0 +1,7 @@
---
"@logto/cli": minor
---
add `translate sync-keys` command
This command is helpful for syncing keys from one language to another. Run `logto translate sync-keys --help` for details.

View file

@ -12,6 +12,7 @@
"start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" dev",
"start": "cd packages/core && NODE_ENV=production node .",
"cli": "logto",
"changeset": "changeset",
"alteration": "logto db alt",
"connectors:build": "pnpm -r --filter \"./packages/connectors/connector-*\" build",
"//": "# `changeset version` won't run version lifecycle scripts, see https://github.com/changesets/changesets/issues/860",
@ -22,7 +23,7 @@
"ci:test": "pnpm -r --parallel --workspace-concurrency=0 test:ci"
},
"devDependencies": {
"@changesets/cli": "^2.25.0",
"@changesets/cli": "^2.26.2",
"@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6",
"@commitlint/types": "^17.4.4",

View file

@ -69,6 +69,7 @@
"slonik-interceptor-preset": "^1.2.10",
"slonik-sql-tag-raw": "^1.1.4",
"tar": "^6.1.11",
"typescript": "^5.0.0",
"yargs": "^17.6.0",
"zod": "^3.20.2"
},
@ -86,8 +87,7 @@
"jest": "^29.5.0",
"lint-staged": "^13.0.0",
"prettier": "^3.0.0",
"sinon": "^15.0.0",
"typescript": "^5.0.0"
"sinon": "^15.0.0"
},
"eslintConfig": {
"extends": "@silverhand",

View file

@ -3,6 +3,7 @@ import type { CommandModule } from 'yargs';
import create from './create.js';
import listTags from './list-tags.js';
import syncKeys from './sync-keys/index.js';
import sync from './sync.js';
const translate: CommandModule = {
@ -18,6 +19,7 @@ const translate: CommandModule = {
.command(create)
.command(listTags)
.command(sync)
.command(syncKeys)
.demandCommand(1),
handler: noop,
};

View file

@ -0,0 +1,115 @@
import { execFile } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { promisify } from 'node:util';
import { isLanguageTag } from '@logto/language-kit';
import ora from 'ora';
import { type CommandModule } from 'yargs';
import { consoleLog } from '../../../utils.js';
import { inquireInstancePath } from '../../connector/utils.js';
import { praseLocaleFiles, syncPhraseKeysAndFileStructure } from './utils.js';
const execPromise = promisify(execFile);
const syncKeys: CommandModule<
{ path?: string },
{ path?: string; baseline: string; target: string; skipLint?: boolean; package: string }
> = {
command: ['sync-keys', 'sk'],
describe: [
'Sync nested object keys and the file structure from baseline to target.',
'If a key is missing in the target, it will be added with a comment to indicate that the phrase is untranslated;',
'If a key is missing in the baseline, it will be removed from the target;',
'If a key exists in both the baseline and the target, the value of the target will be used.',
].join(' '),
builder: (yargs) =>
yargs
.option('baseline', {
alias: 'b',
type: 'string',
describe: 'The baseline language tag',
default: 'en',
})
.option('package', {
alias: 'pkg',
type: 'string',
describe: 'The package name of the phrases, one of `phrases` or `phrases-ui`',
default: 'phrases',
})
.option('target', {
alias: 't',
type: 'string',
describe: 'The target language tag, or `all` to sync all languages',
})
.option('skip-lint', {
alias: 's',
type: 'boolean',
describe: 'Skip running `eslint --fix` for locales after syncing',
})
.demandOption(['baseline', 'target']),
handler: async ({
path: inputPath,
baseline: baselineTag,
target: targetTag,
skipLint,
package: packageName,
}) => {
if (!isLanguageTag(baselineTag)) {
consoleLog.fatal('Invalid baseline language tag');
}
if (targetTag !== 'all' && !isLanguageTag(targetTag)) {
consoleLog.fatal('Invalid target language tag');
}
if (baselineTag === targetTag) {
consoleLog.fatal('Baseline and target cannot be the same');
}
if (packageName !== 'phrases' && packageName !== 'phrases-ui') {
consoleLog.fatal('Invalid package name, expected `phrases` or `phrases-ui`');
}
const instancePath = await inquireInstancePath(inputPath);
const phrasesPath = path.join(instancePath, 'packages', packageName);
const localesPath = path.join(phrasesPath, 'src/locales');
const entrypoint = path.join(localesPath, baselineTag.toLowerCase(), 'index.ts');
const baseline = praseLocaleFiles(entrypoint);
const targetLocales =
targetTag === 'all' ? fs.readdirSync(localesPath) : [targetTag.toLowerCase()];
/* eslint-disable no-await-in-loop */
for (const target of targetLocales) {
if (target === baselineTag) {
continue;
}
const spinner = ora({
text: `Syncing object keys and file structure from ${baselineTag} to ${target}`,
}).start();
const targetDirectory = path.join(localesPath, target);
await syncPhraseKeysAndFileStructure(baseline, target, targetDirectory);
spinner.succeed(`Synced object keys and file structure from ${baselineTag} to ${target}`);
}
/* eslint-enable no-await-in-loop */
if (!skipLint) {
const spinner = ora({
text: 'Running `eslint --fix` for locales',
}).start();
await execPromise(
'pnpm',
['eslint', '--ext', '.ts', path.relative(phrasesPath, localesPath), '--fix'],
{ cwd: phrasesPath }
);
spinner.succeed('Ran `eslint --fix` for locales');
}
},
};
export default syncKeys;

View file

@ -0,0 +1,330 @@
import { readFileSync, existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { trySafe } from '@silverhand/essentials';
import ts from 'typescript';
import { consoleLog } from '../../../utils.js';
type FileStructure = {
[key: string]: { filePath?: string; structure: FileStructure };
};
type NestedPhraseObject = {
[key: string]: string | NestedPhraseObject;
};
type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
/**
* Given a entrypoint file path of a language, parse the nested object of
* phrases and the file structure.
*
* @example
* Given the following file:
*
* ```ts
* import errors from './errors/index.js';
*
* const translation = {
* page_title: 'Anwendungen',
* errors,
* };
* ```
*
* The returned object will be:
*
* ```ts
* {
* page_title: 'Anwendungen',
* errors: {
* page_not_found: 'Seite nicht gefunden',
* },
* }
* ```
*
* And the file structure will be:
*
* ```ts
* {
* errors: {
* filePath: './errors/index.js',
* structure: {},
* },
* }
* ```
*
* @param filePath The entrypoint file path of a language
*
* @returns A tuple of the nested object of phrases and the file structure
*
*/
export const praseLocaleFiles = (filePath: string): ParsedTuple => {
const content = readFileSync(filePath, 'utf8');
const ast = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
const importIdentifierPath = new Map<string, string>();
const traverseNode = (
node: ts.Node,
nestedObject: NestedPhraseObject,
fileStructure: FileStructure
) => {
/**
* Handle property assignment in object literal expression:
*
* - Shorthand property assignments (e.g. `{ errors }`) are treated as import
* - Property assignments will be categorized per its initializer:
* - Object literal expressions (e.g. `{ errors: { page_not_found: 'Page not found' } }`)
* will be treated as nested object
* - String literals (e.g. `{ page_title: 'Applications' }`) or no substitution template
* literals (e.g. `{ page_title: `Applications` }`) will be treated as string
* - Others are not supported, and will exit with error
*/
const handleProperty = (property: ts.ObjectLiteralElementLike) => {
// Treat shorthand property assignment as import
if (ts.isShorthandPropertyAssignment(property)) {
const key = property.name.getText();
const importPath = importIdentifierPath.get(key);
if (!importPath) {
consoleLog.fatal(`Cannot find import path for ${key} in ${filePath}`);
}
const resolvedPath = path.resolve(path.dirname(filePath), importPath);
// Recursively parse the nested object from the imported file
const [phrases, structure] = praseLocaleFiles(resolvedPath);
// eslint-disable-next-line @silverhand/fp/no-mutation
nestedObject[key] = phrases;
// eslint-disable-next-line @silverhand/fp/no-mutation
fileStructure[key] = {
filePath: importPath,
structure,
};
}
if (ts.isPropertyAssignment(property)) {
const key = property.name.getText();
// Nested object, recursively parse it
if (ts.isObjectLiteralExpression(property.initializer)) {
const [phrases, structure] = traverseNode(property.initializer, {}, {});
// eslint-disable-next-line @silverhand/fp/no-mutation
nestedObject[key] = phrases;
// eslint-disable-next-line @silverhand/fp/no-mutation
fileStructure[key] = { structure };
} else if (
ts.isStringLiteral(property.initializer) ||
ts.isNoSubstitutionTemplateLiteral(property.initializer)
) {
const value = property.initializer.getText();
// eslint-disable-next-line @silverhand/fp/no-mutation
nestedObject[key] = value;
} else {
consoleLog.fatal('Unsupported property:', property);
}
}
};
if (ts.isImportDeclaration(node)) {
const importPath = node.moduleSpecifier.getText().slice(1, -1).replace('.js', '.ts');
const importIdentifier = node.importClause?.getText();
// Assuming only default import is used
if (importIdentifier) {
importIdentifierPath.set(importIdentifier, importPath);
}
} else if (ts.isObjectLiteralExpression(node)) {
for (const property of node.properties) {
handleProperty(property);
}
} else {
node.forEachChild((child) => {
traverseNode(child, nestedObject, fileStructure);
});
}
return [nestedObject, fileStructure] as const;
};
return traverseNode(ast, {}, {});
};
const getIdentifier = (filePath: string) => {
const filename = path.basename(filePath, '.ts');
return (filename === 'index' ? path.basename(path.dirname(filePath)) : filename).replaceAll(
'-',
'_'
);
};
/** Traverse the file structure and return an array of imports in the current file. */
const getCurrentFileImports = (fileStructure: FileStructure) => {
const imports = new Set<[string, string]>();
for (const [key, value] of Object.entries(fileStructure)) {
// If the key has a file path, treat it as an import and stop traversing
// since it's pointing to another file
if (value.filePath) {
imports.add([key, value.filePath]);
}
// Otherwise, recursively traverse the nested object
else {
for (const importEntry of getCurrentFileImports(value.structure)) {
imports.add(importEntry);
}
}
}
return [...imports];
};
/**
* Recursively traverse the nested object of phrases and the file structure of
* the baseline language, and generate the target language directory with the
* same file structure.
*
* Values of the nested object will be replaced with the values of the target
* language if the key exists; otherwise, the value of the baseline language
* will be used.
*
* @param baseline The baseline language tuple
* @param targetObject The target language nested object
* @param targetFilePath The target language entrypoint file path
* @param isRoot Whether the target file is the root entrypoint
*/
/* eslint-disable no-await-in-loop */
const traverseNode = async (
baseline: ParsedTuple,
targetObject: NestedPhraseObject,
targetFilePath: string,
isRoot = false
) => {
const [, baselineStructure] = baseline;
const targetDirectory = path.dirname(targetFilePath);
await fs.mkdir(targetDirectory, { recursive: true });
await fs.writeFile(targetFilePath, '', { flag: 'w+' });
if (isRoot) {
await fs.appendFile(targetFilePath, "import type { LocalePhrase } from '../../types.js';\n\n");
}
// Write imports first
const baselineEntries = getCurrentFileImports(baselineStructure);
for (const [key, value] of baselineEntries
.slice()
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))) {
const importPath = path.join(targetDirectory, value);
await fs.appendFile(
targetFilePath,
`import ${key} from './${path
.relative(targetDirectory, importPath)
.replace('.ts', '.js')}';\n`
);
}
// Add a newline between imports and the object
if (baselineEntries.length > 0) {
await fs.appendFile(targetFilePath, '\n');
}
// Write the object
const identifier = getIdentifier(targetFilePath);
await fs.appendFile(targetFilePath, `const ${identifier} = {\n`);
// Recursively traverse the nested object of phrases and the file structure
// of the baseline language
const traverseObject = async (
baseline: ParsedTuple,
targetObject: NestedPhraseObject,
tabSize: number
) => {
const [baselineObject, baselineStructure] = baseline;
for (const [key, value] of Object.entries(baselineObject)) {
const existingValue = targetObject[key];
if (typeof value === 'string') {
// If the key exists in the target language and the value is a string, use
// the value of the target language; otherwise, use the value of the
// baseline language and add a comment to indicate that the phrase is
// untranslated to help identify missing translations.
await (typeof existingValue === 'string'
? fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${existingValue},\n`)
: fs.appendFile(
targetFilePath,
`${' '.repeat(tabSize)}${key}: ${value}, // UNTRANSLATED\n`
));
}
// Not a string, treat it as a nested object or an import
else {
const keyStructure = baselineStructure[key];
// If the key has a file structure, treat it as an import
if (keyStructure?.filePath) {
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key},\n`);
await traverseNode(
[value, keyStructure.structure],
typeof existingValue === 'object' ? existingValue : {},
path.join(targetDirectory, keyStructure.filePath)
);
}
// Otherwise, treat it as a nested object.
else {
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: {\n`);
await traverseObject(
[value, keyStructure?.structure ?? {}],
typeof existingValue === 'object' ? existingValue : {},
tabSize + 2
);
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}},\n`);
}
}
}
};
await traverseObject(baseline, targetObject, 2);
await (isRoot
? fs.appendFile(targetFilePath, '} satisfies LocalePhrase;\n\n')
: fs.appendFile(targetFilePath, '};\n\n'));
await fs.appendFile(targetFilePath, `export default Object.freeze(${identifier});\n`);
};
/* eslint-enable no-await-in-loop */
export const syncPhraseKeysAndFileStructure = async (
baseline: ParsedTuple,
targetLocale: string,
targetDirectory: string
) => {
const targetEntrypoint = path.join(targetDirectory, 'index.ts');
const isTargetLocaleExist = existsSync(targetEntrypoint);
const targetObject = isTargetLocaleExist ? praseLocaleFiles(targetEntrypoint)[0] : {};
const backupDirectory = targetDirectory + '.bak';
if (isTargetLocaleExist) {
await fs.rename(targetDirectory, backupDirectory);
} else {
consoleLog.warn(`Cannot find ${targetLocale} entrypoint, creating one`);
}
await trySafe(
traverseNode(baseline, targetObject, path.join(targetDirectory, 'index.ts'), true),
(error) => {
consoleLog.plain();
consoleLog.error(error);
consoleLog.plain();
consoleLog.fatal(
`Failed to sync keys for ${targetLocale}, the backup is at ${backupDirectory} for recovery`
);
}
);
if (isTargetLocaleExist) {
await fs.rm(backupDirectory, { recursive: true });
}
};

View file

@ -48,7 +48,10 @@
"typescript": "^5.0.0"
},
"eslintConfig": {
"extends": "@silverhand"
"extends": "@silverhand",
"rules": {
"no-template-curly-in-string": "off"
}
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}

View file

@ -24,7 +24,6 @@ const subscription = {
upgrade_pro: 'Pro upgraden',
update_payment: 'Zahlung aktualisieren',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Es wurde ein Zahlungsproblem festgestellt. Der Betrag von ${{price, number}} für den vorherigen Zyklus kann nicht verarbeitet werden. Aktualisieren Sie die Zahlung, um eine Aussetzung des Logto-Dienstes zu vermeiden.',
downgrade: 'Herabstufen',
current: 'Aktuell',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Ihre monatlich aktiven Benutzer (MAU) werden in 3 Stufen unterteilt, basierend darauf, wie oft sie sich während des Abrechnungszeitraums anmelden. Jede Stufe hat einen anderen Preis pro MAU-Einheit.',
unlimited: 'Unbegrenzt',
contact: 'Kontakt',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mo',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} Tag',
days_other: '{{count, number}} Tage',

View file

@ -24,7 +24,6 @@ const subscription = {
upgrade_pro: 'Upgrade Pro',
update_payment: 'Update payment',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Payment issue detected. Unable to process ${{price, number}} for previous cycle. Update payment to avoid Logto service suspension.',
downgrade: 'Downgrade',
current: 'Current',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Your monthly active users (MAU) are divided into 3 tiers based on how often they log in during the billing cycle. Each tier has a different price per MAU unit.',
unlimited: 'Unlimited',
contact: 'Contact',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mo',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} day',
days_other: '{{count, number}} days',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: 'Actualizar a Pro',
update_payment: 'Actualizar pago',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Se ha detectado un problema de pago. No se puede procesar ${ {price, number}} para el ciclo anterior. Actualice el pago para evitar la suspensión del servicio Logto.',
downgrade: 'Degradar',
current: 'Actual',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Sus usuarios activos mensuales (MAU) se dividen en 3 niveles según la frecuencia con la que inician sesión durante el ciclo de facturación. Cada nivel tiene un precio diferente por unidad de MAU.',
unlimited: 'Ilimitado',
contact: 'Contacto',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mes',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} día',
days_other: '{{count, number}} días',

View file

@ -26,7 +26,6 @@ const subscription = {
upgrade_pro: 'Passer au Plan Professionnel',
update_payment: 'Mettre à jour le paiement',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Problème de paiement détecté. Impossible de traiter ${{price, number}} pour le cycle précédent. Mettez à jour le paiement pour éviter la suspension du service Logto.',
downgrade: 'Passer à un Plan Inférieur',
current: 'Actuel',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Vos utilisateurs actifs mensuels (MAU) sont répartis en 3 niveaux en fonction de la fréquence à laquelle ils se connectent pendant le cycle de facturation. Chaque niveau a un prix différent par unité MAU.',
unlimited: 'Illimité',
contact: 'Contact',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mo',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} jour',
days_other: '{{count, number}} jours',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: "Esegui l'upgrade a Pro",
update_payment: 'Aggiorna pagamento',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Rilevato un problema di pagamento. Impossibile elaborare ${{price, number}} per il ciclo precedente. Aggiorna il pagamento per evitare la sospensione del servizio Logto.',
downgrade: 'Degrado',
current: 'Attuale',

View file

@ -59,9 +59,9 @@ const quota_table = {
"* I vostri utenti attivi mensili (MAU) sono divisi in 3 livelli in base a quante volte effettuano l'accesso durante il ciclo di fatturazione. Ogni livello ha un prezzo diverso per unità MAU.",
unlimited: 'Illimitato',
contact: 'Contatta',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mese',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} giorno',
days_other: '{{count, number}} giorni',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: 'プロプランにアップグレード',
update_payment: '支払いを更新する',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'支払いに問題が発生しました。前回のサイクルで ${{price, number}} を処理できませんでした。Logtoのサービス停止を回避するために支払いを更新してください。',
downgrade: 'ダウングレード',
current: '現在',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* 月間アクティブユーザーMAUは、請求サイクル中のログイン頻度に基づいて3つの階層に分かれます。各階層ごとに異なるMAU単価が適用されます。',
unlimited: '無制限',
contact: 'お問い合わせ',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mo',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}}日',
days_other: '{{count, number}}日',

View file

@ -24,7 +24,6 @@ const subscription = {
upgrade_pro: '프로 업그레이드',
update_payment: '결제 정보 업데이트',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'결제 문제가 발생했습니다. 이전 주기에 ${{price, number}}을(를) 처리할 수 없습니다. Logto 서비스 중단을 피하기 위해 결제를 업데이트하세요.',
downgrade: '다운그레이드',
current: '현재',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* 월간 활성 사용자(MAU)는 청구 주기 동안 로그인 빈도에 따라 3단계로 나뉩니다. 각 단계마다 달리 책정되는 MAU 단가가 있습니다.',
unlimited: '무제한',
contact: '문의',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/월',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} 일',
days_other: '{{count, number}} 일',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: 'Uaktualnij do Pro',
update_payment: 'Zaktualizuj płatność',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Wykryto problem z płatnością. Nie można przetworzyć ${{price, number}} za poprzedni cykl. Zaktualizuj płatność, aby uniknąć zawieszenia usługi Logto.',
downgrade: 'Zdegradować',
current: 'Obecnie',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Aktywni użytkownicy miesięcznie (MAU) są podzieleni na 3 poziomy w zależności od częstotliwości logowania się w okresie rozliczeniowym. Każdy poziom ma inną cenę za jednostkę MAU.',
unlimited: 'Nieograniczone',
contact: 'Kontakt',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mies.',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} dzień',
days_other: '{{count, number}} dni',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Seus usuários ativos mensais (MAU) são divididos em 3 níveis com base em quantas vezes eles fazem login durante o ciclo de faturamento. Cada nível tem um preço diferente por unidade de MAU.',
unlimited: 'Ilimitado',
contact: 'Contato',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mês',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} dia',
days_other: '{{count, number}} dias',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: 'Atualizar para Pro',
update_payment: 'Atualizar pagamento',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Problema de pagamento detectado. Não é possível processar ${{price, number}} para o ciclo anterior. Atualize o pagamento para evitar a suspensão do serviço Logto.',
downgrade: 'Downgrade',
current: 'Atual',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Os seus utilizadores ativos mensais (MAU) são divididos em 3 níveis com base na frequência com que iniciam sessão durante o ciclo de faturação. Cada nível tem um preço diferente por unidade de MAU.',
unlimited: 'Ilimitado',
contact: 'Contactar',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/mês',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} dia',
days_other: '{{count, number}} dias',

View file

@ -24,7 +24,6 @@ const subscription = {
upgrade_pro: 'Повысить уровень до Pro',
update_payment: 'Обновить платеж',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Обнаружена ошибка платежа. Невозможно обработать сумму ${{price, number}} за предыдущий цикл. Обновите платежную информацию, чтобы избежать блокировки сервиса Logto.',
downgrade: 'Понизить уровень',
current: 'Текущий',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Ваши активные пользователи в месяц (MAU) разделены на 3 уровня в зависимости от того, как часто они входят в систему в течение биллингового периода. Каждый уровень имеет свою стоимость за единицу MAU.',
unlimited: 'Неограниченно',
contact: 'Связаться',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/мес.',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} день',
days_other: '{{count, number}} дней',

View file

@ -25,7 +25,6 @@ const subscription = {
upgrade_pro: "Pro'ya yükselt",
update_payment: 'Ödemeyi Güncelle',
payment_error:
// eslint-disable-next-line no-template-curly-in-string
'Ödeme hatası tespit edildi. Önceki döngü için ${{price, number}} işlenemedi. Logto hizmeti askıya alınmasını önlemek için ödemeleri güncelleyin.',
downgrade: 'Düşür',
current: 'Mevcut',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* Aylık etkin kullanıcılarınız (MAU), faturalandırma dönemi boyunca ne sıklıkla oturum açtıklarına göre 3 düzeye ayrılır. Her düzeyin farklı bir MAU birim fiyatı vardır.',
unlimited: 'Sınırsız',
contact: 'İletişim',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/ay',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}} gün',
days_other: '{{count, number}} gün',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* 您的每月活跃用户MAU根据在结算周期内登录的频率分为3个层级。每个层级的MAU单价不同。',
unlimited: '无限制',
contact: '联系',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/月',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}}天',
days_other: '{{count, number}}天',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* 您的每月活躍用戶MAU將根據在結算週期內登錄的頻率分為3個層級。每個層級都有不同的MAU單價。',
unlimited: '無限制',
contact: '聯絡',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/月',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}}天',
days_other: '{{count, number}}天',

View file

@ -59,9 +59,9 @@ const quota_table = {
'* 您的每月活躍使用者MAU將根據在結算週期內登錄的頻率分為3個層級。每個層級都有不同的MAU單價。',
unlimited: '無限制',
contact: '聯絡',
// eslint-disable-next-line no-template-curly-in-string
monthly_price: '${{value, number}}/月',
// eslint-disable-next-line no-template-curly-in-string
mau_price: '${{value, number}}/MAU',
days_one: '{{count, number}}天',
days_other: '{{count, number}}天',

View file

@ -13,8 +13,8 @@ importers:
version: link:packages/cli
devDependencies:
'@changesets/cli':
specifier: ^2.25.0
version: 2.25.0
specifier: ^2.26.2
version: 2.26.2
'@commitlint/cli':
specifier: ^17.6.6
version: 17.6.6
@ -181,6 +181,9 @@ importers:
tar:
specifier: ^6.1.11
version: 6.1.11
typescript:
specifier: ^5.0.0
version: 5.0.2
yargs:
specifier: ^17.6.0
version: 17.6.0
@ -230,9 +233,6 @@ importers:
sinon:
specifier: ^15.0.0
version: 15.0.0
typescript:
specifier: ^5.0.0
version: 5.0.2
packages/connectors/connector-alipay-native:
dependencies:
@ -6248,14 +6248,14 @@ packages:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
/@changesets/apply-release-plan@6.1.1:
resolution: {integrity: sha512-LaQiP/Wf0zMVR0HNrLQAjz3rsNsr0d/RlnP6Ef4oi8VafOwnY1EoWdK4kssuUJGgNgDyHpomS50dm8CU3D7k7g==}
/@changesets/apply-release-plan@6.1.4:
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/config': 2.2.0
'@changesets/config': 2.3.1
'@changesets/get-version-range-type': 0.3.2
'@changesets/git': 1.5.0
'@changesets/types': 5.2.0
'@changesets/git': 2.0.0
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
detect-indent: 6.1.0
fs-extra: 7.0.1
@ -6263,47 +6263,47 @@ packages:
outdent: 0.5.0
prettier: 2.8.4
resolve-from: 5.0.0
semver: 5.7.2
semver: 7.5.4
dev: true
/@changesets/assemble-release-plan@5.2.2:
resolution: {integrity: sha512-B1qxErQd85AeZgZFZw2bDKyOfdXHhG+X5S+W3Da2yCem8l/pRy4G/S7iOpEcMwg6lH8q2ZhgbZZwZ817D+aLuQ==}
/@changesets/assemble-release-plan@5.2.4:
resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.4
'@changesets/types': 5.2.0
'@changesets/get-dependents-graph': 1.3.6
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
semver: 5.7.2
semver: 7.5.4
dev: true
/@changesets/changelog-git@0.1.13:
resolution: {integrity: sha512-zvJ50Q+EUALzeawAxax6nF2WIcSsC5PwbuLeWkckS8ulWnuPYx8Fn/Sjd3rF46OzeKA8t30loYYV6TIzp4DIdg==}
/@changesets/changelog-git@0.1.14:
resolution: {integrity: sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==}
dependencies:
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
dev: true
/@changesets/cli@2.25.0:
resolution: {integrity: sha512-Svu5KD2enurVHGEEzCRlaojrHjVYgF9srmMP9VQSy9c1TspX6C9lDPpulsSNIjYY9BuU/oiWpjBgR7RI9eQiAA==}
/@changesets/cli@2.26.2:
resolution: {integrity: sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==}
hasBin: true
dependencies:
'@babel/runtime': 7.19.4
'@changesets/apply-release-plan': 6.1.1
'@changesets/assemble-release-plan': 5.2.2
'@changesets/changelog-git': 0.1.13
'@changesets/config': 2.2.0
'@babel/runtime': 7.21.0
'@changesets/apply-release-plan': 6.1.4
'@changesets/assemble-release-plan': 5.2.4
'@changesets/changelog-git': 0.1.14
'@changesets/config': 2.3.1
'@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.4
'@changesets/get-release-plan': 3.0.15
'@changesets/git': 1.5.0
'@changesets/get-dependents-graph': 1.3.6
'@changesets/get-release-plan': 3.0.17
'@changesets/git': 2.0.0
'@changesets/logger': 0.0.5
'@changesets/pre': 1.0.13
'@changesets/read': 0.5.8
'@changesets/types': 5.2.0
'@changesets/write': 0.2.1
'@changesets/pre': 1.0.14
'@changesets/read': 0.5.9
'@changesets/types': 5.2.1
'@changesets/write': 0.2.3
'@manypkg/get-packages': 1.1.3
'@types/is-ci': 3.0.0
'@types/semver': 6.2.3
'@types/semver': 7.5.0
ansi-colors: 4.1.3
chalk: 2.4.2
enquirer: 2.3.6
@ -6316,19 +6316,19 @@ packages:
p-limit: 2.3.0
preferred-pm: 3.0.3
resolve-from: 5.0.0
semver: 5.7.1
semver: 7.5.4
spawndamnit: 2.0.0
term-size: 2.2.1
tty-table: 4.1.6
dev: true
/@changesets/config@2.2.0:
resolution: {integrity: sha512-GGaokp3nm5FEDk/Fv2PCRcQCOxGKKPRZ7prcMqxEr7VSsG75MnChQE8plaW1k6V8L2bJE+jZWiRm19LbnproOw==}
/@changesets/config@2.3.1:
resolution: {integrity: sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==}
dependencies:
'@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.4
'@changesets/get-dependents-graph': 1.3.6
'@changesets/logger': 0.0.5
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
micromatch: 4.0.5
@ -6340,25 +6340,25 @@ packages:
extendable-error: 0.1.7
dev: true
/@changesets/get-dependents-graph@1.3.4:
resolution: {integrity: sha512-+C4AOrrFY146ydrgKOo5vTZfj7vetNu1tWshOID+UjPUU9afYGDXI8yLnAeib1ffeBXV3TuGVcyphKpJ3cKe+A==}
/@changesets/get-dependents-graph@1.3.6:
resolution: {integrity: sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==}
dependencies:
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
chalk: 2.4.2
fs-extra: 7.0.1
semver: 5.7.2
semver: 7.5.4
dev: true
/@changesets/get-release-plan@3.0.15:
resolution: {integrity: sha512-W1tFwxE178/en+zSj/Nqbc3mvz88mcdqUMJhRzN1jDYqN3QI4ifVaRF9mcWUU+KI0gyYEtYR65tour690PqTcA==}
/@changesets/get-release-plan@3.0.17:
resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/assemble-release-plan': 5.2.2
'@changesets/config': 2.2.0
'@changesets/pre': 1.0.13
'@changesets/read': 0.5.8
'@changesets/types': 5.2.0
'@changesets/assemble-release-plan': 5.2.4
'@changesets/config': 2.3.1
'@changesets/pre': 1.0.14
'@changesets/read': 0.5.9
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
dev: true
@ -6366,14 +6366,15 @@ packages:
resolution: {integrity: sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==}
dev: true
/@changesets/git@1.5.0:
resolution: {integrity: sha512-Xo8AT2G7rQJSwV87c8PwMm6BAc98BnufRMsML7m7Iw8Or18WFvFmxqG5aOL5PBvhgq9KrKvaeIBNIymracSuHg==}
/@changesets/git@2.0.0:
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/errors': 0.1.4
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
is-subdir: 1.2.0
micromatch: 4.0.5
spawndamnit: 2.0.0
dev: true
@ -6383,31 +6384,31 @@ packages:
chalk: 2.4.2
dev: true
/@changesets/parse@0.3.15:
resolution: {integrity: sha512-3eDVqVuBtp63i+BxEWHPFj2P1s3syk0PTrk2d94W9JD30iG+OER0Y6n65TeLlY8T2yB9Fvj6Ev5Gg0+cKe/ZUA==}
/@changesets/parse@0.3.16:
resolution: {integrity: sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==}
dependencies:
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
js-yaml: 3.14.1
dev: true
/@changesets/pre@1.0.13:
resolution: {integrity: sha512-jrZc766+kGZHDukjKhpBXhBJjVQMied4Fu076y9guY1D3H622NOw8AQaLV3oQsDtKBTrT2AUFjt9Z2Y9Qx+GfA==}
/@changesets/pre@1.0.14:
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/errors': 0.1.4
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
dev: true
/@changesets/read@0.5.8:
resolution: {integrity: sha512-eYaNfxemgX7f7ELC58e7yqQICW5FB7V+bd1lKt7g57mxUrTveYME+JPaBPpYx02nP53XI6CQp6YxnR9NfmFPKw==}
/@changesets/read@0.5.9:
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/git': 1.5.0
'@changesets/git': 2.0.0
'@changesets/logger': 0.0.5
'@changesets/parse': 0.3.15
'@changesets/types': 5.2.0
'@changesets/parse': 0.3.16
'@changesets/types': 5.2.1
chalk: 2.4.2
fs-extra: 7.0.1
p-filter: 2.1.0
@ -6417,15 +6418,15 @@ packages:
resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
dev: true
/@changesets/types@5.2.0:
resolution: {integrity: sha512-km/66KOqJC+eicZXsm2oq8A8bVTSpkZJ60iPV/Nl5Z5c7p9kk8xxh6XGRTlnludHldxOOfudhnDN2qPxtHmXzA==}
/@changesets/types@5.2.1:
resolution: {integrity: sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==}
dev: true
/@changesets/write@0.2.1:
resolution: {integrity: sha512-KUd49nt2fnYdGixIqTi1yVE1nAoZYUMdtB3jBfp77IMqjZ65hrmZE5HdccDlTeClZN0420ffpnfET3zzeY8pdw==}
/@changesets/write@0.2.3:
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
dependencies:
'@babel/runtime': 7.21.0
'@changesets/types': 5.2.0
'@changesets/types': 5.2.1
fs-extra: 7.0.1
human-id: 1.0.2
prettier: 2.8.4
@ -9731,14 +9732,14 @@ packages:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: true
/@types/semver@6.2.3:
resolution: {integrity: sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==}
dev: true
/@types/semver@7.3.12:
resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==}
dev: true
/@types/semver@7.5.0:
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
dev: true
/@types/serve-static@1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies:
@ -11471,14 +11472,6 @@ packages:
dependencies:
ms: 2.1.2
/decamelize-keys@1.1.0:
resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==}
engines: {node: '>=0.10.0'}
dependencies:
decamelize: 1.2.0
map-obj: 1.0.1
dev: true
/decamelize-keys@1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
engines: {node: '>=0.10.0'}
@ -12844,7 +12837,7 @@ packages:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
dependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
dev: true
@ -13094,7 +13087,7 @@ packages:
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.2.12
fast-glob: 3.3.0
ignore: 5.2.4
merge2: 1.4.1
slash: 3.0.0
@ -15766,7 +15759,7 @@ packages:
dependencies:
'@types/minimist': 1.2.2
camelcase-keys: 6.2.2
decamelize-keys: 1.1.0
decamelize-keys: 1.1.1
hard-rejection: 2.1.0
minimist-options: 4.1.0
normalize-package-data: 2.5.0
@ -18597,11 +18590,6 @@ packages:
/semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
/semver@5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
dev: true
/semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@ -19897,7 +19885,7 @@ packages:
smartwrap: 2.0.2
strip-ansi: 6.0.1
wcwidth: 1.0.1
yargs: 17.7.1
yargs: 17.7.2
dev: true
/tunnel@0.0.6: