@logto/cli 1.1.0 → 1.2.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/connector/add.js +4 -4
- package/lib/commands/connector/index.js +2 -0
- package/lib/commands/connector/link.d.ts +9 -0
- package/lib/commands/connector/link.js +46 -0
- package/lib/commands/connector/list.js +4 -3
- package/lib/commands/connector/remove.js +6 -6
- package/lib/commands/connector/utils.d.ts +2 -0
- package/lib/commands/connector/utils.js +22 -10
- package/lib/commands/database/alteration/index.js +8 -8
- package/lib/commands/database/alteration/version.js +4 -4
- package/lib/commands/database/config.js +11 -11
- package/lib/commands/database/seed/cloud.js +2 -2
- package/lib/commands/database/seed/index.js +4 -5
- package/lib/commands/database/seed/oidc-config.js +5 -5
- package/lib/commands/database/seed/tables.js +2 -2
- package/lib/commands/database/seed/tenant.js +2 -2
- package/lib/commands/database/system.js +5 -5
- package/lib/commands/database/utils.js +1 -1
- package/lib/commands/install/index.js +3 -3
- package/lib/commands/install/utils.js +12 -12
- package/lib/commands/translate/create.js +17 -54
- package/lib/commands/translate/index.js +2 -0
- package/lib/commands/translate/list-tags.js +2 -1
- package/lib/commands/translate/openai.d.ts +8 -1
- package/lib/commands/translate/openai.js +15 -11
- package/lib/commands/translate/prompts.d.ts +14 -0
- package/lib/commands/translate/prompts.js +39 -0
- package/lib/commands/translate/sync.d.ts +7 -0
- package/lib/commands/translate/sync.js +40 -0
- package/lib/commands/translate/utils.d.ts +11 -0
- package/lib/commands/translate/utils.js +111 -2
- package/lib/connector/factories.js +2 -1
- package/lib/connector/loader.js +4 -3
- package/lib/database.js +3 -3
- package/lib/index.js +2 -2
- package/lib/package-json.d.ts +4 -0
- package/lib/package-json.js +13 -9
- package/lib/utils.d.ts +2 -8
- package/lib/utils.js +13 -17
- package/package.json +14 -10
|
@@ -10,7 +10,7 @@ import * as semver from 'semver';
|
|
|
10
10
|
import tar from 'tar';
|
|
11
11
|
import { createPoolAndDatabaseIfNeeded } from '../../database.js';
|
|
12
12
|
import { packageJson } from '../../package-json.js';
|
|
13
|
-
import { cliConfig, ConfigKey, downloadFile, isTty,
|
|
13
|
+
import { cliConfig, ConfigKey, consoleLog, downloadFile, isTty, oraPromise, safeExecSync, } from '../../utils.js';
|
|
14
14
|
import { seedByPool } from '../database/seed/index.js';
|
|
15
15
|
export const defaultPath = path.join(os.homedir(), 'logto');
|
|
16
16
|
const pgRequired = new semver.SemVer('14.0.0');
|
|
@@ -19,10 +19,10 @@ export const validateNodeVersion = () => {
|
|
|
19
19
|
const requiredVersionString = required.map((version) => '^' + version.version).join(' || ');
|
|
20
20
|
const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' }));
|
|
21
21
|
if (required.every((version) => version.major !== current.major)) {
|
|
22
|
-
|
|
22
|
+
consoleLog.fatal(`Logto requires NodeJS ${requiredVersionString}, but ${current.version} found.`);
|
|
23
23
|
}
|
|
24
24
|
if (required.some((version) => version.major === current.major && version.compare(current) > 0)) {
|
|
25
|
-
|
|
25
|
+
consoleLog.warn(`Logto is tested under NodeJS ${requiredVersionString}, but version ${current.version} found.`);
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
const validatePath = (value) => existsSync(path.resolve(value))
|
|
@@ -44,7 +44,7 @@ export const inquireInstancePath = async (initialPath) => {
|
|
|
44
44
|
// Validate for initialPath
|
|
45
45
|
const validated = validatePath(instancePath);
|
|
46
46
|
if (validated !== true) {
|
|
47
|
-
|
|
47
|
+
consoleLog.fatal(validated);
|
|
48
48
|
}
|
|
49
49
|
return instancePath;
|
|
50
50
|
};
|
|
@@ -65,15 +65,15 @@ export const validateDatabase = async () => {
|
|
|
65
65
|
},
|
|
66
66
|
});
|
|
67
67
|
if (hasPostgresUrl === false) {
|
|
68
|
-
|
|
68
|
+
consoleLog.fatal('Logto requires a Postgres instance to run.');
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
export const downloadRelease = async (url) => {
|
|
72
72
|
const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz');
|
|
73
73
|
const from = url ??
|
|
74
74
|
`https://github.com/logto-io/logto/releases/download/v${packageJson.version}/logto.tar.gz`;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
consoleLog.info(`Download Logto from ${from}`);
|
|
76
|
+
consoleLog.info(`Target ${tarFilePath}`);
|
|
77
77
|
await downloadFile(from, tarFilePath);
|
|
78
78
|
return tarFilePath;
|
|
79
79
|
};
|
|
@@ -84,7 +84,7 @@ export const decompress = async (toPath, tarPath) => {
|
|
|
84
84
|
await tar.extract({ file: tarPath, cwd: toPath, strip: 1 });
|
|
85
85
|
}
|
|
86
86
|
catch (error) {
|
|
87
|
-
|
|
87
|
+
consoleLog.fatal(error);
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
90
|
return oraPromise(run(), {
|
|
@@ -99,23 +99,23 @@ export const seedDatabase = async (instancePath, cloud) => {
|
|
|
99
99
|
await pool.end();
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
102
|
-
|
|
102
|
+
consoleLog.error(error);
|
|
103
103
|
await oraPromise(fs.rm(instancePath, { force: true, recursive: true }), {
|
|
104
104
|
text: 'Clean up',
|
|
105
105
|
prefixText: chalk.blue('[info]'),
|
|
106
106
|
});
|
|
107
|
-
|
|
107
|
+
consoleLog.fatal('Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' +
|
|
108
108
|
` To skip the database seeding, append ${chalk.green('--skip-seed')} to the command options.`);
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
export const createEnv = async (instancePath, databaseUrl) => {
|
|
112
112
|
const dotEnvPath = path.resolve(instancePath, '.env');
|
|
113
113
|
await fs.writeFile(dotEnvPath, `DB_URL=${databaseUrl}`, 'utf8');
|
|
114
|
-
|
|
114
|
+
consoleLog.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`);
|
|
115
115
|
};
|
|
116
116
|
export const logFinale = (instancePath) => {
|
|
117
117
|
const startCommand = `cd ${instancePath} && npm start`;
|
|
118
|
-
|
|
118
|
+
consoleLog.info(`Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}`);
|
|
119
119
|
};
|
|
120
120
|
export const isUrl = (string) => {
|
|
121
121
|
try {
|
|
@@ -1,58 +1,12 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
1
|
import { isLanguageTag } from '@logto/language-kit';
|
|
5
2
|
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
6
3
|
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
|
|
7
|
-
import {
|
|
8
|
-
import { log } from '../../utils.js';
|
|
4
|
+
import { consoleLog } from '../../utils.js';
|
|
9
5
|
import { inquireInstancePath } from '../connector/utils.js';
|
|
10
|
-
import {
|
|
11
|
-
import { baseLanguage, readBaseLocaleFiles } from './utils.js';
|
|
12
|
-
const createFullTranslation = async (instancePath, packageName, languageTag) => {
|
|
13
|
-
const directory = path.join(await inquireInstancePath(instancePath), 'packages', packageName, 'src/locales');
|
|
14
|
-
const files = await readBaseLocaleFiles(directory);
|
|
15
|
-
log.info('Found ' +
|
|
16
|
-
String(files.length) +
|
|
17
|
-
' file' +
|
|
18
|
-
conditionalString(files.length !== 1 && 's') +
|
|
19
|
-
' in ' +
|
|
20
|
-
packageName +
|
|
21
|
-
' to translate');
|
|
22
|
-
const openai = createOpenaiApi();
|
|
23
|
-
/* eslint-disable no-await-in-loop */
|
|
24
|
-
for (const file of files) {
|
|
25
|
-
const relativePath = path.relative(path.join(directory, baseLanguage.toLowerCase()), file);
|
|
26
|
-
const targetPath = path.join(directory, languageTag.toLowerCase(), relativePath);
|
|
27
|
-
const getTranslationPath = async () => {
|
|
28
|
-
if (existsSync(targetPath)) {
|
|
29
|
-
const currentContent = await fs.readFile(targetPath, 'utf8');
|
|
30
|
-
if (currentContent.includes('// UNTRANSLATED')) {
|
|
31
|
-
return targetPath;
|
|
32
|
-
}
|
|
33
|
-
log.info(`Target path ${targetPath} exists and has no untranslated mark, skipping`);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
return file;
|
|
37
|
-
};
|
|
38
|
-
const translationPath = await getTranslationPath();
|
|
39
|
-
if (!translationPath) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
log.info(`Translating ${translationPath}`);
|
|
43
|
-
const result = await translate(openai, languageTag, translationPath);
|
|
44
|
-
if (!result) {
|
|
45
|
-
log.error(`Unable to translate ${translationPath}`);
|
|
46
|
-
}
|
|
47
|
-
await fs.mkdir(path.parse(targetPath).dir, { recursive: true });
|
|
48
|
-
await fs.writeFile(targetPath, result);
|
|
49
|
-
log.succeed(`Translated ${targetPath}`);
|
|
50
|
-
}
|
|
51
|
-
/* eslint-enable no-await-in-loop */
|
|
52
|
-
};
|
|
6
|
+
import { createFullTranslation } from './utils.js';
|
|
53
7
|
const create = {
|
|
54
8
|
command: ['create <language-tag>', 'c'],
|
|
55
|
-
describe: 'Create a new language translation',
|
|
9
|
+
describe: 'Create a new language translation using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
|
56
10
|
builder: (yargs) => yargs.positional('language-tag', {
|
|
57
11
|
describe: 'The language tag to create, e.g. `af-ZA`.',
|
|
58
12
|
type: 'string',
|
|
@@ -60,16 +14,25 @@ const create = {
|
|
|
60
14
|
}),
|
|
61
15
|
handler: async ({ path: inputPath, languageTag }) => {
|
|
62
16
|
if (!isLanguageTag(languageTag)) {
|
|
63
|
-
|
|
17
|
+
consoleLog.fatal('Invalid language tag. Run `logto translate list-tags` to see available list.');
|
|
64
18
|
}
|
|
19
|
+
const instancePath = await inquireInstancePath(inputPath);
|
|
65
20
|
if (isPhrasesBuiltInLanguageTag(languageTag)) {
|
|
66
|
-
|
|
21
|
+
consoleLog.info(languageTag + ' is a built-in tag of phrases, updating untranslated phrases');
|
|
67
22
|
}
|
|
68
|
-
await createFullTranslation(
|
|
23
|
+
await createFullTranslation({
|
|
24
|
+
instancePath,
|
|
25
|
+
packageName: 'phrases',
|
|
26
|
+
languageTag,
|
|
27
|
+
});
|
|
69
28
|
if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
|
|
70
|
-
|
|
29
|
+
consoleLog.info(languageTag + ' is a built-in tag of phrases-ui, updating untranslated phrases');
|
|
71
30
|
}
|
|
72
|
-
await createFullTranslation(
|
|
31
|
+
await createFullTranslation({
|
|
32
|
+
instancePath,
|
|
33
|
+
packageName: 'phrases-ui',
|
|
34
|
+
languageTag,
|
|
35
|
+
});
|
|
73
36
|
},
|
|
74
37
|
};
|
|
75
38
|
export default create;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noop } from '@silverhand/essentials';
|
|
2
2
|
import create from './create.js';
|
|
3
3
|
import listTags from './list-tags.js';
|
|
4
|
+
import sync from './sync.js';
|
|
4
5
|
const translate = {
|
|
5
6
|
command: ['translate', 't'],
|
|
6
7
|
describe: 'Commands for Logto translation',
|
|
@@ -12,6 +13,7 @@ const translate = {
|
|
|
12
13
|
})
|
|
13
14
|
.command(create)
|
|
14
15
|
.command(listTags)
|
|
16
|
+
.command(sync)
|
|
15
17
|
.demandCommand(1),
|
|
16
18
|
handler: noop,
|
|
17
19
|
};
|
|
@@ -2,12 +2,13 @@ import { languages } from '@logto/language-kit';
|
|
|
2
2
|
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
3
|
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
+
import { consoleLog } from '../../utils.js';
|
|
5
6
|
const listTags = {
|
|
6
7
|
command: ['list-tags', 'list'],
|
|
7
8
|
describe: 'List all available language tags',
|
|
8
9
|
handler: async () => {
|
|
9
10
|
for (const tag of Object.keys(languages)) {
|
|
10
|
-
|
|
11
|
+
consoleLog.plain(...[
|
|
11
12
|
tag,
|
|
12
13
|
isPhrasesBuiltInLanguageTag(tag) && chalk.blue('phrases'),
|
|
13
14
|
isPhrasesUiBuiltInLanguageTag(tag) && chalk.blue('phrases-ui'),
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type LanguageTag } from '@logto/language-kit';
|
|
2
2
|
import { type Got } from 'got';
|
|
3
3
|
export declare const createOpenaiApi: () => Got;
|
|
4
|
-
|
|
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,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { languages } from '@logto/language-kit';
|
|
3
2
|
import { trySafe } from '@silverhand/essentials';
|
|
4
3
|
import { got, HTTPError } from 'got';
|
|
5
4
|
import { HttpsProxyAgent } from 'hpagent';
|
|
6
5
|
import { z } from 'zod';
|
|
7
|
-
import {
|
|
6
|
+
import { consoleLog, getProxy } from '../../utils.js';
|
|
7
|
+
import { getTranslationPrompt } from './prompts.js';
|
|
8
8
|
export const createOpenaiApi = () => {
|
|
9
9
|
const proxy = getProxy();
|
|
10
10
|
return got.extend({
|
|
@@ -24,8 +24,8 @@ const gptResponseGuard = z.object({
|
|
|
24
24
|
})
|
|
25
25
|
.array(),
|
|
26
26
|
});
|
|
27
|
-
export const translate = async (api,
|
|
28
|
-
const
|
|
27
|
+
export const translate = async ({ api, targetLanguage, sourceFilePath, extraPrompt, }) => {
|
|
28
|
+
const sourceFileContent = await fs.readFile(sourceFilePath, 'utf8');
|
|
29
29
|
const response = await trySafe(api
|
|
30
30
|
.post('chat/completions', {
|
|
31
31
|
json: {
|
|
@@ -33,15 +33,19 @@ export const translate = async (api, languageTag, filePath) => {
|
|
|
33
33
|
messages: [
|
|
34
34
|
{
|
|
35
35
|
role: 'user',
|
|
36
|
-
content:
|
|
36
|
+
content: getTranslationPrompt({
|
|
37
|
+
sourceFileContent,
|
|
38
|
+
targetLanguage,
|
|
39
|
+
extraPrompt,
|
|
40
|
+
}),
|
|
37
41
|
},
|
|
38
42
|
],
|
|
39
43
|
},
|
|
40
44
|
})
|
|
41
45
|
.json(), (error) => {
|
|
42
|
-
|
|
46
|
+
consoleLog.warn(`Error while translating ${sourceFilePath}:`, String(error));
|
|
43
47
|
if (error instanceof HTTPError) {
|
|
44
|
-
|
|
48
|
+
consoleLog.warn(error.response.body);
|
|
45
49
|
}
|
|
46
50
|
});
|
|
47
51
|
if (!response) {
|
|
@@ -49,16 +53,16 @@ export const translate = async (api, languageTag, filePath) => {
|
|
|
49
53
|
}
|
|
50
54
|
const guarded = gptResponseGuard.safeParse(response);
|
|
51
55
|
if (!guarded.success) {
|
|
52
|
-
|
|
56
|
+
consoleLog.warn(`Error while guarding response for ${sourceFilePath}:`, response);
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
59
|
const [entity] = guarded.data.choices;
|
|
56
60
|
if (!entity) {
|
|
57
|
-
|
|
61
|
+
consoleLog.warn(`No choice found in response when translating ${sourceFilePath}`);
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
64
|
if (entity.finish_reason !== 'stop') {
|
|
61
|
-
|
|
65
|
+
consoleLog.warn(`Unexpected finish reason ${entity.finish_reason} for ${sourceFilePath}`);
|
|
62
66
|
}
|
|
63
67
|
const { content } = entity.message;
|
|
64
68
|
const matched = /```(?:ts)?\n(.*)```/s.exec(content)?.[1];
|
|
@@ -67,7 +71,7 @@ export const translate = async (api, languageTag, filePath) => {
|
|
|
67
71
|
if (['const ', 'import '].some((prefix) => content.startsWith(prefix))) {
|
|
68
72
|
return content;
|
|
69
73
|
}
|
|
70
|
-
|
|
74
|
+
consoleLog.warn('No matching code snippet from response:', content);
|
|
71
75
|
}
|
|
72
76
|
return matched;
|
|
73
77
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type LanguageTag } from '@logto/language-kit';
|
|
2
|
+
type GetTranslationPromptProperties = {
|
|
3
|
+
sourceFileContent: string;
|
|
4
|
+
targetLanguage: LanguageTag;
|
|
5
|
+
extraPrompt?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Note:
|
|
9
|
+
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1200.
|
|
10
|
+
* Remember to check the token limit before adding more prompt.
|
|
11
|
+
* Tokens can be counted in https://platform.openai.com/tokenizer
|
|
12
|
+
*/
|
|
13
|
+
export declare const getTranslationPrompt: ({ sourceFileContent, targetLanguage, extraPrompt, }: GetTranslationPromptProperties) => string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { languages } from '@logto/language-kit';
|
|
2
|
+
import { conditionalString } from '@silverhand/essentials';
|
|
3
|
+
/**
|
|
4
|
+
* Note:
|
|
5
|
+
* The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1200.
|
|
6
|
+
* Remember to check the token limit before adding more prompt.
|
|
7
|
+
* Tokens can be counted in https://platform.openai.com/tokenizer
|
|
8
|
+
*/
|
|
9
|
+
export const getTranslationPrompt = ({ sourceFileContent, targetLanguage, extraPrompt, }) => `Given the following code snippet:
|
|
10
|
+
\`\`\`ts
|
|
11
|
+
${sourceFileContent}
|
|
12
|
+
\`\`\`
|
|
13
|
+
only translate object values to ${languages[targetLanguage]}, keep all object keys original, output ts code only, the code format should be strictly consistent, and should not contains the given code snippet.
|
|
14
|
+
|
|
15
|
+
Take zh-cn as an example, if the input is:
|
|
16
|
+
\`\`\`ts
|
|
17
|
+
import others from './others.js';
|
|
18
|
+
|
|
19
|
+
const translation = {
|
|
20
|
+
hello: '你好',
|
|
21
|
+
world: 'world', // UNTRANSLATED
|
|
22
|
+
others,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default translation;
|
|
26
|
+
\`\`\`
|
|
27
|
+
the output should be:
|
|
28
|
+
\`\`\`ts
|
|
29
|
+
import others from './others.js';
|
|
30
|
+
|
|
31
|
+
const translation = {
|
|
32
|
+
hello: '你好',
|
|
33
|
+
world: '世界',
|
|
34
|
+
others,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default translation;
|
|
38
|
+
\`\`\`
|
|
39
|
+
${conditionalString(extraPrompt)}`;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { languages } from '@logto/language-kit';
|
|
2
|
+
import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
|
|
3
|
+
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
|
|
4
|
+
import PQueue from 'p-queue';
|
|
5
|
+
import { inquireInstancePath } from '../connector/utils.js';
|
|
6
|
+
import { baseLanguage, syncTranslation } from './utils.js';
|
|
7
|
+
const sync = {
|
|
8
|
+
command: ['sync'],
|
|
9
|
+
describe: 'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.',
|
|
10
|
+
handler: async ({ path: inputPath }) => {
|
|
11
|
+
const queue = new PQueue({ concurrency: 5 });
|
|
12
|
+
const instancePath = await inquireInstancePath(inputPath);
|
|
13
|
+
for (const languageTag of Object.keys(languages)) {
|
|
14
|
+
if (languageTag === baseLanguage) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const baseOptions = {
|
|
18
|
+
instancePath,
|
|
19
|
+
verbose: false,
|
|
20
|
+
queue,
|
|
21
|
+
};
|
|
22
|
+
if (isPhrasesBuiltInLanguageTag(languageTag)) {
|
|
23
|
+
void syncTranslation({
|
|
24
|
+
...baseOptions,
|
|
25
|
+
packageName: 'phrases',
|
|
26
|
+
languageTag,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
|
|
30
|
+
void syncTranslation({
|
|
31
|
+
...baseOptions,
|
|
32
|
+
packageName: 'phrases-ui',
|
|
33
|
+
languageTag,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await queue.onIdle();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default sync;
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { type LanguageTag } from '@logto/language-kit';
|
|
2
|
+
import PQueue from 'p-queue';
|
|
1
3
|
export declare const baseLanguage = "en";
|
|
2
4
|
export declare const readLocaleFiles: (directory: string) => Promise<string[]>;
|
|
3
5
|
export declare const readBaseLocaleFiles: (directory: string) => Promise<string[]>;
|
|
6
|
+
export type TranslationOptions = {
|
|
7
|
+
instancePath: string;
|
|
8
|
+
packageName: 'phrases' | 'phrases-ui';
|
|
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,7 +1,12 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
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';
|
|
4
8
|
export const baseLanguage = 'en';
|
|
9
|
+
const untranslatedMark = '// UNTRANSLATED';
|
|
5
10
|
export const readLocaleFiles = async (directory) => {
|
|
6
11
|
const entities = await fs.readdir(directory, { withFileTypes: true });
|
|
7
12
|
const result = await Promise.all(entities.map(async (entity) => {
|
|
@@ -16,7 +21,111 @@ export const readBaseLocaleFiles = async (directory) => {
|
|
|
16
21
|
const enDirectory = path.join(directory, baseLanguage.toLowerCase());
|
|
17
22
|
const stat = await fs.stat(enDirectory);
|
|
18
23
|
if (!stat.isDirectory()) {
|
|
19
|
-
|
|
24
|
+
consoleLog.fatal(directory, 'has no `' + baseLanguage.toLowerCase() + '` directory');
|
|
20
25
|
}
|
|
21
26
|
return readLocaleFiles(enDirectory);
|
|
22
27
|
};
|
|
28
|
+
export const createFullTranslation = async ({ instancePath, packageName, languageTag, verbose = true, queue = new PQueue({ concurrency: 5 }), }) => {
|
|
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: 5 }), }) => {
|
|
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
|
+
extraPrompt: `Object values without an "${untranslatedMark}" mark should be skipped and keep its original value. Remember to remove the "${untranslatedMark}" mark with the spaces before and after it in the output content.`,
|
|
95
|
+
});
|
|
96
|
+
if (!result) {
|
|
97
|
+
consoleLog.fatal(`Unable to translate ${targetLocaleFile}`);
|
|
98
|
+
}
|
|
99
|
+
await fs.unlink(targetLocaleFile);
|
|
100
|
+
await fs.writeFile(targetLocaleFile, result);
|
|
101
|
+
consoleLog.succeed(`Translated ${targetLocaleFile}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/* eslint-enable no-await-in-loop */
|
|
105
|
+
return queue.onIdle();
|
|
106
|
+
};
|
|
107
|
+
const getBaseAndTargetLocaleFiles = async (instancePath, packageName, languageTag) => {
|
|
108
|
+
const directory = path.join(instancePath, 'packages', packageName, 'src/locales');
|
|
109
|
+
const baseLocaleFiles = await readBaseLocaleFiles(directory);
|
|
110
|
+
return baseLocaleFiles.map((baseLocaleFile) => {
|
|
111
|
+
const basePath = path.relative(path.join(directory, baseLanguage.toLowerCase()), baseLocaleFile);
|
|
112
|
+
return {
|
|
113
|
+
baseLocaleFile,
|
|
114
|
+
targetLocaleFile: path.join(directory, languageTag.toLowerCase(), basePath),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const createLocaleFile = async ({ api, baseLocaleFile, targetPath, targetLanguage, queue, }) => queue.add(async () => {
|
|
119
|
+
consoleLog.info(`Creating the translation for ${targetPath}`);
|
|
120
|
+
const result = await translate({
|
|
121
|
+
api,
|
|
122
|
+
sourceFilePath: baseLocaleFile,
|
|
123
|
+
targetLanguage,
|
|
124
|
+
});
|
|
125
|
+
if (!result) {
|
|
126
|
+
consoleLog.fatal(`Unable to create the translation for ${targetPath}`);
|
|
127
|
+
}
|
|
128
|
+
await fs.mkdir(path.parse(targetPath).dir, { recursive: true });
|
|
129
|
+
await fs.writeFile(targetPath, result);
|
|
130
|
+
consoleLog.succeed(`The translation for ${targetPath} created`);
|
|
131
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { consoleLog } from '../utils.js';
|
|
2
3
|
import { notImplemented } from './consts.js';
|
|
3
4
|
import { loadConnector } from './loader.js';
|
|
4
5
|
import { parseMetadata, validateConnectorModule } from './utils.js';
|
|
@@ -22,7 +23,7 @@ export const loadConnectorFactories = async (connectorPackages, ignoreVersionMis
|
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
if (error instanceof Error) {
|
|
25
|
-
|
|
26
|
+
consoleLog.error(`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`);
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
throw error;
|
package/lib/connector/loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import connectorKitMeta from '@logto/connector-kit/package.json' assert { type: 'json' };
|
|
3
3
|
import { satisfies } from 'semver';
|
|
4
|
+
import { consoleLog } from '../utils.js';
|
|
4
5
|
import { isKeyInObject } from './utils.js';
|
|
5
6
|
const connectorKit = '@logto/connector-kit';
|
|
6
7
|
const { version: currentVersion } = connectorKitMeta;
|
|
@@ -8,12 +9,12 @@ const checkConnectorKitVersion = (dependencies, ignoreVersionMismatch) => {
|
|
|
8
9
|
if (isKeyInObject(dependencies, connectorKit)) {
|
|
9
10
|
const value = dependencies[connectorKit];
|
|
10
11
|
if (typeof value === 'string') {
|
|
11
|
-
if (satisfies(currentVersion, value)) {
|
|
12
|
+
if (value.startsWith('workspace:') || satisfies(currentVersion, value)) {
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
const message = `Connector requires ${connectorKit} to be ${value}, but the version here is ${currentVersion}.`;
|
|
15
16
|
if (ignoreVersionMismatch) {
|
|
16
|
-
|
|
17
|
+
consoleLog.warn(`${message}\n\nThis is highly discouraged in production.`);
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
throw new Error(message);
|
|
@@ -33,7 +34,7 @@ export const loadConnector = async (connectorPath, ignoreVersionMismatch) => {
|
|
|
33
34
|
// CJS pattern
|
|
34
35
|
if (isKeyInObject(loaded.default, 'default')) {
|
|
35
36
|
if (typeof loaded.default.default === 'function') {
|
|
36
|
-
|
|
37
|
+
consoleLog.warn(`Load connector ${connectorPath} in CJS mode`);
|
|
37
38
|
// eslint-disable-next-line no-restricted-syntax
|
|
38
39
|
return loaded.default.default;
|
|
39
40
|
}
|
package/lib/database.js
CHANGED
|
@@ -4,7 +4,7 @@ import decamelize from 'decamelize';
|
|
|
4
4
|
import { DatabaseError } from 'pg-protocol';
|
|
5
5
|
import { createPool, parseDsn, sql, stringifyDsn } from 'slonik';
|
|
6
6
|
import { createInterceptors } from 'slonik-interceptor-preset';
|
|
7
|
-
import { ConfigKey,
|
|
7
|
+
import { ConfigKey, consoleLog, getCliConfigWithPrompt } from './utils.js';
|
|
8
8
|
export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto';
|
|
9
9
|
export const getDatabaseUrlFromConfig = async () => (await getCliConfigWithPrompt({
|
|
10
10
|
key: ConfigKey.DatabaseUrl,
|
|
@@ -32,7 +32,7 @@ export const createPoolAndDatabaseIfNeeded = async () => {
|
|
|
32
32
|
// Database does not exist, try to create one
|
|
33
33
|
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
34
34
|
if (!(error instanceof DatabaseError && error.code === '3D000')) {
|
|
35
|
-
|
|
35
|
+
consoleLog.fatal(error);
|
|
36
36
|
}
|
|
37
37
|
const databaseUrl = await getDatabaseUrlFromConfig();
|
|
38
38
|
const dsn = parseDsn(databaseUrl);
|
|
@@ -50,7 +50,7 @@ export const createPoolAndDatabaseIfNeeded = async () => {
|
|
|
50
50
|
connection_limit = -1;
|
|
51
51
|
`);
|
|
52
52
|
await maintenancePool.end();
|
|
53
|
-
|
|
53
|
+
consoleLog.succeed(`Created database ${databaseName}`);
|
|
54
54
|
return createPoolFromConfig();
|
|
55
55
|
}
|
|
56
56
|
};
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import database from './commands/database/index.js';
|
|
|
7
7
|
import install from './commands/install/index.js';
|
|
8
8
|
import translate from './commands/translate/index.js';
|
|
9
9
|
import { packageJson } from './package-json.js';
|
|
10
|
-
import { cliConfig, ConfigKey } from './utils.js';
|
|
10
|
+
import { cliConfig, ConfigKey, consoleLog } from './utils.js';
|
|
11
11
|
void yargs(hideBin(process.argv))
|
|
12
12
|
.version(false)
|
|
13
13
|
.option('env', {
|
|
@@ -28,7 +28,7 @@ void yargs(hideBin(process.argv))
|
|
|
28
28
|
})
|
|
29
29
|
.middleware(({ version }) => {
|
|
30
30
|
if (version) {
|
|
31
|
-
|
|
31
|
+
consoleLog.plain(packageJson.name + ' v' + packageJson.version);
|
|
32
32
|
// eslint-disable-next-line unicorn/no-process-exit
|
|
33
33
|
process.exit(0);
|
|
34
34
|
}
|
package/lib/package-json.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export declare const packageJson: {
|
|
|
57
57
|
nanoid: string;
|
|
58
58
|
ora: string;
|
|
59
59
|
"p-limit": string;
|
|
60
|
+
"p-queue": string;
|
|
60
61
|
"p-retry": string;
|
|
61
62
|
"pg-protocol": string;
|
|
62
63
|
roarr: string;
|
|
@@ -87,6 +88,9 @@ export declare const packageJson: {
|
|
|
87
88
|
};
|
|
88
89
|
eslintConfig: {
|
|
89
90
|
extends: string;
|
|
91
|
+
rules: {
|
|
92
|
+
"no-console": string;
|
|
93
|
+
};
|
|
90
94
|
ignorePatterns: string[];
|
|
91
95
|
};
|
|
92
96
|
prettier: string;
|