@logto/cli 1.18.0 → 1.20.0
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/lib/commands/database/alteration/index.d.ts +1 -1
- package/lib/commands/database/seed/tables.js +2 -1
- package/lib/connector/utils.d.ts +3 -3
- package/lib/index.js +0 -2
- package/lib/package-json.d.ts +1 -6
- package/lib/package-json.js +10 -15
- package/lib/queries/logto-config.d.ts +1 -1
- package/lib/queries/system.d.ts +1 -1
- package/lib/utils.d.ts +0 -1
- package/lib/utils.js +1 -19
- package/package.json +10 -15
- package/lib/commands/translate/create.d.ts +0 -8
- package/lib/commands/translate/create.js +0 -37
- package/lib/commands/translate/index.d.ts +0 -3
- package/lib/commands/translate/index.js +0 -27
- package/lib/commands/translate/list-tags.d.ts +0 -3
- package/lib/commands/translate/list-tags.js +0 -19
- package/lib/commands/translate/openai.d.ts +0 -11
- package/lib/commands/translate/openai.js +0 -73
- package/lib/commands/translate/prompts.d.ts +0 -18
- package/lib/commands/translate/prompts.js +0 -48
- package/lib/commands/translate/sync-keys/index.d.ts +0 -13
- package/lib/commands/translate/sync-keys/index.js +0 -73
- package/lib/commands/translate/sync-keys/utils.d.ts +0 -57
- package/lib/commands/translate/sync-keys/utils.js +0 -295
- package/lib/commands/translate/sync.d.ts +0 -10
- package/lib/commands/translate/sync.js +0 -44
- package/lib/commands/translate/utils.d.ts +0 -14
- package/lib/commands/translate/utils.js +0 -130
|
@@ -2,7 +2,7 @@ import type { CommonQueryMethods } from '@silverhand/slonik';
|
|
|
2
2
|
import type { CommandModule } from 'yargs';
|
|
3
3
|
import type { AlterationFile } from './type.js';
|
|
4
4
|
export declare const getLatestAlterationTimestamp: () => Promise<number>;
|
|
5
|
-
export declare const getAvailableAlterations: (pool: CommonQueryMethods, compareMode?:
|
|
5
|
+
export declare const getAvailableAlterations: (pool: CommonQueryMethods, compareMode?: "gt" | "lte") => Promise<AlterationFile[]>;
|
|
6
6
|
declare const alteration: CommandModule<unknown, {
|
|
7
7
|
action: string;
|
|
8
8
|
target?: string;
|
|
@@ -103,7 +103,8 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
|
103
103
|
await assignScopesToRole(connection, adminTenantId, applicationRole.id, ...cloudAdditionalScopes
|
|
104
104
|
.filter(({ name }) => name === CloudScope.SendSms ||
|
|
105
105
|
name === CloudScope.SendEmail ||
|
|
106
|
-
name === CloudScope.FetchCustomJwt
|
|
106
|
+
name === CloudScope.FetchCustomJwt ||
|
|
107
|
+
name === CloudScope.ReportSubscriptionUpdates)
|
|
107
108
|
.map(({ id }) => id));
|
|
108
109
|
await Promise.all([
|
|
109
110
|
seedLegacyManagementApiUserRole(connection),
|
package/lib/connector/utils.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { ConnectorType } from '@logto/connector-kit';
|
|
|
3
3
|
import type { BaseRoutes, Router } from '@withtyped/server';
|
|
4
4
|
import type { ConnectorFactory } from './types.js';
|
|
5
5
|
export declare function validateConnectorModule(connector: Partial<BaseConnector<ConnectorType>>): asserts connector is BaseConnector<ConnectorType>;
|
|
6
|
-
export declare const readUrl: (url: string, baseUrl: string, type:
|
|
7
|
-
export declare const parseMetadata: (metadata: AllConnector[
|
|
8
|
-
export declare const buildRawConnector: <T extends Router<any, any, BaseRoutes, string>, U extends AllConnector = AllConnector>(connectorFactory: ConnectorFactory<T, U>, getConnectorConfig?: GetConnectorConfig, getCloudServiceClient?: GetCloudServiceClient<T>
|
|
6
|
+
export declare const readUrl: (url: string, baseUrl: string, type: "text" | "image") => Promise<string>;
|
|
7
|
+
export declare const parseMetadata: (metadata: AllConnector["metadata"], packagePath: string) => Promise<AllConnector["metadata"]>;
|
|
8
|
+
export declare const buildRawConnector: <T extends Router<any, any, BaseRoutes, string>, U extends AllConnector = AllConnector>(connectorFactory: ConnectorFactory<T, U>, getConnectorConfig?: GetConnectorConfig, getCloudServiceClient?: GetCloudServiceClient<T>) => Promise<{
|
|
9
9
|
rawConnector: U;
|
|
10
10
|
rawMetadata: ConnectorMetadata;
|
|
11
11
|
}>;
|
package/lib/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import { hideBin } from 'yargs/helpers';
|
|
|
5
5
|
import connector from './commands/connector/index.js';
|
|
6
6
|
import database from './commands/database/index.js';
|
|
7
7
|
import install from './commands/install/index.js';
|
|
8
|
-
import translate from './commands/translate/index.js';
|
|
9
8
|
import { packageJson } from './package-json.js';
|
|
10
9
|
import { cliConfig, ConfigKey, consoleLog } from './utils.js';
|
|
11
10
|
void yargs(hideBin(process.argv))
|
|
@@ -43,7 +42,6 @@ void yargs(hideBin(process.argv))
|
|
|
43
42
|
.command(install)
|
|
44
43
|
.command(database)
|
|
45
44
|
.command(connector)
|
|
46
|
-
.command(translate)
|
|
47
45
|
.demandCommand(1)
|
|
48
46
|
.showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
|
|
49
47
|
.strict()
|
package/lib/package-json.d.ts
CHANGED
|
@@ -41,9 +41,6 @@ export declare const packageJson: {
|
|
|
41
41
|
dependencies: {
|
|
42
42
|
"@logto/connector-kit": string;
|
|
43
43
|
"@logto/core-kit": string;
|
|
44
|
-
"@logto/language-kit": string;
|
|
45
|
-
"@logto/phrases": string;
|
|
46
|
-
"@logto/phrases-experience": string;
|
|
47
44
|
"@logto/schemas": string;
|
|
48
45
|
"@logto/shared": string;
|
|
49
46
|
"@silverhand/essentials": string;
|
|
@@ -57,13 +54,10 @@ export declare const packageJson: {
|
|
|
57
54
|
nanoid: string;
|
|
58
55
|
ora: string;
|
|
59
56
|
"p-limit": string;
|
|
60
|
-
"p-queue": string;
|
|
61
57
|
"p-retry": string;
|
|
62
58
|
"pg-protocol": string;
|
|
63
|
-
roarr: string;
|
|
64
59
|
semver: string;
|
|
65
60
|
tar: string;
|
|
66
|
-
typescript: string;
|
|
67
61
|
yargs: string;
|
|
68
62
|
zod: string;
|
|
69
63
|
};
|
|
@@ -82,6 +76,7 @@ export declare const packageJson: {
|
|
|
82
76
|
"lint-staged": string;
|
|
83
77
|
prettier: string;
|
|
84
78
|
sinon: string;
|
|
79
|
+
typescript: string;
|
|
85
80
|
vitest: string;
|
|
86
81
|
};
|
|
87
82
|
eslintConfig: {
|
package/lib/package-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const packageJson = {
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -44,31 +44,25 @@ export const packageJson = {
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@logto/connector-kit": "workspace:^4.0.0",
|
|
46
46
|
"@logto/core-kit": "workspace:^2.5.0",
|
|
47
|
-
"@logto/
|
|
48
|
-
"@logto/phrases": "workspace:^1.12.0",
|
|
49
|
-
"@logto/phrases-experience": "workspace:^1.7.0",
|
|
50
|
-
"@logto/schemas": "workspace:1.18.0",
|
|
47
|
+
"@logto/schemas": "workspace:1.20.0",
|
|
51
48
|
"@logto/shared": "workspace:^3.1.1",
|
|
52
49
|
"@silverhand/essentials": "^2.9.1",
|
|
53
50
|
"@silverhand/slonik": "31.0.0-beta.2",
|
|
54
|
-
"chalk": "^5.
|
|
51
|
+
"chalk": "^5.3.0",
|
|
55
52
|
"decamelize": "^6.0.0",
|
|
56
|
-
"dotenv": "^16.
|
|
53
|
+
"dotenv": "^16.4.5",
|
|
57
54
|
"got": "^14.0.0",
|
|
58
55
|
"hpagent": "^1.2.0",
|
|
59
56
|
"inquirer": "^9.0.0",
|
|
60
57
|
"nanoid": "^5.0.1",
|
|
61
58
|
"ora": "^8.0.1",
|
|
62
|
-
"p-limit": "^
|
|
63
|
-
"p-queue": "^8.0.0",
|
|
59
|
+
"p-limit": "^6.0.0",
|
|
64
60
|
"p-retry": "^6.0.0",
|
|
65
61
|
"pg-protocol": "^1.6.0",
|
|
66
|
-
"roarr": "^7.11.0",
|
|
67
62
|
"semver": "^7.3.8",
|
|
68
63
|
"tar": "^7.0.0",
|
|
69
|
-
"typescript": "^5.3.3",
|
|
70
64
|
"yargs": "^17.6.0",
|
|
71
|
-
"zod": "^3.
|
|
65
|
+
"zod": "^3.23.8"
|
|
72
66
|
},
|
|
73
67
|
"devDependencies": {
|
|
74
68
|
"@silverhand/eslint-config": "6.0.1",
|
|
@@ -79,13 +73,14 @@ export const packageJson = {
|
|
|
79
73
|
"@types/sinon": "^17.0.0",
|
|
80
74
|
"@types/tar": "^6.1.12",
|
|
81
75
|
"@types/yargs": "^17.0.13",
|
|
82
|
-
"@vitest/coverage-v8": "^
|
|
83
|
-
"@withtyped/server": "^0.
|
|
76
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
77
|
+
"@withtyped/server": "^0.14.0",
|
|
84
78
|
"eslint": "^8.56.0",
|
|
85
79
|
"lint-staged": "^15.0.0",
|
|
86
80
|
"prettier": "^3.0.0",
|
|
87
81
|
"sinon": "^18.0.0",
|
|
88
|
-
"
|
|
82
|
+
"typescript": "^5.5.3",
|
|
83
|
+
"vitest": "^2.0.0"
|
|
89
84
|
},
|
|
90
85
|
"eslintConfig": {
|
|
91
86
|
"extends": "@silverhand",
|
|
@@ -3,4 +3,4 @@ import type { CommonQueryMethods } from '@silverhand/slonik';
|
|
|
3
3
|
import type { z } from 'zod';
|
|
4
4
|
export declare const doesConfigsTableExist: (pool: CommonQueryMethods) => Promise<boolean>;
|
|
5
5
|
export declare const getRowsByKeys: (pool: CommonQueryMethods, tenantId: string, keys: LogtoConfigKey[]) => Promise<import("@silverhand/slonik").QueryResult<LogtoConfig>>;
|
|
6
|
-
export declare const updateValueByKey: <T extends LogtoConfigKey>(pool: CommonQueryMethods, tenantId: string, key: T, value: z.
|
|
6
|
+
export declare const updateValueByKey: <T extends LogtoConfigKey>(pool: CommonQueryMethods, tenantId: string, key: T, value: z.infer<(typeof logtoConfigGuards)[T]>) => Promise<import("@silverhand/slonik").QueryResult<import("@silverhand/slonik").QueryResultRow>>;
|
package/lib/queries/system.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export declare const doesSystemsTableExist: (pool: CommonQueryMethods) => Promis
|
|
|
6
6
|
export declare const getCurrentDatabaseAlterationTimestamp: (pool: CommonQueryMethods) => Promise<number>;
|
|
7
7
|
export declare const updateDatabaseTimestamp: (connection: DatabaseTransactionConnection, timestamp: number) => Promise<void>;
|
|
8
8
|
export declare const getRowByKey: (pool: CommonQueryMethods, key: SystemKey) => Promise<System | null>;
|
|
9
|
-
export declare const updateValueByKey: <T extends SystemKey>(pool: CommonQueryMethods, key: T, value: z.
|
|
9
|
+
export declare const updateValueByKey: <T extends SystemKey>(pool: CommonQueryMethods, key: T, value: z.infer<(typeof systemGuards)[T]>) => Promise<import("@silverhand/slonik").QueryResult<import("@silverhand/slonik").QueryResultRow>>;
|
package/lib/utils.d.ts
CHANGED
package/lib/utils.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { execSync
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
2
|
import { createWriteStream, existsSync } from 'node:fs';
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
6
|
import { ConsoleLog } from '@logto/shared';
|
|
8
7
|
import { assert, conditional, conditionalString } from '@silverhand/essentials';
|
|
9
8
|
import chalk from 'chalk';
|
|
@@ -186,20 +185,3 @@ export const getConnectorPackagesFromDirectory = async (directory) => {
|
|
|
186
185
|
}));
|
|
187
186
|
return rawPackages.filter((packageInfo) => typeof packageInfo.name === 'string');
|
|
188
187
|
};
|
|
189
|
-
const execPromise = promisify(execFile);
|
|
190
|
-
export const lintLocaleFiles = async (
|
|
191
|
-
/** Logto instance path */
|
|
192
|
-
instancePath,
|
|
193
|
-
/** Target package name, ignore to lint both `phrases` and `phrases-experience` packages */
|
|
194
|
-
packageName) => {
|
|
195
|
-
const spinner = ora({
|
|
196
|
-
text: 'Running `eslint --fix` for locales',
|
|
197
|
-
}).start();
|
|
198
|
-
const targetPackages = packageName ? [packageName] : ['phrases', 'phrases-experience'];
|
|
199
|
-
await Promise.all(targetPackages.map(async (packageName) => {
|
|
200
|
-
const phrasesPath = path.join(instancePath, 'packages', packageName);
|
|
201
|
-
const localesPath = path.join(phrasesPath, 'src/locales');
|
|
202
|
-
await execPromise('pnpm', ['eslint', '--ext', '.ts', path.relative(phrasesPath, localesPath), '--fix'], { cwd: phrasesPath });
|
|
203
|
-
}));
|
|
204
|
-
spinner.succeed('Ran `eslint --fix` for locales');
|
|
205
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -30,31 +30,25 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@logto/connector-kit": "^4.0.0",
|
|
32
32
|
"@logto/core-kit": "^2.5.0",
|
|
33
|
-
"@logto/
|
|
34
|
-
"@logto/phrases": "^1.12.0",
|
|
35
|
-
"@logto/phrases-experience": "^1.7.0",
|
|
36
|
-
"@logto/schemas": "1.18.0",
|
|
33
|
+
"@logto/schemas": "1.20.0",
|
|
37
34
|
"@logto/shared": "^3.1.1",
|
|
38
35
|
"@silverhand/essentials": "^2.9.1",
|
|
39
36
|
"@silverhand/slonik": "31.0.0-beta.2",
|
|
40
|
-
"chalk": "^5.
|
|
37
|
+
"chalk": "^5.3.0",
|
|
41
38
|
"decamelize": "^6.0.0",
|
|
42
|
-
"dotenv": "^16.
|
|
39
|
+
"dotenv": "^16.4.5",
|
|
43
40
|
"got": "^14.0.0",
|
|
44
41
|
"hpagent": "^1.2.0",
|
|
45
42
|
"inquirer": "^9.0.0",
|
|
46
43
|
"nanoid": "^5.0.1",
|
|
47
44
|
"ora": "^8.0.1",
|
|
48
|
-
"p-limit": "^
|
|
49
|
-
"p-queue": "^8.0.0",
|
|
45
|
+
"p-limit": "^6.0.0",
|
|
50
46
|
"p-retry": "^6.0.0",
|
|
51
47
|
"pg-protocol": "^1.6.0",
|
|
52
|
-
"roarr": "^7.11.0",
|
|
53
48
|
"semver": "^7.3.8",
|
|
54
49
|
"tar": "^7.0.0",
|
|
55
|
-
"typescript": "^5.3.3",
|
|
56
50
|
"yargs": "^17.6.0",
|
|
57
|
-
"zod": "^3.
|
|
51
|
+
"zod": "^3.23.8"
|
|
58
52
|
},
|
|
59
53
|
"devDependencies": {
|
|
60
54
|
"@silverhand/eslint-config": "6.0.1",
|
|
@@ -65,13 +59,14 @@
|
|
|
65
59
|
"@types/sinon": "^17.0.0",
|
|
66
60
|
"@types/tar": "^6.1.12",
|
|
67
61
|
"@types/yargs": "^17.0.13",
|
|
68
|
-
"@vitest/coverage-v8": "^
|
|
69
|
-
"@withtyped/server": "^0.
|
|
62
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
63
|
+
"@withtyped/server": "^0.14.0",
|
|
70
64
|
"eslint": "^8.56.0",
|
|
71
65
|
"lint-staged": "^15.0.0",
|
|
72
66
|
"prettier": "^3.0.0",
|
|
73
67
|
"sinon": "^18.0.0",
|
|
74
|
-
"
|
|
68
|
+
"typescript": "^5.5.3",
|
|
69
|
+
"vitest": "^2.0.0"
|
|
75
70
|
},
|
|
76
71
|
"eslintConfig": {
|
|
77
72
|
"extends": "@silverhand",
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { isLanguageTag } from '@logto/language-kit';
|
|
2
|
-
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
-
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
|
|
4
|
-
import { consoleLog, inquireInstancePath } from '../../utils.js';
|
|
5
|
-
import { createFullTranslation } from './utils.js';
|
|
6
|
-
const create = {
|
|
7
|
-
command: ['create <language-tag>', 'c'],
|
|
8
|
-
describe: 'Create a new language translation using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
|
9
|
-
builder: (yargs) => yargs.positional('language-tag', {
|
|
10
|
-
describe: 'The language tag to create, e.g. `af-ZA`.',
|
|
11
|
-
type: 'string',
|
|
12
|
-
demandOption: true,
|
|
13
|
-
}),
|
|
14
|
-
handler: async ({ path: inputPath, languageTag }) => {
|
|
15
|
-
if (!isLanguageTag(languageTag)) {
|
|
16
|
-
consoleLog.fatal('Invalid language tag. Run `logto translate list-tags` to see available list.');
|
|
17
|
-
}
|
|
18
|
-
const instancePath = await inquireInstancePath(inputPath);
|
|
19
|
-
if (isPhrasesBuiltInLanguageTag(languageTag)) {
|
|
20
|
-
consoleLog.info(languageTag + ' is a built-in tag of phrases, updating untranslated phrases');
|
|
21
|
-
}
|
|
22
|
-
await createFullTranslation({
|
|
23
|
-
instancePath,
|
|
24
|
-
packageName: 'phrases',
|
|
25
|
-
languageTag,
|
|
26
|
-
});
|
|
27
|
-
if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
|
|
28
|
-
consoleLog.info(languageTag + ' is a built-in tag of phrases-experience, updating untranslated phrases');
|
|
29
|
-
}
|
|
30
|
-
await createFullTranslation({
|
|
31
|
-
instancePath,
|
|
32
|
-
packageName: 'phrases-experience',
|
|
33
|
-
languageTag,
|
|
34
|
-
});
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
export default create;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { noop } from '@silverhand/essentials';
|
|
2
|
-
import create from './create.js';
|
|
3
|
-
import listTags from './list-tags.js';
|
|
4
|
-
import syncKeys from './sync-keys/index.js';
|
|
5
|
-
import sync from './sync.js';
|
|
6
|
-
const translate = {
|
|
7
|
-
command: ['translate', 't'],
|
|
8
|
-
describe: 'Commands for Logto translation',
|
|
9
|
-
builder: (yargs) => yargs
|
|
10
|
-
.option('path', {
|
|
11
|
-
alias: 'p',
|
|
12
|
-
type: 'string',
|
|
13
|
-
describe: 'The path to your Logto instance directory',
|
|
14
|
-
})
|
|
15
|
-
.option('skip-core-check', {
|
|
16
|
-
alias: 'sc',
|
|
17
|
-
type: 'boolean',
|
|
18
|
-
describe: 'Skip checking if the core package is existed',
|
|
19
|
-
})
|
|
20
|
-
.command(create)
|
|
21
|
-
.command(listTags)
|
|
22
|
-
.command(sync)
|
|
23
|
-
.command(syncKeys)
|
|
24
|
-
.demandCommand(1),
|
|
25
|
-
handler: noop,
|
|
26
|
-
};
|
|
27
|
-
export default translate;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { languages } from '@logto/language-kit';
|
|
2
|
-
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
-
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { consoleLog } from '../../utils.js';
|
|
6
|
-
const listTags = {
|
|
7
|
-
command: ['list-tags', 'list'],
|
|
8
|
-
describe: 'List all available language tags',
|
|
9
|
-
handler: async () => {
|
|
10
|
-
for (const tag of Object.keys(languages)) {
|
|
11
|
-
consoleLog.plain(...[
|
|
12
|
-
tag,
|
|
13
|
-
isPhrasesBuiltInLanguageTag(tag) && chalk.blue('phrases'),
|
|
14
|
-
isPhrasesUiBuiltInLanguageTag(tag) && chalk.blue('phrases-experience'),
|
|
15
|
-
].filter(Boolean));
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
export default listTags;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type LanguageTag } from '@logto/language-kit';
|
|
2
|
-
import { type Got } from 'got';
|
|
3
|
-
export declare const createOpenaiApi: () => Got;
|
|
4
|
-
type TranslateConfig = {
|
|
5
|
-
api: Got;
|
|
6
|
-
sourceFilePath: string;
|
|
7
|
-
targetLanguage: LanguageTag;
|
|
8
|
-
extraPrompt?: string;
|
|
9
|
-
};
|
|
10
|
-
export declare const translate: ({ api, targetLanguage, sourceFilePath, extraPrompt, }: TranslateConfig) => Promise<string | undefined>;
|
|
11
|
-
export {};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { trySafe } from '@silverhand/essentials';
|
|
3
|
-
import { got, HTTPError } from 'got';
|
|
4
|
-
import { HttpsProxyAgent } from 'hpagent';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { consoleLog, getProxy } from '../../utils.js';
|
|
7
|
-
import { getTranslationPromptMessages } from './prompts.js';
|
|
8
|
-
export const createOpenaiApi = () => {
|
|
9
|
-
const proxy = getProxy();
|
|
10
|
-
return got.extend({
|
|
11
|
-
prefixUrl: process.env.OPENAI_API_PROXY_ENDPOINT ?? 'https://api.openai.com/v1',
|
|
12
|
-
headers: {
|
|
13
|
-
Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ''}`,
|
|
14
|
-
},
|
|
15
|
-
timeout: { request: 300_000 },
|
|
16
|
-
...(proxy && { agent: { https: new HttpsProxyAgent({ proxy, timeout: 300_000 }) } }),
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
const gptResponseGuard = z.object({
|
|
20
|
-
choices: z
|
|
21
|
-
.object({
|
|
22
|
-
message: z.object({ role: z.string(), content: z.string() }),
|
|
23
|
-
finish_reason: z.string(),
|
|
24
|
-
})
|
|
25
|
-
.array(),
|
|
26
|
-
});
|
|
27
|
-
export const translate = async ({ api, targetLanguage, sourceFilePath, extraPrompt, }) => {
|
|
28
|
-
const sourceFileContent = await fs.readFile(sourceFilePath, 'utf8');
|
|
29
|
-
const response = await trySafe(api
|
|
30
|
-
.post('chat/completions', {
|
|
31
|
-
json: {
|
|
32
|
-
// The full list of OPENAI model can be found at https://platform.openai.com/docs/models.
|
|
33
|
-
model: process.env.OPENAI_MODEL_NAME ?? 'gpt-3.5-turbo-0125',
|
|
34
|
-
messages: getTranslationPromptMessages({
|
|
35
|
-
sourceFileContent,
|
|
36
|
-
targetLanguage,
|
|
37
|
-
extraPrompt,
|
|
38
|
-
}),
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
.json(), (error) => {
|
|
42
|
-
consoleLog.warn(`Error while translating ${sourceFilePath}:`, String(error));
|
|
43
|
-
if (error instanceof HTTPError) {
|
|
44
|
-
consoleLog.warn(error.response.body);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
if (!response) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const guarded = gptResponseGuard.safeParse(response);
|
|
51
|
-
if (!guarded.success) {
|
|
52
|
-
consoleLog.warn(`Error while guarding response for ${sourceFilePath}:`, response);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const [entity] = guarded.data.choices;
|
|
56
|
-
if (!entity) {
|
|
57
|
-
consoleLog.warn(`No choice found in response when translating ${sourceFilePath}`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (entity.finish_reason !== 'stop') {
|
|
61
|
-
consoleLog.warn(`Unexpected finish reason ${entity.finish_reason} for ${sourceFilePath}`);
|
|
62
|
-
}
|
|
63
|
-
const { content } = entity.message;
|
|
64
|
-
const matched = /```(?:ts)?\n(.*)```/s.exec(content)?.[1];
|
|
65
|
-
if (!matched) {
|
|
66
|
-
// Treat as pure code
|
|
67
|
-
if (['const ', 'import '].some((prefix) => content.startsWith(prefix))) {
|
|
68
|
-
return content;
|
|
69
|
-
}
|
|
70
|
-
consoleLog.warn('No matching code snippet from response:', content);
|
|
71
|
-
}
|
|
72
|
-
return matched;
|
|
73
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type LanguageTag } from '@logto/language-kit';
|
|
2
|
-
type GetTranslationPromptProperties = {
|
|
3
|
-
sourceFileContent: string;
|
|
4
|
-
targetLanguage: LanguageTag;
|
|
5
|
-
extraPrompt?: string;
|
|
6
|
-
};
|
|
7
|
-
export declare const untranslatedMark = "/** UNTRANSLATED */";
|
|
8
|
-
/**
|
|
9
|
-
* Note:
|
|
10
|
-
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1600.
|
|
11
|
-
* Remember to check the token limit before adding more prompt.
|
|
12
|
-
* Tokens can be counted in https://platform.openai.com/tokenizer
|
|
13
|
-
*/
|
|
14
|
-
export declare const getTranslationPromptMessages: ({ sourceFileContent, targetLanguage, extraPrompt, }: GetTranslationPromptProperties) => {
|
|
15
|
-
role: string;
|
|
16
|
-
content: string;
|
|
17
|
-
}[];
|
|
18
|
-
export {};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { languages } from '@logto/language-kit';
|
|
2
|
-
import { conditionalString } from '@silverhand/essentials';
|
|
3
|
-
export const untranslatedMark = '/** UNTRANSLATED */';
|
|
4
|
-
/**
|
|
5
|
-
* Note:
|
|
6
|
-
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1600.
|
|
7
|
-
* Remember to check the token limit before adding more prompt.
|
|
8
|
-
* Tokens can be counted in https://platform.openai.com/tokenizer
|
|
9
|
-
*/
|
|
10
|
-
export const getTranslationPromptMessages = ({ sourceFileContent, targetLanguage, extraPrompt, }) => [
|
|
11
|
-
{
|
|
12
|
-
role: 'assistant',
|
|
13
|
-
content: `You are a assistant translator and will receive a TypeScript object. Traverse and find object values with "${untranslatedMark}" annotation on the top, then translate these values to target locale "${languages[targetLanguage]}". Remove the "${untranslatedMark}" annotations from output. Escape the single quotes (if any) in translated results by prepending a backslash. Keep the interpolation double curly brackets and their inner values intact. Make sure there is a space between the CJK and non-CJK characters. Prefer using "你" instead of "您" in Chinese. Do not include sample code snippet below in the final output. ${conditionalString(extraPrompt)}
|
|
14
|
-
|
|
15
|
-
Take translating to zh-CN as an example, if the input is:
|
|
16
|
-
\`\`\`ts
|
|
17
|
-
import others from './others.js';
|
|
18
|
-
|
|
19
|
-
const translation = {
|
|
20
|
-
hello: 'Hello',
|
|
21
|
-
${untranslatedMark}
|
|
22
|
-
world: 'world',
|
|
23
|
-
others,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default translation;
|
|
27
|
-
\`\`\`
|
|
28
|
-
|
|
29
|
-
Then the output should be:
|
|
30
|
-
\`\`\`ts
|
|
31
|
-
import others from './others.js';
|
|
32
|
-
|
|
33
|
-
const translation = {
|
|
34
|
-
hello: 'Hello',
|
|
35
|
-
world: '世界',
|
|
36
|
-
others,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default translation;
|
|
40
|
-
\`\`\``,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
role: 'user',
|
|
44
|
-
content: `\`\`\`ts
|
|
45
|
-
${sourceFileContent}
|
|
46
|
-
\`\`\``,
|
|
47
|
-
},
|
|
48
|
-
];
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type CommandModule } from 'yargs';
|
|
2
|
-
declare const syncKeys: CommandModule<{
|
|
3
|
-
path?: string;
|
|
4
|
-
skipCoreCheck?: boolean;
|
|
5
|
-
}, {
|
|
6
|
-
path?: string;
|
|
7
|
-
skipCoreCheck?: boolean;
|
|
8
|
-
baseline: string;
|
|
9
|
-
target: string;
|
|
10
|
-
skipLint?: boolean;
|
|
11
|
-
package: string;
|
|
12
|
-
}>;
|
|
13
|
-
export default syncKeys;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { isLanguageTag } from '@logto/language-kit';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { consoleLog, inquireInstancePath, lintLocaleFiles } from '../../../utils.js';
|
|
6
|
-
import { parseLocaleFiles, syncPhraseKeysAndFileStructure } from './utils.js';
|
|
7
|
-
const syncKeys = {
|
|
8
|
-
command: ['sync-keys', 'sk'],
|
|
9
|
-
describe: [
|
|
10
|
-
'Sync nested object keys and the file structure from baseline to target.',
|
|
11
|
-
'If a key is missing in the target, it will be added with a comment to indicate that the phrase is untranslated;',
|
|
12
|
-
'If a key is missing in the baseline, it will be removed from the target;',
|
|
13
|
-
'If a key exists in both the baseline and the target, the value of the target will be used.',
|
|
14
|
-
].join(' '),
|
|
15
|
-
builder: (yargs) => yargs
|
|
16
|
-
.option('baseline', {
|
|
17
|
-
alias: 'b',
|
|
18
|
-
type: 'string',
|
|
19
|
-
describe: 'The baseline language tag',
|
|
20
|
-
default: 'en',
|
|
21
|
-
})
|
|
22
|
-
.option('package', {
|
|
23
|
-
alias: 'pkg',
|
|
24
|
-
type: 'string',
|
|
25
|
-
describe: 'The package name of the phrases, e.g. `phrases` or `phrases-experience`',
|
|
26
|
-
default: 'phrases',
|
|
27
|
-
})
|
|
28
|
-
.option('target', {
|
|
29
|
-
alias: 't',
|
|
30
|
-
type: 'string',
|
|
31
|
-
describe: 'The target language tag, or `all` to sync all languages',
|
|
32
|
-
})
|
|
33
|
-
.option('skip-lint', {
|
|
34
|
-
alias: 'sl',
|
|
35
|
-
type: 'boolean',
|
|
36
|
-
describe: 'Skip running `eslint --fix` for locales after syncing',
|
|
37
|
-
})
|
|
38
|
-
.demandOption(['baseline', 'target']),
|
|
39
|
-
handler: async ({ path: inputPath, skipCoreCheck, baseline: baselineTag, target: targetTag, skipLint, package: packageName, }) => {
|
|
40
|
-
if (!isLanguageTag(baselineTag)) {
|
|
41
|
-
consoleLog.fatal('Invalid baseline language tag');
|
|
42
|
-
}
|
|
43
|
-
if (targetTag !== 'all' && !isLanguageTag(targetTag)) {
|
|
44
|
-
consoleLog.fatal('Invalid target language tag');
|
|
45
|
-
}
|
|
46
|
-
if (baselineTag === targetTag) {
|
|
47
|
-
consoleLog.fatal('Baseline and target cannot be the same');
|
|
48
|
-
}
|
|
49
|
-
const instancePath = await inquireInstancePath(inputPath, skipCoreCheck);
|
|
50
|
-
const phrasesPath = path.join(instancePath, 'packages', packageName);
|
|
51
|
-
const localesPath = path.join(phrasesPath, 'src/locales');
|
|
52
|
-
const entrypoint = path.join(localesPath, baselineTag.toLowerCase(), 'index.ts');
|
|
53
|
-
const baseline = parseLocaleFiles(entrypoint);
|
|
54
|
-
const targetLocales = targetTag === 'all' ? fs.readdirSync(localesPath) : [targetTag.toLowerCase()];
|
|
55
|
-
/* eslint-disable no-await-in-loop */
|
|
56
|
-
for (const target of targetLocales) {
|
|
57
|
-
if (target === baselineTag) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const spinner = ora({
|
|
61
|
-
text: `Syncing object keys and file structure from ${baselineTag} to ${target}`,
|
|
62
|
-
}).start();
|
|
63
|
-
const targetDirectory = path.join(localesPath, target);
|
|
64
|
-
await syncPhraseKeysAndFileStructure(baseline, target, targetDirectory);
|
|
65
|
-
spinner.succeed(`Synced object keys and file structure from ${baselineTag} to ${target}`);
|
|
66
|
-
}
|
|
67
|
-
/* eslint-enable no-await-in-loop */
|
|
68
|
-
if (!skipLint) {
|
|
69
|
-
void lintLocaleFiles(instancePath, packageName);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
export default syncKeys;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
type FileStructure = {
|
|
2
|
-
[key: string | number]: {
|
|
3
|
-
filePath?: string;
|
|
4
|
-
structure: FileStructure;
|
|
5
|
-
};
|
|
6
|
-
};
|
|
7
|
-
type NestedPhraseObject = {
|
|
8
|
-
[key: string | number]: [phrase: string, isTranslated: boolean] | NestedPhraseObject;
|
|
9
|
-
};
|
|
10
|
-
type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
|
|
11
|
-
/**
|
|
12
|
-
* Given a entrypoint file path of a language, parse the nested object of
|
|
13
|
-
* phrases and the file structure.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* Given the following file:
|
|
17
|
-
*
|
|
18
|
-
* ```ts
|
|
19
|
-
* import errors from './errors/index.js';
|
|
20
|
-
*
|
|
21
|
-
* const translation = {
|
|
22
|
-
* // UNTRANSLATED
|
|
23
|
-
* page_title: 'Applications',
|
|
24
|
-
* errors,
|
|
25
|
-
* };
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* The returned object will be:
|
|
29
|
-
*
|
|
30
|
-
* ```ts
|
|
31
|
-
* {
|
|
32
|
-
* page_title: ['Anwendungen', false],
|
|
33
|
-
* errors: {
|
|
34
|
-
* page_not_found: ['Seite nicht gefunden', true],
|
|
35
|
-
* },
|
|
36
|
-
* }
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* And the file structure will be:
|
|
40
|
-
*
|
|
41
|
-
* ```ts
|
|
42
|
-
* {
|
|
43
|
-
* errors: {
|
|
44
|
-
* filePath: './errors/index.js',
|
|
45
|
-
* structure: {},
|
|
46
|
-
* },
|
|
47
|
-
* }
|
|
48
|
-
* ```
|
|
49
|
-
*
|
|
50
|
-
* @param filePath The entrypoint file path of a language
|
|
51
|
-
*
|
|
52
|
-
* @returns A tuple of the nested object of phrases and the file structure
|
|
53
|
-
*
|
|
54
|
-
*/
|
|
55
|
-
export declare const parseLocaleFiles: (filePath: string) => ParsedTuple;
|
|
56
|
-
export declare const syncPhraseKeysAndFileStructure: (baseline: ParsedTuple, targetLocale: string, targetDirectory: string) => Promise<void>;
|
|
57
|
-
export {};
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { tryThat } from '@silverhand/essentials';
|
|
5
|
-
import ts from 'typescript';
|
|
6
|
-
import { consoleLog } from '../../../utils.js';
|
|
7
|
-
const getValue = (property) => {
|
|
8
|
-
if (ts.isStringLiteral(property.initializer) ||
|
|
9
|
-
ts.isNoSubstitutionTemplateLiteral(property.initializer)) {
|
|
10
|
-
return property.initializer.getText();
|
|
11
|
-
}
|
|
12
|
-
consoleLog.fatal('Unsupported property:', property.getFullText());
|
|
13
|
-
};
|
|
14
|
-
const hasLeadingUntranslatedComment = (fullText) => {
|
|
15
|
-
const commentRanges = ts.getLeadingCommentRanges(fullText, 0);
|
|
16
|
-
if (commentRanges?.[0]) {
|
|
17
|
-
if (commentRanges.length > 1) {
|
|
18
|
-
consoleLog.fatal('Multiple comments found for property.');
|
|
19
|
-
}
|
|
20
|
-
const commentRange = commentRanges[0];
|
|
21
|
-
const comment = fullText.slice(commentRange.pos, commentRange.end);
|
|
22
|
-
if (comment.includes(untranslatedMark)) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
consoleLog.warn('Unsupported comment:', comment);
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
};
|
|
29
|
-
const untranslatedMark = 'UNTRANSLATED';
|
|
30
|
-
/**
|
|
31
|
-
* Given a entrypoint file path of a language, parse the nested object of
|
|
32
|
-
* phrases and the file structure.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* Given the following file:
|
|
36
|
-
*
|
|
37
|
-
* ```ts
|
|
38
|
-
* import errors from './errors/index.js';
|
|
39
|
-
*
|
|
40
|
-
* const translation = {
|
|
41
|
-
* // UNTRANSLATED
|
|
42
|
-
* page_title: 'Applications',
|
|
43
|
-
* errors,
|
|
44
|
-
* };
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* The returned object will be:
|
|
48
|
-
*
|
|
49
|
-
* ```ts
|
|
50
|
-
* {
|
|
51
|
-
* page_title: ['Anwendungen', false],
|
|
52
|
-
* errors: {
|
|
53
|
-
* page_not_found: ['Seite nicht gefunden', true],
|
|
54
|
-
* },
|
|
55
|
-
* }
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* And the file structure will be:
|
|
59
|
-
*
|
|
60
|
-
* ```ts
|
|
61
|
-
* {
|
|
62
|
-
* errors: {
|
|
63
|
-
* filePath: './errors/index.js',
|
|
64
|
-
* structure: {},
|
|
65
|
-
* },
|
|
66
|
-
* }
|
|
67
|
-
* ```
|
|
68
|
-
*
|
|
69
|
-
* @param filePath The entrypoint file path of a language
|
|
70
|
-
*
|
|
71
|
-
* @returns A tuple of the nested object of phrases and the file structure
|
|
72
|
-
*
|
|
73
|
-
*/
|
|
74
|
-
export const parseLocaleFiles = (filePath) => {
|
|
75
|
-
const content = readFileSync(filePath, 'utf8');
|
|
76
|
-
const ast = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
77
|
-
const importIdentifierPath = new Map();
|
|
78
|
-
const traverseNode = (node, nestedObject, fileStructure) => {
|
|
79
|
-
/**
|
|
80
|
-
* Handle property assignment in object literal expression:
|
|
81
|
-
*
|
|
82
|
-
* - Shorthand property assignments (e.g. `{ errors }`) are treated as import
|
|
83
|
-
* - Property assignments will be categorized per its initializer:
|
|
84
|
-
* - Object literal expressions (e.g. `{ errors: { page_not_found: 'Page not found' } }`)
|
|
85
|
-
* will be treated as nested object
|
|
86
|
-
* - String literals (e.g. `{ page_title: 'Applications' }`) or no substitution template
|
|
87
|
-
* literals (e.g. `{ page_title: `Applications` }`) will be treated as string
|
|
88
|
-
* - Others are not supported, and will exit with error
|
|
89
|
-
*/
|
|
90
|
-
const handleProperty = (property) => {
|
|
91
|
-
// Treat shorthand property assignment as import
|
|
92
|
-
if (ts.isShorthandPropertyAssignment(property)) {
|
|
93
|
-
const key = property.name.getText();
|
|
94
|
-
const importPath = importIdentifierPath.get(key);
|
|
95
|
-
if (!importPath) {
|
|
96
|
-
consoleLog.fatal(`Cannot find import path for ${key} in ${filePath}`);
|
|
97
|
-
}
|
|
98
|
-
const resolvedPath = path.resolve(path.dirname(filePath), importPath);
|
|
99
|
-
// Recursively parse the nested object from the imported file
|
|
100
|
-
const [phrases, structure] = parseLocaleFiles(resolvedPath);
|
|
101
|
-
/* eslint-disable @silverhand/fp/no-mutation */
|
|
102
|
-
nestedObject[key] = phrases;
|
|
103
|
-
fileStructure[key] = {
|
|
104
|
-
filePath: importPath,
|
|
105
|
-
structure,
|
|
106
|
-
};
|
|
107
|
-
/* eslint-enable @silverhand/fp/no-mutation */
|
|
108
|
-
}
|
|
109
|
-
if (ts.isPropertyAssignment(property)) {
|
|
110
|
-
const key = property.name.getText();
|
|
111
|
-
// Nested object, recursively parse it
|
|
112
|
-
if (ts.isObjectLiteralExpression(property.initializer)) {
|
|
113
|
-
const [phrases, structure] = traverseNode(property.initializer, {}, {});
|
|
114
|
-
/* eslint-disable @silverhand/fp/no-mutation */
|
|
115
|
-
nestedObject[key] = phrases;
|
|
116
|
-
fileStructure[key] = { structure };
|
|
117
|
-
/* eslint-enable @silverhand/fp/no-mutation */
|
|
118
|
-
}
|
|
119
|
-
else if (ts.isArrayLiteralExpression(property.initializer)) {
|
|
120
|
-
const elements = property.initializer.elements.map((element) => {
|
|
121
|
-
if (ts.isStringLiteralLike(element)) {
|
|
122
|
-
return [
|
|
123
|
-
[element.getText(), !hasLeadingUntranslatedComment(element.getFullText())],
|
|
124
|
-
{},
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
return traverseNode(element, {}, {});
|
|
128
|
-
});
|
|
129
|
-
/* eslint-disable @silverhand/fp/no-mutation */
|
|
130
|
-
nestedObject[key] = Object.fromEntries(elements.map(([phrases], index) => [index, phrases]));
|
|
131
|
-
fileStructure[key] = {
|
|
132
|
-
structure: Object.fromEntries(elements.map(([, structure], index) => [index, { structure }])),
|
|
133
|
-
};
|
|
134
|
-
/* eslint-enable @silverhand/fp/no-mutation */
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
const value = getValue(property);
|
|
138
|
-
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
139
|
-
nestedObject[key] = [value, !hasLeadingUntranslatedComment(property.getFullText())];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
if (ts.isImportDeclaration(node)) {
|
|
144
|
-
const importPath = node.moduleSpecifier.getText().slice(1, -1).replace('.js', '.ts');
|
|
145
|
-
const importIdentifier = node.importClause?.getText();
|
|
146
|
-
// Assuming only default import is used
|
|
147
|
-
if (importIdentifier) {
|
|
148
|
-
importIdentifierPath.set(importIdentifier, importPath);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else if (ts.isObjectLiteralExpression(node)) {
|
|
152
|
-
for (const property of node.properties) {
|
|
153
|
-
handleProperty(property);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
node.forEachChild((child) => {
|
|
158
|
-
traverseNode(child, nestedObject, fileStructure);
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return [nestedObject, fileStructure];
|
|
162
|
-
};
|
|
163
|
-
return traverseNode(ast, {}, {});
|
|
164
|
-
};
|
|
165
|
-
const getIdentifier = (filePath) => {
|
|
166
|
-
const filename = path.basename(filePath, '.ts');
|
|
167
|
-
return (filename === 'index' ? path.basename(path.dirname(filePath)) : filename).replaceAll('-', '_');
|
|
168
|
-
};
|
|
169
|
-
/** Traverse the file structure and return an array of imports in the current file. */
|
|
170
|
-
const getCurrentFileImports = (fileStructure) => {
|
|
171
|
-
const imports = new Set();
|
|
172
|
-
for (const [key, value] of Object.entries(fileStructure)) {
|
|
173
|
-
// If the key has a file path, treat it as an import and stop traversing
|
|
174
|
-
// since it's pointing to another file
|
|
175
|
-
if (value.filePath) {
|
|
176
|
-
imports.add([key, value.filePath]);
|
|
177
|
-
}
|
|
178
|
-
// Otherwise, recursively traverse the nested object
|
|
179
|
-
else {
|
|
180
|
-
for (const importEntry of getCurrentFileImports(value.structure)) {
|
|
181
|
-
imports.add(importEntry);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return [...imports];
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Recursively traverse the nested object of phrases and the file structure of
|
|
189
|
-
* the baseline language, and generate the target language directory with the
|
|
190
|
-
* same file structure.
|
|
191
|
-
*
|
|
192
|
-
* Values of the nested object will be replaced with the values of the target
|
|
193
|
-
* language if the key exists; otherwise, the value of the baseline language
|
|
194
|
-
* will be used.
|
|
195
|
-
*
|
|
196
|
-
* @param baseline The baseline language tuple
|
|
197
|
-
* @param targetObject The target language nested object
|
|
198
|
-
* @param targetFilePath The target language entrypoint file path
|
|
199
|
-
* @param isRoot Whether the target file is the root entrypoint
|
|
200
|
-
*/
|
|
201
|
-
/* eslint-disable no-await-in-loop */
|
|
202
|
-
const traverseNode = async (baseline, targetObject, targetFilePath, isRoot = false) => {
|
|
203
|
-
const [, baselineStructure] = baseline;
|
|
204
|
-
const targetDirectory = path.dirname(targetFilePath);
|
|
205
|
-
await fs.mkdir(targetDirectory, { recursive: true });
|
|
206
|
-
await fs.writeFile(targetFilePath, '', { flag: 'w+' });
|
|
207
|
-
if (isRoot) {
|
|
208
|
-
await fs.appendFile(targetFilePath, "import { type DeepPartial } from '@silverhand/essentials';\n\nimport type { LocalePhrase } from '../../types.js';\n\n");
|
|
209
|
-
}
|
|
210
|
-
// Write imports first
|
|
211
|
-
const baselineEntries = getCurrentFileImports(baselineStructure);
|
|
212
|
-
for (const [key, value] of baselineEntries
|
|
213
|
-
.slice()
|
|
214
|
-
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))) {
|
|
215
|
-
const importPath = path.join(targetDirectory, value);
|
|
216
|
-
await fs.appendFile(targetFilePath, `import ${key} from './${path
|
|
217
|
-
.relative(targetDirectory, importPath)
|
|
218
|
-
.replace('.ts', '.js')}';\n`);
|
|
219
|
-
}
|
|
220
|
-
// Add a newline between imports and the object
|
|
221
|
-
if (baselineEntries.length > 0) {
|
|
222
|
-
await fs.appendFile(targetFilePath, '\n');
|
|
223
|
-
}
|
|
224
|
-
// Write the object
|
|
225
|
-
const identifier = getIdentifier(targetFilePath);
|
|
226
|
-
await fs.appendFile(targetFilePath, `const ${identifier} = {\n`);
|
|
227
|
-
// Recursively traverse the nested object of phrases and the file structure
|
|
228
|
-
// of the baseline language
|
|
229
|
-
// eslint-disable-next-line complexity
|
|
230
|
-
const traverseObject = async (baseline, targetObject, tabSize) => {
|
|
231
|
-
const [baselineObject, baselineStructure] = baseline;
|
|
232
|
-
const isBaselineArray = 0 in baselineObject;
|
|
233
|
-
for (const [key, value] of Object.entries(baselineObject)) {
|
|
234
|
-
const existingValue = targetObject[key];
|
|
235
|
-
const keyOutput = isBaselineArray ? '' : `${key}: `;
|
|
236
|
-
if (Array.isArray(value)) {
|
|
237
|
-
const [phrase] = value;
|
|
238
|
-
// If the key exists in the target language and the value is a translated
|
|
239
|
-
// string, use the value of the target language; otherwise, use the value
|
|
240
|
-
// of the baseline language and add a comment to indicate that the phrase
|
|
241
|
-
// is untranslated to help identify missing translations.
|
|
242
|
-
if (Array.isArray(existingValue) && existingValue[1]) {
|
|
243
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${keyOutput}${existingValue[0]},\n`);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}/** ${untranslatedMark} */\n`);
|
|
247
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${keyOutput}${phrase},\n`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Not a string, treat it as a nested object or an import
|
|
251
|
-
else {
|
|
252
|
-
const keyStructure = baselineStructure[key];
|
|
253
|
-
// If the key has a file structure, treat it as an import
|
|
254
|
-
if (keyStructure?.filePath) {
|
|
255
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key},\n`);
|
|
256
|
-
await traverseNode([value, keyStructure.structure], Array.isArray(existingValue) ? {} : existingValue ?? {}, path.join(targetDirectory, keyStructure.filePath));
|
|
257
|
-
}
|
|
258
|
-
// Otherwise, treat it as a nested object or array.
|
|
259
|
-
else {
|
|
260
|
-
const isValueArray = 0 in value;
|
|
261
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${keyOutput}${isValueArray ? '[' : '{'}\n`);
|
|
262
|
-
await traverseObject([value, keyStructure?.structure ?? {}], Array.isArray(existingValue) ? {} : existingValue ?? {}, tabSize + 2);
|
|
263
|
-
await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${isValueArray ? ']' : '}'},\n`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
await traverseObject(baseline, targetObject, 2);
|
|
269
|
-
await (isRoot
|
|
270
|
-
? fs.appendFile(targetFilePath, '} satisfies DeepPartial<LocalePhrase>;\n\n')
|
|
271
|
-
: fs.appendFile(targetFilePath, '};\n\n'));
|
|
272
|
-
await fs.appendFile(targetFilePath, `export default Object.freeze(${identifier});\n`);
|
|
273
|
-
};
|
|
274
|
-
/* eslint-enable no-await-in-loop */
|
|
275
|
-
export const syncPhraseKeysAndFileStructure = async (baseline, targetLocale, targetDirectory) => {
|
|
276
|
-
const targetEntrypoint = path.join(targetDirectory, 'index.ts');
|
|
277
|
-
const isTargetLocaleExist = existsSync(targetEntrypoint);
|
|
278
|
-
const targetObject = isTargetLocaleExist ? parseLocaleFiles(targetEntrypoint)[0] : {};
|
|
279
|
-
const backupDirectory = targetDirectory + '.bak';
|
|
280
|
-
if (isTargetLocaleExist) {
|
|
281
|
-
await fs.rename(targetDirectory, backupDirectory);
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
consoleLog.warn(`Cannot find ${targetLocale} entrypoint, creating one`);
|
|
285
|
-
}
|
|
286
|
-
await tryThat(traverseNode(baseline, targetObject, path.join(targetDirectory, 'index.ts'), true), (error) => {
|
|
287
|
-
consoleLog.plain();
|
|
288
|
-
consoleLog.error(error);
|
|
289
|
-
consoleLog.plain();
|
|
290
|
-
consoleLog.fatal(`Failed to sync keys for ${targetLocale}, the backup is at ${backupDirectory} for recovery`);
|
|
291
|
-
});
|
|
292
|
-
if (isTargetLocaleExist) {
|
|
293
|
-
await fs.rm(backupDirectory, { recursive: true });
|
|
294
|
-
}
|
|
295
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { languages } from '@logto/language-kit';
|
|
2
|
-
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
-
import PQueue from 'p-queue';
|
|
4
|
-
import { inquireInstancePath, lintLocaleFiles } from '../../utils.js';
|
|
5
|
-
import { baseLanguage, syncTranslation } from './utils.js';
|
|
6
|
-
const sync = {
|
|
7
|
-
command: ['sync'],
|
|
8
|
-
describe: 'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
|
9
|
-
builder: (yargs) => yargs.option('package', {
|
|
10
|
-
alias: 'pkg',
|
|
11
|
-
type: 'string',
|
|
12
|
-
describe: 'The package name of the phrases, e.g. `phrases` or `phrases-experience`',
|
|
13
|
-
default: 'phrases',
|
|
14
|
-
}),
|
|
15
|
-
handler: async ({ path: inputPath, skipCoreCheck, package: packageName }) => {
|
|
16
|
-
const queue = new PQueue({ concurrency: 10 });
|
|
17
|
-
const instancePath = await inquireInstancePath(inputPath, skipCoreCheck);
|
|
18
|
-
const packages = packageName ? [packageName] : ['phrases', 'phrases-experience'];
|
|
19
|
-
for (const languageTag of Object.keys(languages)) {
|
|
20
|
-
if (languageTag === baseLanguage) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const baseOptions = {
|
|
24
|
-
instancePath,
|
|
25
|
-
verbose: false,
|
|
26
|
-
queue,
|
|
27
|
-
};
|
|
28
|
-
for (const packageName of packages) {
|
|
29
|
-
/* eslint-disable no-await-in-loop */
|
|
30
|
-
if (isPhrasesBuiltInLanguageTag(languageTag)) {
|
|
31
|
-
await syncTranslation({
|
|
32
|
-
...baseOptions,
|
|
33
|
-
packageName,
|
|
34
|
-
languageTag,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
/* eslint-enable no-await-in-loop */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
await queue.onIdle();
|
|
41
|
-
void lintLocaleFiles(instancePath, packageName);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
export default sync;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type LanguageTag } from '@logto/language-kit';
|
|
2
|
-
import PQueue from 'p-queue';
|
|
3
|
-
export declare const baseLanguage = "en";
|
|
4
|
-
export declare const readLocaleFiles: (directory: string) => Promise<string[]>;
|
|
5
|
-
export declare const readBaseLocaleFiles: (directory: string) => Promise<string[]>;
|
|
6
|
-
export type TranslationOptions = {
|
|
7
|
-
instancePath: string;
|
|
8
|
-
packageName: string;
|
|
9
|
-
languageTag: LanguageTag;
|
|
10
|
-
verbose?: boolean;
|
|
11
|
-
queue?: PQueue;
|
|
12
|
-
};
|
|
13
|
-
export declare const createFullTranslation: ({ instancePath, packageName, languageTag, verbose, queue, }: TranslationOptions) => Promise<void>;
|
|
14
|
-
export declare const syncTranslation: ({ instancePath, packageName, languageTag, verbose, queue, }: TranslationOptions) => Promise<void>;
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { conditionalString } from '@silverhand/essentials';
|
|
5
|
-
import PQueue from 'p-queue';
|
|
6
|
-
import { consoleLog } from '../../utils.js';
|
|
7
|
-
import { createOpenaiApi, translate } from './openai.js';
|
|
8
|
-
import { untranslatedMark } from './prompts.js';
|
|
9
|
-
export const baseLanguage = 'en';
|
|
10
|
-
export const readLocaleFiles = async (directory) => {
|
|
11
|
-
const entities = await fs.readdir(directory, { withFileTypes: true });
|
|
12
|
-
const result = await Promise.all(entities.map(async (entity) => {
|
|
13
|
-
if (entity.isDirectory()) {
|
|
14
|
-
return readLocaleFiles(path.join(directory, entity.name));
|
|
15
|
-
}
|
|
16
|
-
return entity.name.endsWith('.ts') ? path.join(directory, entity.name) : [];
|
|
17
|
-
}));
|
|
18
|
-
return result.flat();
|
|
19
|
-
};
|
|
20
|
-
export const readBaseLocaleFiles = async (directory) => {
|
|
21
|
-
const enDirectory = path.join(directory, baseLanguage.toLowerCase());
|
|
22
|
-
const stat = await fs.stat(enDirectory);
|
|
23
|
-
if (!stat.isDirectory()) {
|
|
24
|
-
consoleLog.fatal(directory, 'has no `' + baseLanguage.toLowerCase() + '` directory');
|
|
25
|
-
}
|
|
26
|
-
return readLocaleFiles(enDirectory);
|
|
27
|
-
};
|
|
28
|
-
export const createFullTranslation = async ({ instancePath, packageName, languageTag, verbose = true, queue = new PQueue({ concurrency: 10 }), }) => {
|
|
29
|
-
const localeFiles = await getBaseAndTargetLocaleFiles(instancePath, packageName, languageTag);
|
|
30
|
-
if (verbose) {
|
|
31
|
-
consoleLog.info('Found ' +
|
|
32
|
-
String(localeFiles.length) +
|
|
33
|
-
' file' +
|
|
34
|
-
conditionalString(localeFiles.length !== 1 && 's') +
|
|
35
|
-
' in ' +
|
|
36
|
-
packageName +
|
|
37
|
-
' to create');
|
|
38
|
-
}
|
|
39
|
-
const openai = createOpenaiApi();
|
|
40
|
-
for (const { baseLocaleFile, targetLocaleFile } of localeFiles) {
|
|
41
|
-
if (existsSync(targetLocaleFile)) {
|
|
42
|
-
if (verbose) {
|
|
43
|
-
consoleLog.info(`Target locale file ${targetLocaleFile} exists, skipping`);
|
|
44
|
-
}
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
void createLocaleFile({
|
|
48
|
-
api: openai,
|
|
49
|
-
baseLocaleFile,
|
|
50
|
-
targetPath: targetLocaleFile,
|
|
51
|
-
targetLanguage: languageTag,
|
|
52
|
-
queue,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return queue.onIdle();
|
|
56
|
-
};
|
|
57
|
-
export const syncTranslation = async ({ instancePath, packageName, languageTag, verbose = true, queue = new PQueue({ concurrency: 10 }), }) => {
|
|
58
|
-
const localeFiles = await getBaseAndTargetLocaleFiles(instancePath, packageName, languageTag);
|
|
59
|
-
if (verbose) {
|
|
60
|
-
consoleLog.info('Found ' +
|
|
61
|
-
String(localeFiles.length) +
|
|
62
|
-
' file' +
|
|
63
|
-
conditionalString(localeFiles.length !== 1 && 's') +
|
|
64
|
-
' in ' +
|
|
65
|
-
packageName +
|
|
66
|
-
' to translate');
|
|
67
|
-
}
|
|
68
|
-
const openai = createOpenaiApi();
|
|
69
|
-
/* eslint-disable no-await-in-loop */
|
|
70
|
-
for (const { baseLocaleFile, targetLocaleFile } of localeFiles) {
|
|
71
|
-
if (!existsSync(targetLocaleFile)) {
|
|
72
|
-
void createLocaleFile({
|
|
73
|
-
api: openai,
|
|
74
|
-
baseLocaleFile,
|
|
75
|
-
targetPath: targetLocaleFile,
|
|
76
|
-
targetLanguage: languageTag,
|
|
77
|
-
queue,
|
|
78
|
-
});
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const currentContent = await fs.readFile(targetLocaleFile, 'utf8');
|
|
82
|
-
if (!currentContent.includes(untranslatedMark)) {
|
|
83
|
-
if (verbose) {
|
|
84
|
-
consoleLog.info(`Target path ${targetLocaleFile} exists and has no untranslated mark, skipping`);
|
|
85
|
-
}
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
void queue.add(async () => {
|
|
89
|
-
consoleLog.info(`Translating ${targetLocaleFile}`);
|
|
90
|
-
const result = await translate({
|
|
91
|
-
api: openai,
|
|
92
|
-
sourceFilePath: targetLocaleFile,
|
|
93
|
-
targetLanguage: languageTag,
|
|
94
|
-
});
|
|
95
|
-
if (!result) {
|
|
96
|
-
consoleLog.fatal(`Unable to translate ${targetLocaleFile}`);
|
|
97
|
-
}
|
|
98
|
-
await fs.unlink(targetLocaleFile);
|
|
99
|
-
await fs.writeFile(targetLocaleFile, result);
|
|
100
|
-
consoleLog.succeed(`Translated ${targetLocaleFile}`);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
/* eslint-enable no-await-in-loop */
|
|
104
|
-
return queue.onIdle();
|
|
105
|
-
};
|
|
106
|
-
const getBaseAndTargetLocaleFiles = async (instancePath, packageName, languageTag) => {
|
|
107
|
-
const directory = path.join(instancePath, 'packages', packageName, 'src/locales');
|
|
108
|
-
const baseLocaleFiles = await readBaseLocaleFiles(directory);
|
|
109
|
-
return baseLocaleFiles.map((baseLocaleFile) => {
|
|
110
|
-
const basePath = path.relative(path.join(directory, baseLanguage.toLowerCase()), baseLocaleFile);
|
|
111
|
-
return {
|
|
112
|
-
baseLocaleFile,
|
|
113
|
-
targetLocaleFile: path.join(directory, languageTag.toLowerCase(), basePath),
|
|
114
|
-
};
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
const createLocaleFile = async ({ api, baseLocaleFile, targetPath, targetLanguage, queue, }) => queue.add(async () => {
|
|
118
|
-
consoleLog.info(`Creating the translation for ${targetPath}`);
|
|
119
|
-
const result = await translate({
|
|
120
|
-
api,
|
|
121
|
-
sourceFilePath: baseLocaleFile,
|
|
122
|
-
targetLanguage,
|
|
123
|
-
});
|
|
124
|
-
if (!result) {
|
|
125
|
-
consoleLog.fatal(`Unable to create the translation for ${targetPath}`);
|
|
126
|
-
}
|
|
127
|
-
await fs.mkdir(path.parse(targetPath).dir, { recursive: true });
|
|
128
|
-
await fs.writeFile(targetPath, result);
|
|
129
|
-
consoleLog.succeed(`The translation for ${targetPath} created`);
|
|
130
|
-
});
|