@abdess76/i18nkit 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.
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/bin/cli.js +48 -0
- package/bin/commands/apply.js +48 -0
- package/bin/commands/check-sync.js +35 -0
- package/bin/commands/extract-utils.js +216 -0
- package/bin/commands/extract.js +198 -0
- package/bin/commands/find-orphans.js +36 -0
- package/bin/commands/help.js +34 -0
- package/bin/commands/index.js +79 -0
- package/bin/commands/translate.js +51 -0
- package/bin/commands/version.js +17 -0
- package/bin/commands/watch.js +34 -0
- package/bin/core/applier-utils.js +144 -0
- package/bin/core/applier.js +165 -0
- package/bin/core/args.js +147 -0
- package/bin/core/backup.js +74 -0
- package/bin/core/command-interface.js +69 -0
- package/bin/core/config.js +108 -0
- package/bin/core/context.js +86 -0
- package/bin/core/detector.js +152 -0
- package/bin/core/file-walker.js +159 -0
- package/bin/core/fs-adapter.js +56 -0
- package/bin/core/help-generator.js +208 -0
- package/bin/core/index.js +63 -0
- package/bin/core/json-utils.js +213 -0
- package/bin/core/key-generator.js +75 -0
- package/bin/core/log-utils.js +26 -0
- package/bin/core/orphan-finder.js +208 -0
- package/bin/core/parser-utils.js +187 -0
- package/bin/core/paths.js +60 -0
- package/bin/core/plugin-interface.js +83 -0
- package/bin/core/plugin-resolver-utils.js +166 -0
- package/bin/core/plugin-resolver.js +211 -0
- package/bin/core/sync-checker-utils.js +99 -0
- package/bin/core/sync-checker.js +199 -0
- package/bin/core/translator.js +197 -0
- package/bin/core/types.js +297 -0
- package/bin/core/watcher.js +119 -0
- package/bin/plugins/adapter-transloco.js +156 -0
- package/bin/plugins/parser-angular.js +56 -0
- package/bin/plugins/parser-primeng.js +79 -0
- package/bin/plugins/parser-typescript.js +66 -0
- package/bin/plugins/provider-deepl.js +65 -0
- package/bin/plugins/provider-mymemory.js +192 -0
- package/package.json +123 -0
- package/types/index.d.ts +85 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadConfig } = require('./config');
|
|
4
|
+
const { parseArgs, detectCommand } = require('./args');
|
|
5
|
+
const { resolvePaths } = require('./paths');
|
|
6
|
+
const { loadPlugins } = require('./plugin-resolver');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} Context
|
|
10
|
+
* @property {string[]} args
|
|
11
|
+
* @property {Object} config
|
|
12
|
+
* @property {string} cwd
|
|
13
|
+
* @property {Object} exitCodes
|
|
14
|
+
* @property {boolean} verbose
|
|
15
|
+
* @property {boolean} jsonOutput
|
|
16
|
+
* @property {boolean} strict
|
|
17
|
+
* @property {boolean} dryRun
|
|
18
|
+
* @property {boolean} ciMode
|
|
19
|
+
* @property {boolean} useDeepL
|
|
20
|
+
* @property {string} srcDir
|
|
21
|
+
* @property {string} i18nDir
|
|
22
|
+
* @property {string} reportDir
|
|
23
|
+
* @property {string} backupDir
|
|
24
|
+
* @property {string|null} lang
|
|
25
|
+
* @property {string} format
|
|
26
|
+
* @property {string} outputFile
|
|
27
|
+
* @property {string} keyMappingFile
|
|
28
|
+
* @property {string[]} excludedFolders
|
|
29
|
+
* @property {boolean} merge
|
|
30
|
+
* @property {boolean} skipTranslated
|
|
31
|
+
* @property {boolean} extractTsObjects
|
|
32
|
+
* @property {boolean} autoApply
|
|
33
|
+
* @property {string[]} initLangs
|
|
34
|
+
* @property {boolean} backup
|
|
35
|
+
* @property {boolean} interactive
|
|
36
|
+
* @property {string|null} translatePair
|
|
37
|
+
* @property {string|null} translateEmail
|
|
38
|
+
* @property {Array} parsers
|
|
39
|
+
* @property {Object} adapter
|
|
40
|
+
* @property {Object} provider
|
|
41
|
+
* @property {function} log
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const EXIT_CODES = Object.freeze({
|
|
45
|
+
SUCCESS: 0,
|
|
46
|
+
UNTRANSLATED: 1,
|
|
47
|
+
ERROR: 2,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create execution context from command line args
|
|
52
|
+
* @param {string[]} [args]
|
|
53
|
+
* @param {string} [cwd]
|
|
54
|
+
* @returns {Context}
|
|
55
|
+
*/
|
|
56
|
+
function buildPluginConfig(config, flags) {
|
|
57
|
+
const provider =
|
|
58
|
+
flags.useDeepL ? '@i18nkit/provider-deepl' : config.provider || '@i18nkit/provider-mymemory';
|
|
59
|
+
return { ...config, provider };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createContext(args = process.argv.slice(2), cwd = process.cwd()) {
|
|
63
|
+
const config = loadConfig(cwd);
|
|
64
|
+
const flags = parseArgs(args, config);
|
|
65
|
+
const paths = resolvePaths({ args, config, cwd, lang: flags.lang });
|
|
66
|
+
const { parsers, adapter, provider } = loadPlugins(buildPluginConfig(config, flags), cwd);
|
|
67
|
+
const log = (...msgs) => !flags.jsonOutput && console.log(...msgs);
|
|
68
|
+
return {
|
|
69
|
+
args,
|
|
70
|
+
config,
|
|
71
|
+
cwd,
|
|
72
|
+
exitCodes: EXIT_CODES,
|
|
73
|
+
...flags,
|
|
74
|
+
...paths,
|
|
75
|
+
parsers,
|
|
76
|
+
adapter,
|
|
77
|
+
provider,
|
|
78
|
+
log,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
createContext,
|
|
84
|
+
detectCommand,
|
|
85
|
+
EXIT_CODES,
|
|
86
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Auto-detection of frontend frameworks, UI libraries, and i18n solutions.
|
|
5
|
+
* Analyzes package.json dependencies and root files to identify project stack.
|
|
6
|
+
* @module detector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('./fs-adapter');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const stripVersion = v => v?.replace(/[\^~]/, '');
|
|
13
|
+
const getDep = (ctx, pkg) => ctx.pkg.dependencies?.[pkg];
|
|
14
|
+
const getVersion = pkg => ctx => stripVersion(getDep(ctx, pkg));
|
|
15
|
+
|
|
16
|
+
const DETECTION_RULES = [
|
|
17
|
+
{
|
|
18
|
+
id: 'angular',
|
|
19
|
+
type: 'framework',
|
|
20
|
+
detect: ctx => getDep(ctx, '@angular/core') || ctx.files.includes('angular.json'),
|
|
21
|
+
plugins: ['parser-angular'],
|
|
22
|
+
label: 'Angular',
|
|
23
|
+
version: getVersion('@angular/core'),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'react',
|
|
27
|
+
type: 'framework',
|
|
28
|
+
detect: ctx => getDep(ctx, 'react'),
|
|
29
|
+
plugins: ['parser-react'],
|
|
30
|
+
label: 'React',
|
|
31
|
+
version: getVersion('react'),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'vue',
|
|
35
|
+
type: 'framework',
|
|
36
|
+
detect: ctx => getDep(ctx, 'vue'),
|
|
37
|
+
plugins: ['parser-vue'],
|
|
38
|
+
label: 'Vue',
|
|
39
|
+
version: getVersion('vue'),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'primeng',
|
|
43
|
+
type: 'library',
|
|
44
|
+
detect: ctx => getDep(ctx, 'primeng'),
|
|
45
|
+
plugins: ['parser-primeng'],
|
|
46
|
+
label: 'PrimeNG',
|
|
47
|
+
version: getVersion('primeng'),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'material',
|
|
51
|
+
type: 'library',
|
|
52
|
+
detect: ctx => getDep(ctx, '@angular/material'),
|
|
53
|
+
plugins: ['parser-material'],
|
|
54
|
+
label: 'Angular Material',
|
|
55
|
+
version: getVersion('@angular/material'),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'transloco',
|
|
59
|
+
type: 'i18n',
|
|
60
|
+
detect: ctx => getDep(ctx, '@jsverse/transloco') || getDep(ctx, '@ngneat/transloco'),
|
|
61
|
+
plugins: ['adapter-transloco'],
|
|
62
|
+
label: 'Transloco',
|
|
63
|
+
version: ctx =>
|
|
64
|
+
stripVersion(getDep(ctx, '@jsverse/transloco') || getDep(ctx, '@ngneat/transloco')),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'ngx-translate',
|
|
68
|
+
type: 'i18n',
|
|
69
|
+
detect: ctx => getDep(ctx, '@ngx-translate/core'),
|
|
70
|
+
plugins: ['adapter-ngx-translate'],
|
|
71
|
+
label: 'ngx-translate',
|
|
72
|
+
version: getVersion('@ngx-translate/core'),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'react-i18next',
|
|
76
|
+
type: 'i18n',
|
|
77
|
+
detect: ctx => getDep(ctx, 'react-i18next'),
|
|
78
|
+
plugins: ['adapter-react-i18next'],
|
|
79
|
+
label: 'react-i18next',
|
|
80
|
+
version: getVersion('react-i18next'),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function readPackageJson(cwd) {
|
|
85
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
86
|
+
if (!fs.existsSync(pkgPath)) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listRootFiles(cwd) {
|
|
97
|
+
try {
|
|
98
|
+
return fs.readdirSync(cwd);
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createEmptyDetected() {
|
|
105
|
+
return { framework: null, libraries: [], i18n: null, plugins: new Set(), details: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildDetail(rule, context) {
|
|
109
|
+
const version = typeof rule.version === 'function' ? rule.version(context) : null;
|
|
110
|
+
return { id: rule.id, label: rule.label, type: rule.type, version };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyRuleToDetected(rule, detail, detected) {
|
|
114
|
+
detected.details.push(detail);
|
|
115
|
+
rule.plugins.forEach(p => detected.plugins.add(p));
|
|
116
|
+
if (rule.type === 'framework') {
|
|
117
|
+
detected.framework = detail;
|
|
118
|
+
}
|
|
119
|
+
if (rule.type === 'library') {
|
|
120
|
+
detected.libraries.push(detail);
|
|
121
|
+
}
|
|
122
|
+
if (rule.type === 'i18n') {
|
|
123
|
+
detected.i18n = detail;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function processDetectionRule(rule, context, detected) {
|
|
128
|
+
if (!rule.detect(context)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
applyRuleToDetected(rule, buildDetail(rule, context), detected);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Detects framework, libraries, and i18n setup from package.json
|
|
136
|
+
* @param {string} [cwd=process.cwd()]
|
|
137
|
+
* @returns {DetectionResult}
|
|
138
|
+
* @example
|
|
139
|
+
* const result = detectProject('/path/to/project');
|
|
140
|
+
* // { framework: { id: 'angular', label: 'Angular', version: '17.0.0' }, ... }
|
|
141
|
+
*/
|
|
142
|
+
function detectProject(cwd = process.cwd()) {
|
|
143
|
+
const context = { pkg: readPackageJson(cwd), files: listRootFiles(cwd), cwd };
|
|
144
|
+
const detected = createEmptyDetected();
|
|
145
|
+
DETECTION_RULES.forEach(rule => processDetectionRule(rule, context, detected));
|
|
146
|
+
detected.plugins = [...detected.plugins];
|
|
147
|
+
return detected;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
detectProject,
|
|
152
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Recursive directory walker for Angular/TypeScript projects.
|
|
5
|
+
* Filters source files, extracts inline templates, and loads external templates.
|
|
6
|
+
* @module file-walker
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('./fs-adapter');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_EXCLUDED_FOLDERS = ['node_modules', 'dist', '.git', 'coverage', 'e2e', '.angular'];
|
|
13
|
+
|
|
14
|
+
const VALID_SOURCE_RE = /\.(ts|html)$/;
|
|
15
|
+
const EXCLUDED_SOURCE_RE = /\.(spec|test|e2e|mock)\./;
|
|
16
|
+
|
|
17
|
+
const isValidSourceFile = file => VALID_SOURCE_RE.test(file) && !EXCLUDED_SOURCE_RE.test(file);
|
|
18
|
+
|
|
19
|
+
function validateDir(dir) {
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldSkipEntry(entry, excludedFolders) {
|
|
26
|
+
return excludedFolders.includes(entry.name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function* processEntry(entry, dir, excludedFolders) {
|
|
30
|
+
const filePath = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
yield* walkDirAsync(filePath, excludedFolders);
|
|
33
|
+
} else if (isValidSourceFile(entry.name)) {
|
|
34
|
+
yield filePath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function* walkDirAsync(dir, excludedFolders = DEFAULT_EXCLUDED_FOLDERS) {
|
|
39
|
+
validateDir(dir);
|
|
40
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (shouldSkipEntry(entry, excludedFolders)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
yield* processEntry(entry, dir, excludedFolders);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collects all .ts/.html files recursively, excluding test files
|
|
51
|
+
* @param {string} dir
|
|
52
|
+
* @param {string[]} [excludedFolders]
|
|
53
|
+
* @returns {Promise<string[]>}
|
|
54
|
+
*/
|
|
55
|
+
const collectFiles = (dir, excludedFolders) => Array.fromAsync(walkDirAsync(dir, excludedFolders));
|
|
56
|
+
|
|
57
|
+
function readFileContent(filePath, verbose = false) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
60
|
+
} catch {
|
|
61
|
+
if (verbose) {
|
|
62
|
+
console.warn(`Warning: Cannot read ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function processTypeScriptFile(content, filePath, ctx) {
|
|
69
|
+
const hasComponent = /@Component\s*\(/.test(content);
|
|
70
|
+
if (!hasComponent) {
|
|
71
|
+
return { template: null, typescript: content, type: 'component' };
|
|
72
|
+
}
|
|
73
|
+
const { template: inlineTemplate, tsCode } = extractInlineTemplate(content);
|
|
74
|
+
const template = inlineTemplate || loadExternalTemplate(content, filePath, ctx);
|
|
75
|
+
if (!template && !tsCode) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return { template, typescript: tsCode, type: 'component' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extracts template and TypeScript content from a file
|
|
83
|
+
* @param {string} filePath
|
|
84
|
+
* @param {Set<string>} processedTemplates - Tracks processed templates to avoid duplicates
|
|
85
|
+
* @param {boolean} [verbose=false]
|
|
86
|
+
* @returns {FileContent|null}
|
|
87
|
+
*/
|
|
88
|
+
function getFileContent(filePath, processedTemplates, verbose = false) {
|
|
89
|
+
const content = readFileContent(filePath, verbose);
|
|
90
|
+
if (!content) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const ext = path.extname(filePath);
|
|
94
|
+
if (ext === '.html') {
|
|
95
|
+
return handleHtmlFile(content, filePath, processedTemplates);
|
|
96
|
+
}
|
|
97
|
+
if (ext !== '.ts') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return processTypeScriptFile(content, filePath, { processedTemplates, verbose });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleHtmlFile(content, filePath, processedTemplates) {
|
|
104
|
+
const normalizedPath = path.resolve(filePath);
|
|
105
|
+
if (processedTemplates.has(normalizedPath)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
processedTemplates.add(normalizedPath);
|
|
109
|
+
return { template: content, typescript: null, type: 'html' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractInlineTemplate(content) {
|
|
113
|
+
const templateMatches = [...content.matchAll(/template\s*:\s*`([\s\S]*?)`/g)];
|
|
114
|
+
if (templateMatches.length === 0) {
|
|
115
|
+
return { template: null, tsCode: content };
|
|
116
|
+
}
|
|
117
|
+
const template = templateMatches.map(m => m[1]).join('\n');
|
|
118
|
+
let tsCode = content;
|
|
119
|
+
for (const m of templateMatches) {
|
|
120
|
+
tsCode = tsCode.replace(m[0], '');
|
|
121
|
+
}
|
|
122
|
+
return { template, tsCode };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveTemplatePath(content, filePath) {
|
|
126
|
+
const urlMatch = content.match(/templateUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
127
|
+
return urlMatch ? path.resolve(path.dirname(filePath), urlMatch[1]) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isTemplateInvalid(templatePath, processedTemplates) {
|
|
131
|
+
return !templatePath || !fs.existsSync(templatePath) || processedTemplates.has(templatePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function tryReadTemplate(templatePath, verbose) {
|
|
135
|
+
try {
|
|
136
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
137
|
+
} catch {
|
|
138
|
+
if (verbose) {
|
|
139
|
+
console.warn(`Warning: Cannot read template ${templatePath}`);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function loadExternalTemplate(content, filePath, ctx) {
|
|
146
|
+
const { processedTemplates, verbose = false } = ctx;
|
|
147
|
+
const templatePath = resolveTemplatePath(content, filePath);
|
|
148
|
+
if (isTemplateInvalid(templatePath, processedTemplates)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
processedTemplates.add(templatePath);
|
|
152
|
+
return tryReadTemplate(templatePath, verbose);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
DEFAULT_EXCLUDED_FOLDERS,
|
|
157
|
+
collectFiles,
|
|
158
|
+
getFileContent,
|
|
159
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Filesystem abstraction layer for dependency injection.
|
|
5
|
+
* Enables mocking in tests without monkey-patching Node.js fs module.
|
|
6
|
+
* @module fs-adapter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const nodeFs = require('fs');
|
|
10
|
+
const nodeFsp = nodeFs.promises;
|
|
11
|
+
|
|
12
|
+
let fs = nodeFs;
|
|
13
|
+
let fsp = nodeFsp;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Injects custom fs implementation for testing
|
|
17
|
+
* @param {FsAdapter} adapter
|
|
18
|
+
*/
|
|
19
|
+
const setAdapter = adapter => {
|
|
20
|
+
fs = adapter.fs || nodeFs;
|
|
21
|
+
fsp = adapter.fsp || nodeFsp;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const resetAdapter = () => {
|
|
25
|
+
fs = nodeFs;
|
|
26
|
+
fsp = nodeFsp;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const existsSync = path => fs.existsSync(path);
|
|
30
|
+
const readFileSync = (path, encoding = 'utf-8') => fs.readFileSync(path, encoding);
|
|
31
|
+
const writeFileSync = (path, data, encoding = 'utf-8') => fs.writeFileSync(path, data, encoding);
|
|
32
|
+
const readdirSync = (path, options) => fs.readdirSync(path, options);
|
|
33
|
+
const statSync = path => fs.statSync(path);
|
|
34
|
+
const watch = (path, options, listener) => fs.watch(path, options, listener);
|
|
35
|
+
const mkdirSync = (path, options) => fs.mkdirSync(path, options);
|
|
36
|
+
|
|
37
|
+
const readFile = (path, encoding = 'utf-8') => fsp.readFile(path, encoding);
|
|
38
|
+
const writeFile = (path, data, encoding = 'utf-8') => fsp.writeFile(path, data, encoding);
|
|
39
|
+
const readdir = (path, options) => fsp.readdir(path, options);
|
|
40
|
+
const mkdir = (path, options) => fsp.mkdir(path, options);
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
setAdapter,
|
|
44
|
+
resetAdapter,
|
|
45
|
+
existsSync,
|
|
46
|
+
readFileSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
readdirSync,
|
|
49
|
+
mkdirSync,
|
|
50
|
+
statSync,
|
|
51
|
+
watch,
|
|
52
|
+
readFile,
|
|
53
|
+
writeFile,
|
|
54
|
+
readdir,
|
|
55
|
+
mkdir,
|
|
56
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CLI help text generator.
|
|
5
|
+
* Builds formatted help output from commands, plugins, and detection results.
|
|
6
|
+
* @module help-generator
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { formatPluginHelp } = require('./plugin-interface');
|
|
10
|
+
const {
|
|
11
|
+
formatCommandHelp,
|
|
12
|
+
groupCommandsByCategory,
|
|
13
|
+
COMMAND_CATEGORIES,
|
|
14
|
+
} = require('./command-interface');
|
|
15
|
+
|
|
16
|
+
function generateHeader(packageInfo) {
|
|
17
|
+
const {
|
|
18
|
+
name = 'i18nkit',
|
|
19
|
+
version = '1.0.0',
|
|
20
|
+
description = 'Universal i18n CLI',
|
|
21
|
+
} = packageInfo || {};
|
|
22
|
+
return `${name} v${version}\n${description}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateUsage() {
|
|
26
|
+
return `USAGE:
|
|
27
|
+
i18nkit [command] [options]
|
|
28
|
+
i18nkit --extract [options]`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasDetection(detected) {
|
|
32
|
+
return detected.framework || detected.libraries?.length > 0 || detected.i18n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatVersionLabel(item) {
|
|
36
|
+
return `${item.label}${item.version ? ` v${item.version}` : ''}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addDetectedLine(lines, label, value) {
|
|
40
|
+
if (value) {
|
|
41
|
+
lines.push(` ${label}: ${value}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatLibraries(detected) {
|
|
46
|
+
return detected.libraries?.length > 0 ?
|
|
47
|
+
detected.libraries.map(formatVersionLabel).join(', ')
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatAutoActivated(detected) {
|
|
52
|
+
return detected.plugins?.length > 0 ? detected.plugins.join(', ') : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generateDetectedSection(detected) {
|
|
56
|
+
const lines = ['DETECTED PROJECT:'];
|
|
57
|
+
addDetectedLine(lines, 'Framework', detected.framework && formatVersionLabel(detected.framework));
|
|
58
|
+
addDetectedLine(lines, 'Libraries', formatLibraries(detected));
|
|
59
|
+
addDetectedLine(lines, 'i18n', detected.i18n && formatVersionLabel(detected.i18n));
|
|
60
|
+
addDetectedLine(lines, 'Auto-activated', formatAutoActivated(detected));
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function generateCommandsSection(commands) {
|
|
65
|
+
const lines = ['COMMANDS:'];
|
|
66
|
+
const grouped = groupCommandsByCategory(commands);
|
|
67
|
+
|
|
68
|
+
for (const cat of COMMAND_CATEGORIES) {
|
|
69
|
+
const catCommands = grouped[cat.id]?.commands || [];
|
|
70
|
+
if (catCommands.length === 0) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
lines.push(`\n ${cat.label}:`);
|
|
74
|
+
catCommands.forEach(cmd => lines.push(formatCommandHelp(cmd)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generateGlobalOptions() {
|
|
81
|
+
return `GLOBAL OPTIONS:
|
|
82
|
+
--config <path> Path to config file (default: i18nkit.config.js)
|
|
83
|
+
--src <dir> Source directory (default: src)
|
|
84
|
+
--locales <dir> Locales directory (default: src/assets/i18n)
|
|
85
|
+
--default-lang <code> Default language (default: fr)
|
|
86
|
+
--dry-run Preview changes without writing
|
|
87
|
+
--verbose Verbose output
|
|
88
|
+
--help, -h Show this help
|
|
89
|
+
--version, -v Show version`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatPluginsByType(plugins, type, label) {
|
|
93
|
+
const items = plugins[type] || [];
|
|
94
|
+
if (items.length === 0) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return [`\n ${label}:`, ...items.map(formatPluginHelp)];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getPluginCount(plugins, type) {
|
|
101
|
+
return plugins[type]?.length || 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countPlugins(plugins) {
|
|
105
|
+
return (
|
|
106
|
+
getPluginCount(plugins, 'parsers') +
|
|
107
|
+
getPluginCount(plugins, 'adapters') +
|
|
108
|
+
getPluginCount(plugins, 'providers')
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generatePluginsSection(plugins) {
|
|
113
|
+
const lines = ['PLUGINS:'];
|
|
114
|
+
lines.push(
|
|
115
|
+
...formatPluginsByType(plugins, 'parsers', 'PARSERS (extract i18n from source files)'),
|
|
116
|
+
);
|
|
117
|
+
lines.push(...formatPluginsByType(plugins, 'adapters', 'ADAPTERS (i18n library integration)'));
|
|
118
|
+
lines.push(...formatPluginsByType(plugins, 'providers', 'PROVIDERS (translation services)'));
|
|
119
|
+
if (countPlugins(plugins) === 0) {
|
|
120
|
+
lines.push(' No plugins loaded');
|
|
121
|
+
}
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectPluginExamples(plugins) {
|
|
126
|
+
if (!plugins) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const all = [
|
|
130
|
+
...(plugins.parsers || []),
|
|
131
|
+
...(plugins.adapters || []),
|
|
132
|
+
...(plugins.providers || []),
|
|
133
|
+
];
|
|
134
|
+
return all.flatMap(p => p.examples || []);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function generateExamplesSection(plugins) {
|
|
138
|
+
const lines = [
|
|
139
|
+
'EXAMPLES:',
|
|
140
|
+
' # Extract i18n keys from Angular project',
|
|
141
|
+
' i18nkit --extract',
|
|
142
|
+
'',
|
|
143
|
+
' # Check translation sync',
|
|
144
|
+
' i18nkit check-sync --strict',
|
|
145
|
+
'',
|
|
146
|
+
' # Find orphan keys',
|
|
147
|
+
' i18nkit find-orphans',
|
|
148
|
+
'',
|
|
149
|
+
' # Translate to English',
|
|
150
|
+
' i18nkit translate fr:en',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const pluginExamples = collectPluginExamples(plugins);
|
|
154
|
+
if (pluginExamples.length > 0) {
|
|
155
|
+
lines.push('', ' # Plugin examples:');
|
|
156
|
+
pluginExamples.slice(0, 3).forEach(ex => lines.push(` ${ex}`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function generateFooter() {
|
|
163
|
+
return `DOCUMENTATION:
|
|
164
|
+
https://github.com/Abdess/i18nkit
|
|
165
|
+
|
|
166
|
+
PLUGIN LOCATIONS:
|
|
167
|
+
builtin: <package>/bin/plugins/
|
|
168
|
+
local: .i18n/plugins/
|
|
169
|
+
npm: i18nkit-* packages`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addDetectedIfPresent(sections, detected) {
|
|
173
|
+
if (detected && hasDetection(detected)) {
|
|
174
|
+
sections.push(generateDetectedSection(detected));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addCommandsIfPresent(sections, commands) {
|
|
179
|
+
if (commands?.length > 0) {
|
|
180
|
+
sections.push(generateCommandsSection(commands));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildHelpSections(config) {
|
|
185
|
+
const { plugins, commands, detected, packageInfo } = config;
|
|
186
|
+
const sections = [generateHeader(packageInfo), generateUsage()];
|
|
187
|
+
addDetectedIfPresent(sections, detected);
|
|
188
|
+
addCommandsIfPresent(sections, commands);
|
|
189
|
+
sections.push(generateGlobalOptions());
|
|
190
|
+
if (plugins) {
|
|
191
|
+
sections.push(generatePluginsSection(plugins));
|
|
192
|
+
}
|
|
193
|
+
return sections;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generates complete CLI help text with all sections
|
|
198
|
+
* @param {Object} config
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
function generateFullHelp(config) {
|
|
202
|
+
const sections = buildHelpSections(config);
|
|
203
|
+
sections.push(generateExamplesSection(config.plugins));
|
|
204
|
+
sections.push(generateFooter());
|
|
205
|
+
return sections.join('\n\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { generateFullHelp };
|