@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,297 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Shared type definitions for the i18nkit library.
5
+ * All types are JSDoc-only for editor intellisense and documentation generation.
6
+ * @module types
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} DetectedTech
11
+ * @property {string} id
12
+ * @property {string} label
13
+ * @property {string} type - 'framework', 'library', or 'i18n'
14
+ * @property {string} version
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} DetectionResult
19
+ * @property {DetectedTech} framework
20
+ * @property {Array<DetectedTech>} libraries
21
+ * @property {DetectedTech} i18n
22
+ * @property {Array<string>} plugins
23
+ * @property {Array<DetectedTech>} details
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} LangFile
28
+ * @property {string} name
29
+ * @property {string} path
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} IcuMismatch
34
+ * @property {string} key
35
+ * @property {Array<string>} hasIcu
36
+ * @property {Array<string>} missingIcu
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} IdenticalValue
41
+ * @property {string} key
42
+ * @property {string} value
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} IcuMessage
47
+ * @property {string} key
48
+ * @property {Array<string>} langs
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} SyncResult
53
+ * @property {Set<string>} allKeys
54
+ * @property {Array<LangFile>} langFiles
55
+ * @property {Object<string, Array<string>>} missingByLang
56
+ * @property {Array<IdenticalValue>} identicalValues
57
+ * @property {Array<IcuMessage>} icuMessages
58
+ * @property {Array<IcuMismatch>} icuMismatches
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} SyncCheckResult
63
+ * @property {boolean} success
64
+ * @property {number} exitCode
65
+ * @property {SyncResult} result
66
+ */
67
+
68
+ /**
69
+ * @typedef {Object} DynamicPattern
70
+ * @property {string} file
71
+ * @property {string} pattern
72
+ */
73
+
74
+ /**
75
+ * @typedef {Object} OrphanResult
76
+ * @property {boolean} success
77
+ * @property {number} exitCode
78
+ * @property {Array<string>} orphanKeys
79
+ * @property {Array<string>} usedKeys
80
+ * @property {Array<string>} allKeys
81
+ * @property {Array<DynamicPattern>} dynamicPatterns
82
+ */
83
+
84
+ /**
85
+ * @typedef {Object} TranslateResult
86
+ * @property {boolean} success
87
+ * @property {number} translated
88
+ * @property {number} failed
89
+ */
90
+
91
+ /**
92
+ * @typedef {Object} ModifiedFile
93
+ * @property {string} file
94
+ * @property {number} count
95
+ */
96
+
97
+ /**
98
+ * @typedef {Object} ApplyResult
99
+ * @property {boolean} success
100
+ * @property {boolean} aborted
101
+ * @property {number} totalFiles
102
+ * @property {number} totalReplacements
103
+ * @property {Array<ModifiedFile>} modifiedFiles
104
+ */
105
+
106
+ /**
107
+ * @typedef {Object} Finding
108
+ * @property {string} file
109
+ * @property {number} line
110
+ * @property {string} original
111
+ * @property {string} key
112
+ * @property {string} scope
113
+ */
114
+
115
+ /**
116
+ * @typedef {Object} ExitCodes
117
+ * @property {number} success
118
+ * @property {number} untranslated
119
+ */
120
+
121
+ /**
122
+ * @typedef {Object} TranslateOptions
123
+ * @property {string} i18nDir
124
+ * @property {Object} provider
125
+ * @property {boolean} useDeepL
126
+ * @property {string} email
127
+ * @property {boolean} verbose
128
+ * @property {boolean} dryRun
129
+ * @property {Function} log
130
+ */
131
+
132
+ /**
133
+ * @typedef {Object} SyncOptions
134
+ * @property {string} i18nDir
135
+ * @property {string} format - 'nested' or 'flat'
136
+ * @property {Function} log
137
+ * @property {boolean} strict
138
+ * @property {ExitCodes} exitCodes
139
+ */
140
+
141
+ /**
142
+ * @typedef {Object} OrphanOptions
143
+ * @property {string} i18nDir
144
+ * @property {string} srcDir
145
+ * @property {string} format - 'nested' or 'flat'
146
+ * @property {Array<string>} excludedFolders
147
+ * @property {boolean} verbose
148
+ * @property {boolean} strict
149
+ * @property {Function} log
150
+ * @property {ExitCodes} exitCodes
151
+ */
152
+
153
+ /**
154
+ * @typedef {Object} ApplyOptions
155
+ * @property {string} srcDir
156
+ * @property {string} backupDir
157
+ * @property {Object} adapter
158
+ * @property {boolean} backup
159
+ * @property {boolean} dryRun
160
+ * @property {boolean} verbose
161
+ * @property {boolean} interactive
162
+ * @property {Function} log
163
+ */
164
+
165
+ /**
166
+ * @typedef {Object} WatchOptions
167
+ * @property {string} srcDir
168
+ * @property {Array<string>} excludedFolders
169
+ * @property {Function} onFileChange
170
+ * @property {Function} onStart
171
+ * @property {Function} log
172
+ */
173
+
174
+ /**
175
+ * @typedef {Object} FileContent
176
+ * @property {string} template
177
+ * @property {string} typescript
178
+ * @property {string} type - 'component' or 'html'
179
+ */
180
+
181
+ /**
182
+ * @typedef {Object} ExtractedKey
183
+ * @property {string} key
184
+ * @property {string} original
185
+ * @property {string} scope
186
+ * @property {string} file
187
+ * @property {number} line
188
+ */
189
+
190
+ /**
191
+ * @typedef {Object} ExtractResult
192
+ * @property {Array<ExtractedKey>} findings
193
+ * @property {Object<string, string>} keys
194
+ * @property {number} filesScanned
195
+ * @property {number} keysExtracted
196
+ */
197
+
198
+ /**
199
+ * @typedef {Object} FsAdapter
200
+ * @property {Object} fs
201
+ * @property {Object} fsp
202
+ */
203
+
204
+ /**
205
+ * @typedef {Object} PluginMeta
206
+ * @property {string} description
207
+ * @property {string} category
208
+ */
209
+
210
+ /**
211
+ * @typedef {Object} PluginOption
212
+ * @property {string} flag
213
+ * @property {string} description
214
+ * @property {boolean} required
215
+ */
216
+
217
+ /**
218
+ * @typedef {Object} PluginEnv
219
+ * @property {string} name
220
+ * @property {string} description
221
+ */
222
+
223
+ /**
224
+ * @typedef {Object} Plugin
225
+ * @property {string} name
226
+ * @property {string} type - 'parser', 'adapter', or 'provider'
227
+ * @property {PluginMeta} meta
228
+ * @property {string} source - 'builtin', 'local', or 'npm'
229
+ * @property {number} priority
230
+ * @property {Array<string>} extensions
231
+ * @property {Array<PluginOption>} options
232
+ * @property {Array<PluginEnv>} env
233
+ * @property {Array<string>} examples
234
+ * @property {Function} detect
235
+ * @property {Function} extract
236
+ * @property {Function} transform
237
+ * @property {Function} translate
238
+ * @property {Function} translateBatch
239
+ */
240
+
241
+ /**
242
+ * @typedef {Object} PluginError
243
+ * @property {string} source
244
+ * @property {string} plugin
245
+ * @property {string} error
246
+ * @property {Array<string>} errors
247
+ */
248
+
249
+ /**
250
+ * @typedef {Object} PluginRegistry
251
+ * @property {Array<Plugin>} parsers
252
+ * @property {Array<Plugin>} adapters
253
+ * @property {Array<Plugin>} providers
254
+ * @property {Array<Plugin>} all
255
+ * @property {Map<string, Plugin>} byName
256
+ * @property {Array<PluginError>} errors
257
+ */
258
+
259
+ /**
260
+ * @typedef {Object} PluginValidation
261
+ * @property {boolean} valid
262
+ * @property {Array<string>} errors
263
+ */
264
+
265
+ /**
266
+ * @typedef {Object} DetectionContext
267
+ * @property {DetectedTech} framework
268
+ * @property {Array<DetectedTech>} libraries
269
+ * @property {DetectedTech} i18n
270
+ * @property {Object} packageJson
271
+ */
272
+
273
+ /**
274
+ * @typedef {Object} CommandOption
275
+ * @property {string} flag
276
+ * @property {string} description
277
+ * @property {boolean} required
278
+ */
279
+
280
+ /**
281
+ * @typedef {Object} CommandMeta
282
+ * @property {string} description
283
+ * @property {string} category
284
+ */
285
+
286
+ /**
287
+ * @typedef {Object} Command
288
+ * @property {string} name
289
+ * @property {string} description
290
+ * @property {string} category
291
+ * @property {Array<string>} aliases
292
+ * @property {Array<CommandOption>} options
293
+ * @property {CommandMeta} meta
294
+ * @property {Function} run
295
+ */
296
+
297
+ module.exports = {};
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview File system watcher for incremental extraction.
5
+ * Monitors .ts/.html files with debouncing and recursive support.
6
+ * @module watcher
7
+ */
8
+
9
+ const fs = require('./fs-adapter');
10
+ const path = require('path');
11
+
12
+ const WATCH_DEBOUNCE_MS = 500;
13
+ const watchDebounceMap = new Map();
14
+
15
+ function isWatchableFile(filename) {
16
+ if (!filename || !/\.(ts|html)$/.test(filename)) {
17
+ return false;
18
+ }
19
+ return !/\.(spec|test|e2e|mock)\./.test(filename);
20
+ }
21
+
22
+ function createWatchHandler(options = {}) {
23
+ const { srcDir, onFileChange, log = console.log } = options;
24
+
25
+ return filePath => {
26
+ const now = Date.now();
27
+ if (now - (watchDebounceMap.get(filePath) || 0) < WATCH_DEBOUNCE_MS) {
28
+ return;
29
+ }
30
+ watchDebounceMap.set(filePath, now);
31
+ log(
32
+ `\n[${new Date().toLocaleTimeString()}] Change detected: ${path.relative(srcDir, filePath)}`,
33
+ );
34
+ if (onFileChange) {
35
+ onFileChange(filePath);
36
+ }
37
+ };
38
+ }
39
+
40
+ function watchSubdirectory(dir, handleChange, excludedFolders) {
41
+ for (const file of fs.readdirSync(dir)) {
42
+ if (excludedFolders.includes(file)) {
43
+ continue;
44
+ }
45
+ const filePath = path.join(dir, file);
46
+ if (fs.statSync(filePath).isDirectory()) {
47
+ watchSubdirectory(filePath, handleChange, excludedFolders);
48
+ }
49
+ }
50
+ fs.watch(dir, { recursive: false }, (_, filename) => {
51
+ if (!isWatchableFile(filename)) {
52
+ return;
53
+ }
54
+ const filePath = path.join(dir, filename);
55
+ if (fs.existsSync(filePath)) {
56
+ handleChange(filePath);
57
+ }
58
+ });
59
+ }
60
+
61
+ function watchDirRecursive(dir, handleChange, excludedFolders = []) {
62
+ if (!fs.existsSync(dir)) {
63
+ return;
64
+ }
65
+ watchSubdirectory(dir, handleChange, excludedFolders);
66
+ }
67
+
68
+ function setupRecursiveWatch(ctx) {
69
+ const { srcDir, excludedFolders, handleChange, log } = ctx;
70
+ const watcher = fs.watch(srcDir, { recursive: true }, (_, filename) => {
71
+ if (!isWatchableFile(filename) || excludedFolders.some(f => filename.includes(f))) {
72
+ return;
73
+ }
74
+ const filePath = path.join(srcDir, filename);
75
+ if (fs.existsSync(filePath)) {
76
+ handleChange(filePath);
77
+ }
78
+ });
79
+ watcher.on('error', err => console.error('Watch error:', err.message));
80
+ log('Using recursive watch (native support)');
81
+ }
82
+
83
+ function setupFallbackWatch(ctx) {
84
+ const { srcDir, excludedFolders, handleChange, log } = ctx;
85
+ log('Recursive watch not supported, using directory watchers');
86
+ watchDirRecursive(srcDir, handleChange, excludedFolders);
87
+ }
88
+
89
+ function logWatchHeader(srcDir, log) {
90
+ log('Transloco Watch Mode');
91
+ log('='.repeat(50));
92
+ log(`Watching: ${srcDir}\nPress Ctrl+C to stop\n`);
93
+ }
94
+
95
+ function initWatch(ctx) {
96
+ try {
97
+ setupRecursiveWatch(ctx);
98
+ } catch {
99
+ setupFallbackWatch(ctx);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Watches source directory for .ts/.html changes
105
+ * @param {WatchOptions} [options]
106
+ * @example
107
+ * watchFiles({ srcDir: './src', onFileChange: path => extract(path) });
108
+ */
109
+ function watchFiles(options = {}) {
110
+ const { srcDir, excludedFolders = [], onFileChange, onStart, log = console.log } = options;
111
+ logWatchHeader(srcDir, log);
112
+ const handleChange = createWatchHandler({ srcDir, onFileChange, log });
113
+ initWatch({ srcDir, excludedFolders, handleChange, log });
114
+ if (onStart) {
115
+ onStart();
116
+ }
117
+ }
118
+
119
+ module.exports = { watchFiles };
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ const TRANSLATABLE_ATTRS = [
4
+ 'placeholder',
5
+ 'pTooltip',
6
+ 'tooltip',
7
+ 'title',
8
+ 'label',
9
+ 'header',
10
+ 'emptyMessage',
11
+ 'emptyFilterMessage',
12
+ 'summary',
13
+ 'detail',
14
+ 'message',
15
+ 'acceptLabel',
16
+ 'rejectLabel',
17
+ 'chooseLabel',
18
+ 'uploadLabel',
19
+ 'cancelLabel',
20
+ 'prefix',
21
+ 'suffix',
22
+ 'defaultLabel',
23
+ 'selectedItemsLabel',
24
+ 'text',
25
+ ];
26
+
27
+ const ATTR_REPLACEMENT_MAP = Object.fromEntries(
28
+ TRANSLATABLE_ATTRS.map(attr => [attr, attr === 'aria-label' ? 'attr.aria-label' : attr]),
29
+ );
30
+
31
+ const TAG_CONTENT_RE =
32
+ /(<(?:h[1-6]|p|span|div|li|td|th|a|button|label|option)[^>]*>)\s*(.+?)\s*(<\/(?:h[1-6]|p|span|div|li|td|th|a|button|label|option)>)/gi;
33
+
34
+ const hasTranslocoPipe = content => /\bTranslocoPipe\b/.test(content);
35
+ const hasTranslocoImport = content => /@jsverse\/transloco/.test(content);
36
+
37
+ function addToExistingImport(content) {
38
+ return content.replace(
39
+ /import\s*\{([^}]+)\}\s*from\s*['"]@jsverse\/transloco['"]/,
40
+ (match, imports) => {
41
+ const importList = imports.split(',').map(s => s.trim());
42
+ if (!importList.includes('TranslocoPipe')) {
43
+ importList.push('TranslocoPipe');
44
+ }
45
+ return `import { ${importList.join(', ')} } from '@jsverse/transloco'`;
46
+ },
47
+ );
48
+ }
49
+
50
+ function addNewTranslocoImport(content) {
51
+ const lastImportMatch = content.match(/^(import\s+.+from\s+['"][^'"]+['"];?\s*\n)/gm);
52
+ if (!lastImportMatch) {
53
+ return content;
54
+ }
55
+ const lastImport = lastImportMatch.at(-1);
56
+ const insertPos = content.lastIndexOf(lastImport) + lastImport.length;
57
+ return `${content.slice(0, insertPos)}import { TranslocoPipe } from '@jsverse/transloco';\n${content.slice(insertPos)}`;
58
+ }
59
+
60
+ function addPipeToImportsArray(content) {
61
+ return content.replace(/imports\s*:\s*\[([^\]]+)\]/, (match, imports) => {
62
+ const importList = imports
63
+ .split(',')
64
+ .map(s => s.trim())
65
+ .filter(Boolean);
66
+ if (!importList.includes('TranslocoPipe')) {
67
+ importList.push('TranslocoPipe');
68
+ }
69
+ return `imports: [${importList.join(', ')}]`;
70
+ });
71
+ }
72
+
73
+ function addTranslocoPipeImport(content) {
74
+ if (hasTranslocoPipe(content)) {
75
+ return content;
76
+ }
77
+ const withImport =
78
+ hasTranslocoImport(content) ? addToExistingImport(content) : addNewTranslocoImport(content);
79
+ return addPipeToImportsArray(withImport);
80
+ }
81
+
82
+ function replaceAttributes(content, rawText, translocoExpr) {
83
+ let result = content;
84
+ let replacements = 0;
85
+ for (const attr of TRANSLATABLE_ATTRS) {
86
+ const search = `${attr}="${rawText}"`;
87
+ if (!result.includes(search)) {
88
+ continue;
89
+ }
90
+ const bindAttr = ATTR_REPLACEMENT_MAP[attr] ?? attr;
91
+ result = result.replaceAll(search, `[${bindAttr}]=${translocoExpr}`);
92
+ replacements++;
93
+ }
94
+ return { content: result, replacements };
95
+ }
96
+
97
+ function buildTagReplacement(parts, rawText, key) {
98
+ const [, open, text, close] = parts;
99
+ return text.trim() === rawText ? `${open}{{ '${key}' | transloco }}${close}` : parts[0];
100
+ }
101
+
102
+ function replaceTagContent(content, rawText, key) {
103
+ const result = content.replace(TAG_CONTENT_RE, (...parts) =>
104
+ buildTagReplacement(parts, rawText, key),
105
+ );
106
+ return { content: result, replaced: result !== content };
107
+ }
108
+
109
+ function transformTemplate(ctx) {
110
+ const { content, rawText, key } = ctx;
111
+ const translocoExpr = `"'${key}' | transloco"`;
112
+ const result = replaceAttributes(content, rawText, translocoExpr);
113
+
114
+ if (result.replacements === 0) {
115
+ const tagResult = replaceTagContent(result.content, rawText, key);
116
+ return { content: tagResult.content, replacements: tagResult.replaced ? 1 : 0 };
117
+ }
118
+ return result;
119
+ }
120
+
121
+ module.exports = {
122
+ name: 'adapter-transloco',
123
+ type: 'adapter',
124
+
125
+ meta: {
126
+ description: 'Transform extracted strings to Transloco pipe syntax',
127
+ version: '1.0.0',
128
+ },
129
+
130
+ examples: ['i18nkit --extract --apply (uses Transloco syntax)'],
131
+
132
+ detect(ctx) {
133
+ return (
134
+ ctx.pkg.dependencies?.['@jsverse/transloco'] || ctx.pkg.dependencies?.['@ngneat/transloco']
135
+ );
136
+ },
137
+
138
+ TRANSLATABLE_ATTRS,
139
+ ATTR_REPLACEMENT_MAP,
140
+
141
+ transform(ctx) {
142
+ const { content, rawText, key, context } = ctx;
143
+ if (context.startsWith('ts_')) {
144
+ return { content, replacements: 0 };
145
+ }
146
+ return transformTemplate({ content, rawText, key });
147
+ },
148
+
149
+ updateImports(tsContent) {
150
+ return addTranslocoPipeImport(tsContent);
151
+ },
152
+
153
+ addTranslocoPipeImport,
154
+ replaceAttributes,
155
+ replaceTagContent,
156
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const parserUtils = require('../core/parser-utils');
4
+
5
+ const EXTRACTION_PATTERNS = [
6
+ { regex: /<(h[1-6])(?:\s[^>]*)?>([^<]+)<\/\1>/gi, context: 'titles', group: 2, attr: null },
7
+ {
8
+ regex: /<(p|span|div|li|td|th)(?:\s[^>]*)?>([^<]+)<\/\1>/gi,
9
+ context: 'text',
10
+ group: 2,
11
+ attr: null,
12
+ },
13
+ { regex: /<(a)(?:\s[^>]*)?>([^<]+)<\/\1>/gi, context: 'links', group: 2, attr: null },
14
+ { regex: /<(button)(?:\s[^>]*)?>([^<]+)<\/\1>/gi, context: 'buttons', group: 2, attr: null },
15
+ { regex: /<(label)(?:\s[^>]*)?>([^<]+)<\/\1>/gi, context: 'labels', group: 2, attr: null },
16
+ { regex: /<(option)(?:\s[^>]*)?>([^<]+)<\/\1>/gi, context: 'options', group: 2, attr: null },
17
+ { regex: /\bplaceholder="([^"]+)"/gi, context: 'placeholders', attr: 'placeholder' },
18
+ { regex: /\b(?:pTooltip|tooltip)="([^"]+)"/gi, context: 'tooltips', attr: 'pTooltip' },
19
+ { regex: /\baria-label="([^"]+)"/gi, context: 'aria', attr: 'aria-label' },
20
+ { regex: /\btitle="([^"]+)"/gi, context: 'titles', attr: 'title' },
21
+ ];
22
+
23
+ module.exports = {
24
+ name: 'parser-angular',
25
+ type: 'parser',
26
+
27
+ meta: {
28
+ description: 'Extract i18n strings from Angular HTML templates',
29
+ version: '1.0.0',
30
+ },
31
+
32
+ extensions: ['.html', '.component.html'],
33
+ priority: 10,
34
+
35
+ options: [
36
+ {
37
+ flag: '--skip-translated',
38
+ type: 'boolean',
39
+ description: 'Skip already translated strings (default: true)',
40
+ },
41
+ ],
42
+
43
+ examples: ['i18nkit --extract --src src/app'],
44
+
45
+ detect(ctx) {
46
+ return ctx.pkg.dependencies?.['@angular/core'] || ctx.files.includes('angular.json');
47
+ },
48
+
49
+ EXTRACTION_PATTERNS,
50
+
51
+ extract(content, filePath, options = {}) {
52
+ const { skipTranslated = true } = options;
53
+ const cleaned = parserUtils.cleanTranslocoExpressions(content, skipTranslated);
54
+ return parserUtils.extractWithPatterns(cleaned, EXTRACTION_PATTERNS);
55
+ },
56
+ };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const parserUtils = require('../core/parser-utils');
4
+
5
+ const PRIMENG_PATTERNS = [
6
+ { regex: /<p-(?:button|splitButton)[^>]*\blabel="([^"]+)"/gi, context: 'buttons', attr: 'label' },
7
+ {
8
+ regex: /<p-(?:card|dialog|panel|fieldset)[^>]*\bheader="([^"]+)"/gi,
9
+ context: 'titles',
10
+ attr: 'header',
11
+ },
12
+ {
13
+ regex: /<p-(?:tabPanel|accordionTab|steps?)[^>]*\bheader="([^"]+)"/gi,
14
+ context: 'titles',
15
+ attr: 'header',
16
+ },
17
+ { regex: /<p-column[^>]*\bheader="([^"]+)"/gi, context: 'columns', attr: 'header' },
18
+ { regex: /<p-table[^>]*\bemptyMessage="([^"]+)"/gi, context: 'messages', attr: 'emptyMessage' },
19
+ {
20
+ regex: /<p-confirm[^>]*\b(?:message|header)="([^"]+)"/gi,
21
+ context: 'messages',
22
+ attr: 'message',
23
+ },
24
+ {
25
+ regex: /<p-confirm[^>]*\b(?:acceptLabel|rejectLabel)="([^"]+)"/gi,
26
+ context: 'buttons',
27
+ attr: 'acceptLabel',
28
+ },
29
+ {
30
+ regex:
31
+ /<p-(?:dropdown|multiSelect|listbox)[^>]*\b(?:placeholder|emptyMessage|emptyFilterMessage|defaultLabel|selectedItemsLabel)="([^"]+)"/gi,
32
+ context: 'placeholders',
33
+ attr: 'placeholder',
34
+ },
35
+ {
36
+ regex: /<p-fileUpload[^>]*\b(?:chooseLabel|uploadLabel|cancelLabel)="([^"]+)"/gi,
37
+ context: 'buttons',
38
+ attr: 'chooseLabel',
39
+ },
40
+ { regex: /<p-(?:chip|tag)[^>]*\blabel="([^"]+)"/gi, context: 'labels', attr: 'label' },
41
+ {
42
+ regex: /<p-(?:inputNumber|calendar)[^>]*\b(?:prefix|suffix)="([^"]+)"/gi,
43
+ context: 'labels',
44
+ attr: 'prefix',
45
+ },
46
+ {
47
+ regex: /<p-message[^>]*\b(?:text|summary|detail)="([^"]+)"/gi,
48
+ context: 'messages',
49
+ attr: 'text',
50
+ },
51
+ { regex: /<p-toast[^>]*\bsummary="([^"]+)"/gi, context: 'messages', attr: 'summary' },
52
+ ];
53
+
54
+ module.exports = {
55
+ name: 'parser-primeng',
56
+ type: 'parser',
57
+
58
+ meta: {
59
+ description: 'Extract i18n strings from PrimeNG component attributes',
60
+ version: '1.0.0',
61
+ },
62
+
63
+ extensions: ['.html', '.component.html'],
64
+ priority: 20,
65
+
66
+ examples: ['i18nkit --extract (auto-activated with PrimeNG)'],
67
+
68
+ detect(ctx) {
69
+ return ctx.pkg.dependencies?.primeng;
70
+ },
71
+
72
+ PRIMENG_PATTERNS,
73
+
74
+ extract(content, filePath, options = {}) {
75
+ const { skipTranslated = true } = options;
76
+ const cleaned = parserUtils.cleanTranslocoExpressions(content, skipTranslated);
77
+ return parserUtils.extractWithPatterns(cleaned, PRIMENG_PATTERNS);
78
+ },
79
+ };