@logto/cli 1.1.0 → 1.2.1

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.
Files changed (40) hide show
  1. package/lib/commands/connector/add.js +4 -4
  2. package/lib/commands/connector/index.js +2 -0
  3. package/lib/commands/connector/link.d.ts +9 -0
  4. package/lib/commands/connector/link.js +46 -0
  5. package/lib/commands/connector/list.js +4 -3
  6. package/lib/commands/connector/remove.js +6 -6
  7. package/lib/commands/connector/utils.d.ts +2 -0
  8. package/lib/commands/connector/utils.js +22 -10
  9. package/lib/commands/database/alteration/index.js +8 -8
  10. package/lib/commands/database/alteration/version.js +4 -4
  11. package/lib/commands/database/config.js +11 -11
  12. package/lib/commands/database/seed/cloud.js +2 -2
  13. package/lib/commands/database/seed/index.js +4 -5
  14. package/lib/commands/database/seed/oidc-config.js +5 -5
  15. package/lib/commands/database/seed/tables.js +2 -2
  16. package/lib/commands/database/seed/tenant.js +2 -2
  17. package/lib/commands/database/system.js +5 -5
  18. package/lib/commands/database/utils.js +1 -1
  19. package/lib/commands/install/index.js +3 -3
  20. package/lib/commands/install/utils.js +12 -12
  21. package/lib/commands/translate/create.js +17 -54
  22. package/lib/commands/translate/index.js +2 -0
  23. package/lib/commands/translate/list-tags.js +2 -1
  24. package/lib/commands/translate/openai.d.ts +8 -1
  25. package/lib/commands/translate/openai.js +15 -11
  26. package/lib/commands/translate/prompts.d.ts +14 -0
  27. package/lib/commands/translate/prompts.js +39 -0
  28. package/lib/commands/translate/sync.d.ts +7 -0
  29. package/lib/commands/translate/sync.js +40 -0
  30. package/lib/commands/translate/utils.d.ts +11 -0
  31. package/lib/commands/translate/utils.js +111 -2
  32. package/lib/connector/factories.js +2 -1
  33. package/lib/connector/loader.js +4 -3
  34. package/lib/database.js +3 -3
  35. package/lib/index.js +2 -2
  36. package/lib/package-json.d.ts +4 -0
  37. package/lib/package-json.js +13 -9
  38. package/lib/utils.d.ts +2 -8
  39. package/lib/utils.js +13 -17
  40. 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, log, oraPromise, safeExecSync, } from '../../utils.js';
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
- log.error(`Logto requires NodeJS ${requiredVersionString}, but ${current.version} found.`);
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
- log.warn(`Logto is tested under NodeJS ${requiredVersionString}, but version ${current.version} found.`);
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
- log.error(validated);
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
- log.error('Logto requires a Postgres instance to run.');
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
- log.info(`Download Logto from ${from}`);
76
- log.info(`Target ${tarFilePath}`);
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
- log.error(error);
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
- console.error(error);
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
- log.error('Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' +
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
- log.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`);
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
- log.info(`Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}`);
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 { conditionalString } from '@silverhand/essentials';
8
- import { log } from '../../utils.js';
4
+ import { consoleLog } from '../../utils.js';
9
5
  import { inquireInstancePath } from '../connector/utils.js';
10
- import { createOpenaiApi, translate } from './openai.js';
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
- log.error('Invalid language tag. Run `logto translate list-tags` to see available list.');
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
- log.info(languageTag + ' is a built-in tag of phrases, updating untranslated phrases');
21
+ consoleLog.info(languageTag + ' is a built-in tag of phrases, updating untranslated phrases');
67
22
  }
68
- await createFullTranslation(inputPath, 'phrases', languageTag);
23
+ await createFullTranslation({
24
+ instancePath,
25
+ packageName: 'phrases',
26
+ languageTag,
27
+ });
69
28
  if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
70
- log.info(languageTag + ' is a built-in tag of phrases-ui, updating untranslated phrases');
29
+ consoleLog.info(languageTag + ' is a built-in tag of phrases-ui, updating untranslated phrases');
71
30
  }
72
- await createFullTranslation(inputPath, 'phrases-ui', languageTag);
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
- console.log(...[
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
- export declare const translate: (api: Got, languageTag: LanguageTag, filePath: string) => Promise<string | undefined>;
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 { getProxy, log } from '../../utils.js';
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, languageTag, filePath) => {
28
- const fileContent = await fs.readFile(filePath, 'utf8');
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: `Given the following code snippet, only translate object values to ${languages[languageTag]}, keep all object keys original, output ts code only: \n \`\`\`ts\n${fileContent}\n\`\`\``,
36
+ content: getTranslationPrompt({
37
+ sourceFileContent,
38
+ targetLanguage,
39
+ extraPrompt,
40
+ }),
37
41
  },
38
42
  ],
39
43
  },
40
44
  })
41
45
  .json(), (error) => {
42
- log.warn(`Error while translating ${filePath}:`, String(error));
46
+ consoleLog.warn(`Error while translating ${sourceFilePath}:`, String(error));
43
47
  if (error instanceof HTTPError) {
44
- log.warn(error.response.body);
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
- log.warn(`Error while guarding response for ${filePath}:`, response);
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
- log.warn(`No choice found in response when translating ${filePath}`);
61
+ consoleLog.warn(`No choice found in response when translating ${sourceFilePath}`);
58
62
  return;
59
63
  }
60
64
  if (entity.finish_reason !== 'stop') {
61
- log.warn(`Unexpected finish reason ${entity.finish_reason} for ${filePath}`);
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
- log.warn('No matching code snippet from response:', content);
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,7 @@
1
+ import type { CommandModule } from 'yargs';
2
+ declare const sync: CommandModule<{
3
+ path?: string;
4
+ }, {
5
+ path?: string;
6
+ }>;
7
+ export default sync;
@@ -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 { log } from '../../utils.js';
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
- log.error(directory, 'has no `' + baseLanguage.toLowerCase() + '` directory');
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
- console.log(`${chalk.red(`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`)}`);
26
+ consoleLog.error(`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`);
26
27
  return;
27
28
  }
28
29
  throw error;
@@ -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
- console.warn(`[warn] ${message}\n\nThis is highly discouraged in production.`);
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
- console.log(`[warn] Load connector ${connectorPath} in CJS mode`);
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, getCliConfigWithPrompt, log } from './utils.js';
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
- log.error(error);
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
- log.succeed(`Created database ${databaseName}`);
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
- console.log(packageJson.name + ' v' + packageJson.version);
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
  }
@@ -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;