@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/bin/cli.js +48 -0
  5. package/bin/commands/apply.js +48 -0
  6. package/bin/commands/check-sync.js +35 -0
  7. package/bin/commands/extract-utils.js +216 -0
  8. package/bin/commands/extract.js +198 -0
  9. package/bin/commands/find-orphans.js +36 -0
  10. package/bin/commands/help.js +34 -0
  11. package/bin/commands/index.js +79 -0
  12. package/bin/commands/translate.js +51 -0
  13. package/bin/commands/version.js +17 -0
  14. package/bin/commands/watch.js +34 -0
  15. package/bin/core/applier-utils.js +144 -0
  16. package/bin/core/applier.js +165 -0
  17. package/bin/core/args.js +147 -0
  18. package/bin/core/backup.js +74 -0
  19. package/bin/core/command-interface.js +69 -0
  20. package/bin/core/config.js +108 -0
  21. package/bin/core/context.js +86 -0
  22. package/bin/core/detector.js +152 -0
  23. package/bin/core/file-walker.js +159 -0
  24. package/bin/core/fs-adapter.js +56 -0
  25. package/bin/core/help-generator.js +208 -0
  26. package/bin/core/index.js +63 -0
  27. package/bin/core/json-utils.js +213 -0
  28. package/bin/core/key-generator.js +75 -0
  29. package/bin/core/log-utils.js +26 -0
  30. package/bin/core/orphan-finder.js +208 -0
  31. package/bin/core/parser-utils.js +187 -0
  32. package/bin/core/paths.js +60 -0
  33. package/bin/core/plugin-interface.js +83 -0
  34. package/bin/core/plugin-resolver-utils.js +166 -0
  35. package/bin/core/plugin-resolver.js +211 -0
  36. package/bin/core/sync-checker-utils.js +99 -0
  37. package/bin/core/sync-checker.js +199 -0
  38. package/bin/core/translator.js +197 -0
  39. package/bin/core/types.js +297 -0
  40. package/bin/core/watcher.js +119 -0
  41. package/bin/plugins/adapter-transloco.js +156 -0
  42. package/bin/plugins/parser-angular.js +56 -0
  43. package/bin/plugins/parser-primeng.js +79 -0
  44. package/bin/plugins/parser-typescript.js +66 -0
  45. package/bin/plugins/provider-deepl.js +65 -0
  46. package/bin/plugins/provider-mymemory.js +192 -0
  47. package/package.json +123 -0
  48. 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 };