locale-sync-cli 1.0.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.
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // Polyfill Web APIs (fetch, Headers, FormData, etc.) for Node < 18
3
+ if (typeof globalThis.fetch === 'undefined') {
4
+ const undici = require('undici');
5
+ const globals = ['fetch','Headers','Request','Response','FormData','File'];
6
+ for (const k of globals) { if (undici[k]) globalThis[k] = undici[k]; }
7
+ }
8
+ if (typeof globalThis.Blob === 'undefined') {
9
+ globalThis.Blob = require('buffer').Blob;
10
+ }
11
+ if (typeof globalThis.ReadableStream === 'undefined') {
12
+ const webStreams = require('stream/web');
13
+ const streamGlobals = ['ReadableStream','WritableStream','TransformStream'];
14
+ for (const k of streamGlobals) { if (webStreams[k]) globalThis[k] = webStreams[k]; }
15
+ }
16
+
17
+ const { Command } = require('commander');
18
+ const pkg = require('../package.json');
19
+ const chalk = require('chalk');
20
+
21
+ const program = new Command();
22
+ program
23
+ .name('locale-sync')
24
+ .description('Enterprise i18n Sync CLI')
25
+ .version(pkg.version, '-v, --version');
26
+
27
+ program
28
+ .command('set-credentials')
29
+ .description('🔑 Save Google service-account credentials to local env var')
30
+ .action(require('../lib/cli/commands/set-credentials'));
31
+
32
+ program
33
+ .command('init')
34
+ .description(' Initialize project config')
35
+ .action(require('../lib/cli/commands/init'));
36
+
37
+ program
38
+ .command('push')
39
+ .alias('p')
40
+ .description('📤 Local → Google Sheets')
41
+ .option('-y, --yes', 'Skip confirmation prompt')
42
+ .option('-v, --verbose', 'Enable verbose logging')
43
+ .action(require('../lib/cli/commands/push'));
44
+
45
+ program
46
+ .command('pull')
47
+ .alias('l')
48
+ .description('📥 Google Sheets → Local')
49
+ .option('-y, --yes', 'Skip confirmation prompt')
50
+ .option('-v, --verbose', 'Enable verbose logging')
51
+ .action(require('../lib/cli/commands/pull'));
52
+
53
+ program
54
+ .command('ignore [patterns...]')
55
+ .description('🚫 Manage ignore patterns (files/folders/extensions to skip during scan)')
56
+ .option('-l, --list', 'List current ignore patterns')
57
+ .option('-r, --remove', 'Remove the specified patterns instead of adding')
58
+ .action(require('../lib/cli/commands/ignore'));
59
+
60
+ program
61
+ .description('⚙️ Show current configuration')
62
+ .action(async () => {
63
+ const { loadConfig } = require('../lib/config/manager');
64
+ console.log(chalk.bold('\n📋 Current Config:'));
65
+ console.log(JSON.stringify(await loadConfig(), null, 2));
66
+ });
67
+
68
+ program.parse(process.argv);
69
+
70
+ if (!process.argv.slice(2).length) {
71
+ program.outputHelp();
72
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "project": "${package.json.name}",
3
+ "sheetId": "",
4
+ "keyFile": "./service-account.json",
5
+ "locales": ["locales/lang"],
6
+ "ignore": [],
7
+ "targetLocales": ["ar", "pt", "es", "zh", "ja", "fr", "de"],
8
+ "sync": {
9
+ "antiLoopWindow": 0,
10
+ "ciAntiLoopWindow": 300,
11
+ "dryRun": true,
12
+ "pruneDeadKeys": false
13
+ },
14
+ "formats": {
15
+ "js": { "strategy": "ast", "preserve": "100%" },
16
+ "json": { "strategy": "stable", "indent": 2, "sortKeys": true }
17
+ }
18
+ }
@@ -0,0 +1,79 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const logger = require('../utils/logger');
4
+
5
+ const CONFIG_PATH = () => path.join(process.cwd(), '.locale-sync.json');
6
+
7
+ async function readConfig() {
8
+ try {
9
+ return JSON.parse(await fs.readFile(CONFIG_PATH(), 'utf8'));
10
+ } catch (e) {
11
+ if (e.code === 'ENOENT') throw new Error('No .locale-sync.json found. Run `locale-sync init` first.');
12
+ throw e;
13
+ }
14
+ }
15
+
16
+ async function writeConfig(config) {
17
+ await fs.writeFile(CONFIG_PATH(), JSON.stringify(config, null, 2));
18
+ }
19
+
20
+ module.exports = async (patterns, options) => {
21
+ // --list: 显示当前忽略规则
22
+ if (options.list) {
23
+ const config = await readConfig();
24
+ const current = config.ignore || [];
25
+ if (current.length === 0) {
26
+ logger.info('No ignore patterns configured.');
27
+ } else {
28
+ logger.info('📋 Current ignore patterns:');
29
+ current.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
30
+ }
31
+ return;
32
+ }
33
+
34
+ // --remove: 移除指定规则
35
+ if (options.remove) {
36
+ const config = await readConfig();
37
+ const before = config.ignore || [];
38
+ config.ignore = before.filter(p => !patterns.includes(p));
39
+ const removed = before.filter(p => patterns.includes(p));
40
+ if (removed.length === 0) {
41
+ logger.warn(`⚠️ None of the specified patterns were found: ${patterns.join(', ')}`);
42
+ return;
43
+ }
44
+ await writeConfig(config);
45
+ logger.success(`✅ Removed: ${removed.join(', ')}`);
46
+ if (config.ignore.length > 0) {
47
+ logger.info(`📋 Remaining: ${config.ignore.join(', ')}`);
48
+ }
49
+ return;
50
+ }
51
+
52
+ // 默认:添加规则
53
+ if (!patterns || patterns.length === 0) {
54
+ logger.warn('Usage: locale-sync ignore <pattern...>');
55
+ logger.info(' Examples:');
56
+ logger.info(' locale-sync ignore node_modules');
57
+ logger.info(' locale-sync ignore *.d.ts index.js');
58
+ logger.info(' locale-sync ignore --list');
59
+ logger.info(' locale-sync ignore --remove node_modules');
60
+ return;
61
+ }
62
+
63
+ const config = await readConfig();
64
+ const current = config.ignore || [];
65
+ const toAdd = patterns.filter(p => !current.includes(p));
66
+ const skipped = patterns.filter(p => current.includes(p));
67
+
68
+ if (toAdd.length === 0) {
69
+ logger.info(`All patterns already exist: ${patterns.join(', ')}`);
70
+ return;
71
+ }
72
+
73
+ config.ignore = [...current, ...toAdd];
74
+ await writeConfig(config);
75
+
76
+ logger.success(`✅ Added: ${toAdd.join(', ')}`);
77
+ if (skipped.length > 0) logger.info(`⏭ Already existed: ${skipped.join(', ')}`);
78
+ logger.info(`📋 All ignore patterns: ${config.ignore.join(', ')}`);
79
+ };
@@ -0,0 +1,59 @@
1
+ const inquirer = require('inquirer');
2
+ const fs = require('fs');
3
+ const fsP = require('fs').promises;
4
+ const path = require('path');
5
+ const logger = require('../utils/logger');
6
+ const { hasCredentials, fetchOrReadCredentials, writeCredentials } = require('../utils/credentials');
7
+
8
+ function isUrl(s) { return /^https?:\/\//i.test(s.trim()); }
9
+
10
+ module.exports = async () => {
11
+ logger.info('🛠 Initialize locale-sync configuration\n');
12
+ const answers = await inquirer.prompt([
13
+ { type: 'input', name: 'sheetId', message: 'Google Sheet ID:', validate: v => v.length > 10 || 'Required' },
14
+ { type: 'input', name: 'locales', message: 'Locales Directory:', default: 'locales/lang' },
15
+ { type: 'input', name: 'project', message: 'Project Name:', default: path.basename(process.cwd()) },
16
+ { type: 'input', name: 'ignore', message: 'Ignore patterns (comma-separated, e.g. node_modules,*.d.ts):', default: '' }
17
+ ]);
18
+
19
+ const config = {
20
+ sheetId: answers.sheetId,
21
+ locales: [answers.locales],
22
+ ignore: answers.ignore ? answers.ignore.split(',').map(s => s.trim()).filter(Boolean) : [],
23
+ project: answers.project,
24
+ sync: { dryRun: true, pruneDeadKeys: false, antiLoopWindow: 0 }
25
+ };
26
+
27
+ await fsP.writeFile(path.join(process.cwd(), '.locale-sync.json'), JSON.stringify(config, null, 2));
28
+ logger.success('✅ Configuration saved to .locale-sync.json');
29
+
30
+ if (hasCredentials()) {
31
+ logger.info('✅ LOCALE_SYNC_CREDENTIALS already set, skipping credential setup.');
32
+ } else {
33
+ logger.warn('⚠️ LOCALE_SYNC_CREDENTIALS not found. Please provide your service-account.json.\n');
34
+ const { source } = await inquirer.prompt([{
35
+ type: 'input',
36
+ name: 'source',
37
+ message: 'Path or URL to service-account.json:',
38
+ default: './service-account.json',
39
+ validate: v => {
40
+ if (isUrl(v)) return true;
41
+ if (!fs.existsSync(v.trim())) return `File not found: ${v.trim()}`;
42
+ try { JSON.parse(fs.readFileSync(v.trim(), 'utf8')); return true; }
43
+ catch { return 'File is not valid JSON'; }
44
+ }
45
+ }]);
46
+ if (isUrl(source)) {
47
+ logger.warn('⚠️ Fetching credentials from a URL. Make sure this URL is private and not publicly accessible.');
48
+ }
49
+ const raw = await fetchOrReadCredentials(source);
50
+ const profile = writeCredentials(raw);
51
+ logger.success(`✅ Credentials saved to ${profile}`);
52
+ if (!isUrl(source)) {
53
+ logger.info(`💡 The original file can now be deleted: rm ${source.trim()}`);
54
+ }
55
+ logger.info(`💡 For existing terminals run: source ${profile}`);
56
+ }
57
+
58
+ logger.info('\n💡 Run `locale-sync push` or `locale-sync pull` to start syncing.');
59
+ };
@@ -0,0 +1,60 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const inquirer = require('inquirer');
4
+ const { loadConfig } = require('../../config/manager');
5
+ const { getClient, readSheet, ensureMetadataColumns } = require('../../io/googleSheets');
6
+ const { scanLocales } = require('../../io/localFs/scanner');
7
+ const { generatePullPlan } = require('../../core/engines/pullEngine');
8
+ const { applyAstUpdates } = require('../../io/localFs/astUpdater');
9
+ const logger = require('../utils/logger');
10
+
11
+ module.exports = async (options) => {
12
+ const config = await loadConfig(options);
13
+ const spinner = ora('📥 Reading Sheets & Scanning Locales...').start();
14
+
15
+ try {
16
+ const sheets = await getClient(config.keyFile);
17
+ await ensureMetadataColumns(sheets, config.sheetId, config.project);
18
+ await ensureMetadataColumns(sheets, config.sheetId, 'common');
19
+
20
+ const [commonData, projectData] = await Promise.all([
21
+ readSheet(sheets, config.sheetId, 'common'),
22
+ readSheet(sheets, config.sheetId, config.project)
23
+ ]);
24
+
25
+ const local = await scanLocales(config.locales, config.ignore);
26
+ spinner.succeed('✅ Data loaded');
27
+
28
+ const plan = await generatePullPlan(projectData, commonData, local, config);
29
+
30
+ console.log(chalk.bold('\n📋 Pull Preview'));
31
+ console.log(` Updates: ${plan.length}`);
32
+ if (plan.length > 0) {
33
+ plan.forEach(p => console.log(` - ${p.locale}/${p.key}: "${p.oldValue}" → "${p.newValue}"`));
34
+ }
35
+
36
+ if (plan.length === 0 || config.sync.dryRun) {
37
+ spinner.info('✅ No changes or Dry-Run mode');
38
+ return;
39
+ }
40
+
41
+ if (!options.yes) {
42
+ const { proceed } = await inquirer.prompt([{ type: 'confirm', name: 'proceed', message: 'Apply to local files?', default: false }]);
43
+ if (!proceed) return logger.info('👋 Sync cancelled.');
44
+ }
45
+
46
+ spinner.start(' Updating local files...');
47
+ const res = await applyAstUpdates(plan, false);
48
+ spinner.succeed(`✅ Updated ${res.updated} files`);
49
+
50
+ if (res.warnings.length) {
51
+ logger.warn('⚠️ Warnings:');
52
+ res.warnings.forEach(w => logger.warn(` ${w}`));
53
+ }
54
+ } catch (err) {
55
+ spinner.fail(' Pull failed');
56
+ logger.error(err.message);
57
+ if (options.verbose) console.error(err);
58
+ process.exit(1);
59
+ }
60
+ };
@@ -0,0 +1,136 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const inquirer = require('inquirer');
4
+ const { loadConfig } = require('../../config/manager');
5
+ const { getClient, readSheet, ensureMetadataColumns, batchWrite, buildSheetFormatRequests } = require('../../io/googleSheets');
6
+ const { scanLocales } = require('../../io/localFs/scanner');
7
+ const { generatePushPlan } = require('../../core/engines/pushEngine');
8
+ const { formatLocaleHeader } = require('../../core/utils/stringNormalizer');
9
+ const logger = require('../utils/logger');
10
+
11
+ module.exports = async (options) => {
12
+ const config = await loadConfig(options);
13
+ const spinner = ora('📤 Reading Sheets & Scanning Locales...').start();
14
+
15
+ try {
16
+ const sheets = await getClient(config.keyFile);
17
+ // 先扫描语言包,再动态生成表头(en-US / ar-SA 格式)
18
+ const local = await scanLocales(config.locales, config.ignore);
19
+ const _getLocaleBase = loc => loc.length <= 3 ? loc : loc.slice(0, 2);
20
+ const scannedNorms = Object.keys(local)
21
+ .filter(k => Object.keys(local[k].values).length > 0)
22
+ .map(k => k.toLowerCase().replace(/[-_]/g, ''));
23
+ const enNorm = scannedNorms.find(k => _getLocaleBase(k) === 'en') || 'en';
24
+ const enColHeader = formatLocaleHeader(enNorm);
25
+ const nonEnHeaders = scannedNorms
26
+ .filter(k => _getLocaleBase(k) !== 'en')
27
+ .map(formatLocaleHeader);
28
+ // 自动初始化 Sheet 结构(新 Sheet 时写入动态表头)
29
+ const projectBaseHeaders = ['key', 'filePath', enColHeader, ...nonEnHeaders];
30
+ const commonBaseHeaders = [enColHeader, ...nonEnHeaders];
31
+ await ensureMetadataColumns(sheets, config.sheetId, config.project, projectBaseHeaders);
32
+ await ensureMetadataColumns(sheets, config.sheetId, 'common', commonBaseHeaders);
33
+
34
+ const [commonData, projectData] = await Promise.all([
35
+ readSheet(sheets, config.sheetId, 'common'),
36
+ readSheet(sheets, config.sheetId, config.project)
37
+ ]);
38
+ spinner.succeed('✅ Data loaded');
39
+
40
+ // locale 基础语言码映射:'enus'→'en', 'arae'→'ar', 'zhcn'→'zh'(≤3位视为纯代码)
41
+ const getLocaleBase = loc => loc.length <= 3 ? loc : loc.slice(0, 2);
42
+ // 优先找纯语言文件,否则找任意同基础语言的 locale
43
+ const findLocaleData = base =>
44
+ local[base] || Object.entries(local).find(([k]) => getLocaleBase(k) === base)?.[1];
45
+
46
+ const enData = findLocaleData('en');
47
+ const allKeys = new Set();
48
+ for (const data of Object.values(local)) {
49
+ for (const key of Object.keys(data.values)) allKeys.add(key);
50
+ }
51
+
52
+ // ── 扫描摘要:帮助确认本地内容是否完整 ──
53
+ const localeEntries = Object.entries(local)
54
+ .filter(([, d]) => Object.keys(d.values).length > 0)
55
+ .sort(([, a], [, b]) => Object.keys(b.values).length - Object.keys(a.values).length);
56
+ const maxLocale = Math.max(...localeEntries.map(([k]) => k.length), 6);
57
+ console.log(chalk.bold('\n🔍 Scan Summary'));
58
+ console.log(` Total unique keys : ${chalk.cyan(allKeys.size)}`);
59
+ console.log(` Locales scanned : ${localeEntries.length}`);
60
+ for (const [loc, data] of localeEntries) {
61
+ const count = Object.keys(data.values).length;
62
+ const emptyCount = Object.values(data.values).filter(v => v == null || String(v).trim() === '').length;
63
+ const pad = ' '.repeat(maxLocale - loc.length);
64
+ const emptyNote = emptyCount > 0 ? chalk.gray(` (${emptyCount} empty)`) : '';
65
+ console.log(` ${chalk.yellow(loc)}${pad} ${count} keys${emptyNote}`);
66
+ }
67
+
68
+ const localItems = [...allKeys].map(key => {
69
+ const translations = {};
70
+ for (const [locale, data] of Object.entries(local)) {
71
+ if (data.values[key] === undefined) continue;
72
+ translations[locale] = data.values[key];
73
+ // 同时填充基础语言码(arae→ar, enus→en),纯语言文件优先不覆盖
74
+ const base = getLocaleBase(locale);
75
+ if (base !== locale && !(base in translations)) {
76
+ translations[base] = data.values[key];
77
+ }
78
+ }
79
+ let filePath = '';
80
+ for (const data of Object.values(local)) {
81
+ const fp = data.filePaths?.[key] || (data.values[key] !== undefined ? data.filePath : null);
82
+ if (fp) { filePath = fp; break; }
83
+ }
84
+ return { key, filePath, en: translations['en'] || enData?.values[key] || '', translations };
85
+ });
86
+
87
+ const requests = await generatePushPlan(localItems, {
88
+ common: commonData,
89
+ project: projectData,
90
+ projectHeaders: projectData.headers || [],
91
+ projectSheetId: projectData.sheetId ?? 0,
92
+ commonHeaders: commonData.headers || [],
93
+ commonSheetId: commonData.sheetId ?? 0
94
+ }, config);
95
+
96
+ let newRows = 0;
97
+ const updatedRowKeys = new Set();
98
+ for (const req of requests) {
99
+ if (req.appendCells) {
100
+ newRows += req.appendCells.rows?.length ?? 0;
101
+ } else if (req.updateCells) {
102
+ // 仅统计翻译/内容写入(有 backgroundColor 格式),排除纯元数据行
103
+ if (req.updateCells.fields && req.updateCells.fields.includes('backgroundColor')) {
104
+ const r = req.updateCells.range;
105
+ updatedRowKeys.add(`${r.sheetId}:${r.startRowIndex}`);
106
+ }
107
+ }
108
+ }
109
+ console.log(chalk.bold('\n📋 Push Preview'));
110
+ console.log(` New rows : ${newRows}`);
111
+ console.log(` Updated rows: ${updatedRowKeys.size}`);
112
+
113
+ if (config.sync.dryRun || requests.length === 0) {
114
+ spinner.info('✅ No changes or Dry-Run mode');
115
+ return;
116
+ }
117
+
118
+ if (!options.yes) {
119
+ const { proceed } = await inquirer.prompt([{ type: 'confirm', name: 'proceed', message: 'Apply to Google Sheets?', default: false }]);
120
+ if (!proceed) return logger.info('👋 Sync cancelled.');
121
+ }
122
+
123
+ spinner.start('🔄 Syncing to Google Sheets...');
124
+ const formatReqs = [
125
+ ...buildSheetFormatRequests(commonData.sheetId),
126
+ ...buildSheetFormatRequests(projectData.sheetId)
127
+ ];
128
+ await batchWrite(sheets, config.sheetId, [...formatReqs, ...requests]);
129
+ spinner.succeed('✅ Push completed successfully');
130
+ } catch (err) {
131
+ spinner.fail('❌ Push failed');
132
+ logger.error(err.message);
133
+ if (options.verbose) console.error(err);
134
+ process.exit(1);
135
+ }
136
+ };
@@ -0,0 +1,35 @@
1
+ const inquirer = require('inquirer');
2
+ const fs = require('fs');
3
+ const logger = require('../utils/logger');
4
+ const { fetchOrReadCredentials, writeCredentials } = require('../utils/credentials');
5
+
6
+ function isUrl(s) { return /^https?:\/\//i.test(s.trim()); }
7
+
8
+ module.exports = async () => {
9
+ logger.info('🔑 Update Google Service Account credentials\n');
10
+
11
+ const { source } = await inquirer.prompt([{
12
+ type: 'input',
13
+ name: 'source',
14
+ message: 'Path or URL to service-account.json:',
15
+ default: './service-account.json',
16
+ validate: v => {
17
+ if (isUrl(v)) return true; // URL 延迟验证
18
+ if (!fs.existsSync(v.trim())) return `File not found: ${v.trim()}`;
19
+ try { JSON.parse(fs.readFileSync(v.trim(), 'utf8')); return true; }
20
+ catch { return 'File is not valid JSON'; }
21
+ }
22
+ }]);
23
+
24
+ if (isUrl(source)) {
25
+ logger.warn('⚠️ Fetching credentials from a URL. Make sure this URL is private and not publicly accessible.');
26
+ }
27
+
28
+ const raw = await fetchOrReadCredentials(source);
29
+ const profile = writeCredentials(raw);
30
+ logger.success(`✅ Credentials saved to ${profile}`);
31
+ if (!isUrl(source)) {
32
+ logger.info(`💡 The original file can now be deleted: rm ${source.trim()}`);
33
+ }
34
+ logger.info(`💡 For existing terminals run: source ${profile}`);
35
+ };
@@ -0,0 +1,66 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const ENV_KEY = 'LOCALE_SYNC_CREDENTIALS';
6
+
7
+ function detectShellProfile() {
8
+ const shell = process.env.SHELL || '';
9
+ if (shell.includes('zsh')) return path.join(os.homedir(), '.zshrc');
10
+ if (shell.includes('fish')) return path.join(os.homedir(), '.config', 'fish', 'config.fish');
11
+ return path.join(os.homedir(), '.bash_profile');
12
+ }
13
+
14
+ /**
15
+ * 从本地文件路径或 http(s) URL 读取 service-account JSON 内容。
16
+ * @param {string} source 文件路径或 URL
17
+ * @returns {Promise<string>} JSON 字符串
18
+ */
19
+ async function fetchOrReadCredentials(source) {
20
+ const trimmed = source.trim();
21
+ if (/^https?:\/\//i.test(trimmed)) {
22
+ const res = await fetch(trimmed);
23
+ if (!res.ok) throw new Error(`Failed to fetch credentials: ${res.status} ${res.statusText}`);
24
+ return res.text();
25
+ }
26
+ // 本地文件
27
+ return fs.readFileSync(trimmed, 'utf8');
28
+ }
29
+
30
+ /**
31
+ * 将 service-account JSON 字符串 base64 编码后写入 shell profile,
32
+ * 同时设置当前进程的环境变量(本次运行立即生效)。
33
+ * @param {string} jsonString service-account JSON 原文
34
+ * @returns {string} 写入的 profile 文件路径
35
+ */
36
+ function writeCredentials(jsonString) {
37
+ JSON.parse(jsonString.trim()); // 验证 JSON 合法
38
+
39
+ const token = Buffer.from(jsonString.trim()).toString('base64');
40
+ const profile = detectShellProfile();
41
+
42
+ let content = '';
43
+ try { content = fs.readFileSync(profile, 'utf8'); } catch { /* 文件不存在则新建 */ }
44
+
45
+ const lines = content.split('\n');
46
+ const idx = lines.findIndex(l => l.includes(ENV_KEY));
47
+ const line = `export ${ENV_KEY}="${token}"`;
48
+
49
+ if (idx >= 0) {
50
+ lines[idx] = line;
51
+ } else {
52
+ lines.push('', '# locale-sync Google credentials (base64 service-account JSON)', line);
53
+ }
54
+
55
+ fs.writeFileSync(profile, lines.join('\n'));
56
+ process.env[ENV_KEY] = token;
57
+
58
+ return profile;
59
+ }
60
+
61
+ /** 检查当前环境变量是否已配置 */
62
+ function hasCredentials() {
63
+ return !!process.env[ENV_KEY];
64
+ }
65
+
66
+ module.exports = { ENV_KEY, detectShellProfile, fetchOrReadCredentials, writeCredentials, hasCredentials };
@@ -0,0 +1,9 @@
1
+ const chalk = require('chalk');
2
+
3
+ module.exports = {
4
+ info: (msg) => console.log(chalk.blue('ℹ️'), msg),
5
+ success: (msg) => console.log(chalk.green('✅'), msg),
6
+ warn: (msg) => console.warn(chalk.yellow('⚠️'), msg),
7
+ error: (msg) => console.error(chalk.red('❌'), msg),
8
+ debug: (msg) => process.env.DEBUG ? console.log(chalk.gray('🐛'), msg) : null
9
+ };
@@ -0,0 +1,44 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+
4
+ const LOCAL_CONFIG = path.join(process.cwd(), '.locale-sync.json');
5
+ const DEFAULT_CONFIG = require('../../config/default.json');
6
+
7
+ function deepMerge(target, source) {
8
+ const output = Object.assign({}, target);
9
+ if (typeof source === 'object' && source !== null) {
10
+ Object.keys(source).forEach(key => {
11
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
12
+ output[key] = deepMerge(output[key] != null ? output[key] : {}, source[key]);
13
+ } else {
14
+ output[key] = source[key];
15
+ }
16
+ });
17
+ }
18
+ return output;
19
+ }
20
+
21
+ async function loadConfig(overrides = {}) {
22
+ let config = DEFAULT_CONFIG;
23
+ try {
24
+ const local = JSON.parse(await fs.readFile(LOCAL_CONFIG, 'utf8'));
25
+ config = deepMerge(config, local);
26
+ } catch (err) {
27
+ if (err.code !== 'ENOENT') console.warn(`[locale-sync] Warning: could not load config: ${err.message}`);
28
+ }
29
+
30
+ if (config.project === '${package.json.name}') {
31
+ try {
32
+ config.project = require(path.join(process.cwd(), 'package.json')).name;
33
+ } catch {
34
+ config.project = 'unknown-project';
35
+ }
36
+ }
37
+
38
+ const isCI = process.env.CI === 'true' || process.argv.includes('--ci');
39
+ config.sync.antiLoopWindow = isCI ? config.sync.ciAntiLoopWindow : config.sync.antiLoopWindow;
40
+
41
+ return deepMerge(config, overrides);
42
+ }
43
+
44
+ module.exports = { loadConfig };