i18next-cli 1.34.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli.js +271 -1
  3. package/dist/cjs/config.js +211 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +364 -1
  5. package/dist/cjs/extractor/core/extractor.js +245 -1
  6. package/dist/cjs/extractor/core/key-finder.js +132 -1
  7. package/dist/cjs/extractor/core/translation-manager.js +745 -1
  8. package/dist/cjs/extractor/parsers/ast-utils.js +85 -1
  9. package/dist/cjs/extractor/parsers/call-expression-handler.js +941 -1
  10. package/dist/cjs/extractor/parsers/comment-parser.js +375 -1
  11. package/dist/cjs/extractor/parsers/expression-resolver.js +362 -1
  12. package/dist/cjs/extractor/parsers/jsx-handler.js +492 -1
  13. package/dist/cjs/extractor/parsers/jsx-parser.js +355 -1
  14. package/dist/cjs/extractor/parsers/scope-manager.js +408 -1
  15. package/dist/cjs/extractor/plugin-manager.js +106 -1
  16. package/dist/cjs/heuristic-config.js +99 -1
  17. package/dist/cjs/index.js +28 -1
  18. package/dist/cjs/init.js +174 -1
  19. package/dist/cjs/linter.js +431 -1
  20. package/dist/cjs/locize.js +269 -1
  21. package/dist/cjs/migrator.js +196 -1
  22. package/dist/cjs/rename-key.js +354 -1
  23. package/dist/cjs/status.js +336 -1
  24. package/dist/cjs/syncer.js +120 -1
  25. package/dist/cjs/types-generator.js +165 -1
  26. package/dist/cjs/utils/default-value.js +43 -1
  27. package/dist/cjs/utils/file-utils.js +136 -1
  28. package/dist/cjs/utils/funnel-msg-tracker.js +75 -1
  29. package/dist/cjs/utils/logger.js +36 -1
  30. package/dist/cjs/utils/nested-object.js +124 -1
  31. package/dist/cjs/utils/validation.js +71 -1
  32. package/dist/esm/cli.js +269 -1
  33. package/dist/esm/config.js +206 -1
  34. package/dist/esm/extractor/core/ast-visitors.js +362 -1
  35. package/dist/esm/extractor/core/extractor.js +241 -1
  36. package/dist/esm/extractor/core/key-finder.js +130 -1
  37. package/dist/esm/extractor/core/translation-manager.js +743 -1
  38. package/dist/esm/extractor/parsers/ast-utils.js +80 -1
  39. package/dist/esm/extractor/parsers/call-expression-handler.js +939 -1
  40. package/dist/esm/extractor/parsers/comment-parser.js +373 -1
  41. package/dist/esm/extractor/parsers/expression-resolver.js +360 -1
  42. package/dist/esm/extractor/parsers/jsx-handler.js +490 -1
  43. package/dist/esm/extractor/parsers/jsx-parser.js +334 -1
  44. package/dist/esm/extractor/parsers/scope-manager.js +406 -1
  45. package/dist/esm/extractor/plugin-manager.js +103 -1
  46. package/dist/esm/heuristic-config.js +97 -1
  47. package/dist/esm/index.js +11 -1
  48. package/dist/esm/init.js +172 -1
  49. package/dist/esm/linter.js +425 -1
  50. package/dist/esm/locize.js +265 -1
  51. package/dist/esm/migrator.js +194 -1
  52. package/dist/esm/rename-key.js +352 -1
  53. package/dist/esm/status.js +334 -1
  54. package/dist/esm/syncer.js +118 -1
  55. package/dist/esm/types-generator.js +163 -1
  56. package/dist/esm/utils/default-value.js +41 -1
  57. package/dist/esm/utils/file-utils.js +131 -1
  58. package/dist/esm/utils/funnel-msg-tracker.js +72 -1
  59. package/dist/esm/utils/logger.js +34 -1
  60. package/dist/esm/utils/nested-object.js +120 -1
  61. package/dist/esm/utils/validation.js +68 -1
  62. package/package.json +2 -2
  63. package/types/locize.d.ts.map +1 -1
@@ -1 +1,41 @@
1
- function t(t,r,n,e,u){if("function"==typeof t)try{return t(r,n,e,u||r)}catch(t){return""}return t||""}export{t as resolveDefaultValue};
1
+ /**
2
+ * Resolves the default value for a missing key in secondary languages.
3
+ * Supports both string and function-based default values.
4
+ *
5
+ * @param defaultValue - The configured default value (string or function)
6
+ * @param key - The translation key
7
+ * @param namespace - The namespace for the key
8
+ * @param language - The target language
9
+ * @returns The resolved default value
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // String-based default value
14
+ * const result1 = resolveDefaultValue('[MISSING]', 'user.name', 'common', 'de', 'Alice')
15
+ * // Returns: '[MISSING]'
16
+ *
17
+ * // Function-based default value
18
+ * const defaultValueFn = (key, ns, lang) => `${lang.toUpperCase()}_${ns}_${key}`
19
+ * const result2 = resolveDefaultValue(defaultValueFn, 'user.name', 'common', 'de', 'Alice')
20
+ * // Returns: 'DE_common_user.name_Alice'
21
+ *
22
+ * // Error handling - function throws
23
+ * const errorFn = () => { throw new Error('Oops') }
24
+ * const result3 = resolveDefaultValue(errorFn, 'user.name', 'common', 'de', 'Alice')
25
+ * // Returns: '' (fallback to empty string)
26
+ * ```
27
+ */
28
+ function resolveDefaultValue(defaultValue, key, namespace, language, value) {
29
+ if (typeof defaultValue === 'function') {
30
+ try {
31
+ return defaultValue(key, namespace, language, value || key);
32
+ }
33
+ catch (error) {
34
+ // If the function throws an error, fall back to empty string
35
+ return '';
36
+ }
37
+ }
38
+ return defaultValue || '';
39
+ }
40
+
41
+ export { resolveDefaultValue };
@@ -1 +1,131 @@
1
- import{access as t,readFile as n}from"node:fs/promises";import{resolve as r,normalize as e,extname as o}from"node:path";import{createJiti as a}from"jiti";import{getTsConfigAliases as s}from"../config.js";import{JsonParser as c,JsonObjectNode as i}from"@croct/json5-parser";function u(t,n,r){if(!t)return e(`locales/${n}/${r??"translation"}.json`);if("function"==typeof t)try{const o=String(t(n,r));return e(o.replace(/\/\/+/g,"/"))}catch{return e(`locales/${n}/${r??"translation"}.json`)}let o=String(t);return o=o.replace(/\{\{language\}\}|\{\{lng\}\}/g,n),o=null!=r?o.replace(/\{\{namespace\}\}/g,r):o.replace(/\/?\{\{namespace\}\}/g,""),o=o.replace(/\/\/+/g,"/"),e(o)}async function l(e){const u=r(process.cwd(),e);try{await t(u)}catch{return null}try{const t=o(u).toLowerCase();if(".json5"===t){const t=await n(u,"utf-8");return c.parse(t,i).toJSON()}if(".json"===t){const t=await n(u,"utf-8");return JSON.parse(t)}if(".ts"===t||".js"===t){const t=await s(),n=a(process.cwd(),{alias:t,interopDefault:!0});return await n.import(u,{default:!0})}return null}catch(t){return console.warn(`Could not parse translation file ${e}:`,t),null}}async function p(e){const o=r(process.cwd(),e);try{return await t(o),await n(o,"utf-8")}catch{return null}}function f(t,n="json",r=2,e){const o=JSON.stringify(t,null,r);switch(n){case"json5":if(e){const n=c.parse(e,i);return n.update(t),n.toString({object:{indentationSize:Number(r)??2}})}return c.parse(o,i).toString({object:{indentationSize:Number(r)??2}});case"js":case"js-esm":return`export default ${o};\n`;case"js-cjs":return`module.exports = ${o};\n`;case"ts":return`export default ${o} as const;\n`;default:return`${o}\n`}}export{u as getOutputPath,p as loadRawJson5Content,l as loadTranslationFile,f as serializeTranslationFile};
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { resolve, normalize, extname } from 'node:path';
3
+ import { createJiti } from 'jiti';
4
+ import { getTsConfigAliases } from '../config.js';
5
+ import { JsonParser, JsonObjectNode } from '@croct/json5-parser';
6
+
7
+ /**
8
+ * Resolve an output template (string or function) into an actual path string.
9
+ *
10
+ * - If `outputTemplate` is a function, call it with (language, namespace)
11
+ * - If it's a string, replace placeholders:
12
+ * - {{language}} or {{lng}} -> language
13
+ * - {{namespace}} -> namespace (or removed if namespace is undefined)
14
+ * - Normalizes duplicate slashes and returns a platform-correct path.
15
+ */
16
+ function getOutputPath(outputTemplate, language, namespace) {
17
+ if (!outputTemplate) {
18
+ // Fallback to a sensible default
19
+ return normalize(`locales/${language}/${namespace ?? 'translation'}.json`);
20
+ }
21
+ if (typeof outputTemplate === 'function') {
22
+ try {
23
+ const result = String(outputTemplate(language, namespace));
24
+ return normalize(result.replace(/\/\/+/g, '/'));
25
+ }
26
+ catch {
27
+ // If user function throws, fallback to default path
28
+ return normalize(`locales/${language}/${namespace ?? 'translation'}.json`);
29
+ }
30
+ }
31
+ // It's a string template
32
+ let out = String(outputTemplate);
33
+ out = out.replace(/\{\{language\}\}|\{\{lng\}\}/g, language);
34
+ if (namespace !== undefined && namespace !== null) {
35
+ out = out.replace(/\{\{namespace\}\}/g, namespace);
36
+ }
37
+ else {
38
+ // remove any occurrences of /{{namespace}} or {{namespace}} (keeping surrounding slashes tidy)
39
+ out = out.replace(/\/?\{\{namespace\}\}/g, '');
40
+ }
41
+ // collapse duplicate slashes and normalize to platform-specific separators
42
+ out = out.replace(/\/\/+/g, '/');
43
+ return normalize(out);
44
+ }
45
+ /**
46
+ * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
47
+ * @param filePath - The path to the translation file.
48
+ * @returns The parsed content of the file, or null if not found or failed to parse.
49
+ */
50
+ async function loadTranslationFile(filePath) {
51
+ const fullPath = resolve(process.cwd(), filePath);
52
+ try {
53
+ await access(fullPath);
54
+ }
55
+ catch {
56
+ return null; // File doesn't exist
57
+ }
58
+ try {
59
+ const ext = extname(fullPath).toLowerCase();
60
+ if (ext === '.json5') {
61
+ const content = await readFile(fullPath, 'utf-8');
62
+ // Parse as a JSON5 object node
63
+ const node = JsonParser.parse(content, JsonObjectNode);
64
+ return node.toJSON();
65
+ }
66
+ else if (ext === '.json') {
67
+ const content = await readFile(fullPath, 'utf-8');
68
+ return JSON.parse(content);
69
+ }
70
+ else if (ext === '.ts' || ext === '.js') {
71
+ // Load TypeScript path aliases for proper module resolution
72
+ const aliases = await getTsConfigAliases();
73
+ const jiti = createJiti(process.cwd(), {
74
+ alias: aliases,
75
+ interopDefault: true,
76
+ });
77
+ const module = await jiti.import(fullPath, { default: true });
78
+ return module;
79
+ }
80
+ return null; // Unsupported file type
81
+ }
82
+ catch (error) {
83
+ console.warn(`Could not parse translation file ${filePath}:`, error);
84
+ return null;
85
+ }
86
+ }
87
+ // Helper to load raw JSON5 content for preservation
88
+ async function loadRawJson5Content(filePath) {
89
+ const fullPath = resolve(process.cwd(), filePath);
90
+ try {
91
+ await access(fullPath);
92
+ return await readFile(fullPath, 'utf-8');
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * Serializes a translation object into a string based on the desired format.
100
+ * For JSON5, preserves comments and formatting using JsonObjectNode.update().
101
+ */
102
+ function serializeTranslationFile(data, format = 'json', indentation = 2, rawContent // Pass raw content for JSON5 preservation
103
+ ) {
104
+ const jsonString = JSON.stringify(data, null, indentation);
105
+ switch (format) {
106
+ case 'json5': {
107
+ if (rawContent) {
108
+ // Parse the original JSON5 file, update it, and output as string
109
+ const node = JsonParser.parse(rawContent, JsonObjectNode);
110
+ node.update(data);
111
+ return node.toString({ object: { indentationSize: Number(indentation) ?? 2 } });
112
+ }
113
+ // Fallback: create a new node by parsing the generated JSON string and output as string
114
+ const node = JsonParser.parse(jsonString, JsonObjectNode);
115
+ return node.toString({ object: { indentationSize: Number(indentation) ?? 2 } });
116
+ }
117
+ case 'js':
118
+ case 'js-esm':
119
+ return `export default ${jsonString};\n`;
120
+ case 'js-cjs':
121
+ return `module.exports = ${jsonString};\n`;
122
+ case 'ts':
123
+ // Using `as const` provides better type inference for TypeScript users
124
+ return `export default ${jsonString} as const;\n`;
125
+ case 'json':
126
+ default:
127
+ return `${jsonString}\n`;
128
+ }
129
+ }
130
+
131
+ export { getOutputPath, loadRawJson5Content, loadTranslationFile, serializeTranslationFile };
@@ -1 +1,72 @@
1
- import{join as t}from"node:path";import{tmpdir as n}from"node:os";import{readFile as o,writeFile as r}from"node:fs/promises";const a={},e=t(n(),"i18next-cli-last-funnel-message-shown.json");async function s(t){if(a[t])return!1;try{const n=await o(e,"utf-8"),r=JSON.parse(n);if(Date.now()-(r[t]||0)<864e5)return!1}catch(t){}return!0}async function i(t){try{a[t]=!0;let n={};try{const t=await o(e,"utf-8");n=JSON.parse(t)}catch(t){}n[t]=Date.now(),await r(e,JSON.stringify(n))}catch(t){}}export{i as recordFunnelShown,s as shouldShowFunnel};
1
+ import { join } from 'node:path';
2
+ import { tmpdir } from 'node:os';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+
5
+ /**
6
+ * In-memory cache to track which funnel messages have been shown in the current session.
7
+ */
8
+ const hasLocizeFunnelBeenShown = {};
9
+ /**
10
+ * Path to the persistent file that stores the last time each funnel message was shown.
11
+ * Stored in the OS temporary directory to persist across CLI sessions.
12
+ */
13
+ const LAST_FUNNEL_FILE = join(tmpdir(), 'i18next-cli-last-funnel-message-shown.json'); // Store in OS temp dir
14
+ /**
15
+ * Cooldown period in milliseconds before a funnel message can be shown again.
16
+ * Currently set to 24 hours.
17
+ */
18
+ const TIP_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
19
+ /**
20
+ * Determines whether a funnel message should be shown to the user.
21
+ *
22
+ * A funnel message will not be shown if:
23
+ * - It has already been shown in the current session (in-memory cache)
24
+ * - It was shown within the last 24 hours (persistent file cache)
25
+ *
26
+ * @param funnelMessage - The unique identifier for the funnel message
27
+ * @returns Promise that resolves to true if the message should be shown, false otherwise
28
+ */
29
+ async function shouldShowFunnel(funnelMessage) {
30
+ if (hasLocizeFunnelBeenShown[funnelMessage])
31
+ return false;
32
+ try {
33
+ const content = await readFile(LAST_FUNNEL_FILE, 'utf-8');
34
+ const cnt = JSON.parse(content);
35
+ if (Date.now() - (cnt[funnelMessage] || 0) < TIP_COOLDOWN_MS) {
36
+ return false; // Less than 24 hours since last shown
37
+ }
38
+ }
39
+ catch (e) {
40
+ // File doesn't exist or is invalid, assume it's okay to show the tip
41
+ }
42
+ return true;
43
+ }
44
+ /**
45
+ * Records that a funnel message has been shown to the user.
46
+ *
47
+ * Updates both the in-memory cache and the persistent file cache with the current timestamp.
48
+ * This prevents the message from being shown again within the cooldown period.
49
+ *
50
+ * @param funnelMessage - The unique identifier for the funnel message that was shown
51
+ * @returns Promise that resolves when the record has been updated
52
+ */
53
+ async function recordFunnelShown(funnelMessage) {
54
+ try {
55
+ hasLocizeFunnelBeenShown[funnelMessage] = true;
56
+ let data = {};
57
+ try {
58
+ const existing = await readFile(LAST_FUNNEL_FILE, 'utf-8');
59
+ data = JSON.parse(existing);
60
+ }
61
+ catch (err) {
62
+ // ignore, we'll create a new file
63
+ }
64
+ data[funnelMessage] = Date.now();
65
+ await writeFile(LAST_FUNNEL_FILE, JSON.stringify(data));
66
+ }
67
+ catch (e) {
68
+ // Ignore errors here, it's just a best-effort cache
69
+ }
70
+ }
71
+
72
+ export { recordFunnelShown, shouldShowFunnel };
@@ -1 +1,34 @@
1
- class o{info(o){console.log(o)}warn(o){console.warn(o)}error(o){console.error(o)}}export{o as ConsoleLogger};
1
+ /**
2
+ * Default console-based logger implementation for the i18next toolkit.
3
+ * Provides basic logging functionality with different severity levels.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const logger = new ConsoleLogger()
8
+ * logger.info('Extraction started')
9
+ * logger.warn('Deprecated configuration option used')
10
+ * logger.error('Failed to parse file')
11
+ * ```
12
+ */
13
+ class ConsoleLogger {
14
+ /**
15
+ * Logs an informational message to the console.
16
+ *
17
+ * @param message - The message to log
18
+ */
19
+ info(message) { console.log(message); }
20
+ /**
21
+ * Logs a warning message to the console.
22
+ *
23
+ * @param message - The warning message to log
24
+ */
25
+ warn(message) { console.warn(message); }
26
+ /**
27
+ * Logs an error message to the console.
28
+ *
29
+ * @param message - The error message to log
30
+ */
31
+ error(message) { console.error(message); }
32
+ }
33
+
34
+ export { ConsoleLogger };
@@ -1 +1,120 @@
1
- function t(t,e,n,r){if(!1===r)return void(t[e]=n);const o=e.split(r);let i=t;for(let r=0;r<o.length;r++){const u=o[r];if(r===o.length-1)return void(i[u]=n);const c=i[u];if(void 0!==c&&("object"!=typeof c||null===c))return void(t[e]=n);void 0===c&&(i[u]={}),i=i[u]}}function e(t,e,n){return!1===n?t[e]:e.split(n).reduce((t,e)=>t&&t[e],t)}function n(t,e,r=""){return!1===e?Object.keys(t):Object.entries(t).reduce((t,[o,i])=>{const u=r?`${r}${e}${o}`:o;return"object"!=typeof i||null===i||Array.isArray(i)?t.push(u):t.push(...n(i,e,u)),t},[])}export{n as getNestedKeys,e as getNestedValue,t as setNestedValue};
1
+ /**
2
+ * Sets a nested value in an object using a key path and separator.
3
+ * Creates intermediate objects as needed.
4
+ *
5
+ * @param obj - The target object to modify
6
+ * @param path - The key path (e.g., 'user.profile.name')
7
+ * @param value - The value to set
8
+ * @param keySeparator - The separator to use for splitting the path, or false for flat keys
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const obj = {}
13
+ * setNestedValue(obj, 'user.profile.name', 'John', '.')
14
+ * // Result: { user: { profile: { name: 'John' } } }
15
+ *
16
+ * // With flat keys
17
+ * setNestedValue(obj, 'user.name', 'Jane', false)
18
+ * // Result: { 'user.name': 'Jane' }
19
+ * ```
20
+ */
21
+ function setNestedValue(obj, path, value, keySeparator) {
22
+ if (keySeparator === false) {
23
+ obj[path] = value;
24
+ return;
25
+ }
26
+ const keys = path.split(keySeparator);
27
+ let current = obj;
28
+ for (let i = 0; i < keys.length; i++) {
29
+ const key = keys[i];
30
+ const isLastKey = i === keys.length - 1;
31
+ if (isLastKey) {
32
+ // We've reached the end of the path, set the value.
33
+ current[key] = value;
34
+ return;
35
+ }
36
+ const nextLevel = current[key];
37
+ // Check for a conflict: the path requires an object, but a primitive exists.
38
+ if (nextLevel !== undefined && (typeof nextLevel !== 'object' || nextLevel === null)) {
39
+ // Conflict detected. The parent path is already a leaf node.
40
+ // We must set the entire original path as a flat key on the root object.
41
+ obj[path] = value;
42
+ return; // Stop processing to prevent overwriting the parent.
43
+ }
44
+ // If the path doesn't exist, create an empty object to continue.
45
+ if (nextLevel === undefined) {
46
+ current[key] = {};
47
+ }
48
+ current = current[key];
49
+ }
50
+ }
51
+ /**
52
+ * Retrieves a nested value from an object using a key path and separator.
53
+ *
54
+ * @param obj - The object to search in
55
+ * @param path - The key path (e.g., 'user.profile.name')
56
+ * @param keySeparator - The separator to use for splitting the path, or false for flat keys
57
+ * @returns The found value or undefined if not found
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const obj = { user: { profile: { name: 'John' } } }
62
+ * const name = getNestedValue(obj, 'user.profile.name', '.')
63
+ * // Returns: 'John'
64
+ *
65
+ * // With flat keys
66
+ * const flatObj = { 'user.name': 'Jane' }
67
+ * const name = getNestedValue(flatObj, 'user.name', false)
68
+ * // Returns: 'Jane'
69
+ * ```
70
+ */
71
+ function getNestedValue(obj, path, keySeparator) {
72
+ if (keySeparator === false) {
73
+ return obj[path];
74
+ }
75
+ return path.split(keySeparator).reduce((acc, key) => acc && acc[key], obj);
76
+ }
77
+ /**
78
+ * Extracts all nested keys from an object, optionally with a prefix.
79
+ * Recursively traverses the object structure to build a flat list of key paths.
80
+ *
81
+ * @param obj - The object to extract keys from
82
+ * @param keySeparator - The separator to use for joining keys, or false for flat keys
83
+ * @param prefix - Optional prefix to prepend to all keys
84
+ * @returns Array of all nested key paths
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const obj = {
89
+ * user: {
90
+ * profile: { name: 'John', age: 30 },
91
+ * settings: { theme: 'dark' }
92
+ * }
93
+ * }
94
+ *
95
+ * const keys = getNestedKeys(obj, '.')
96
+ * // Returns: ['user.profile.name', 'user.profile.age', 'user.settings.theme']
97
+ *
98
+ * // With flat keys
99
+ * const flatObj = { 'user.name': 'Jane', 'user.age': 25 }
100
+ * const flatKeys = getNestedKeys(flatObj, false)
101
+ * // Returns: ['user.name', 'user.age']
102
+ * ```
103
+ */
104
+ function getNestedKeys(obj, keySeparator, prefix = '') {
105
+ if (keySeparator === false) {
106
+ return Object.keys(obj);
107
+ }
108
+ return Object.entries(obj).reduce((acc, [key, value]) => {
109
+ const newKey = prefix ? `${prefix}${keySeparator}${key}` : key;
110
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
111
+ acc.push(...getNestedKeys(value, keySeparator, newKey));
112
+ }
113
+ else {
114
+ acc.push(newKey);
115
+ }
116
+ return acc;
117
+ }, []);
118
+ }
119
+
120
+ export { getNestedKeys, getNestedValue, setNestedValue };
@@ -1 +1,68 @@
1
- function t(t){if(!t.extract.input?.length)throw new e("extract.input must be specified and non-empty");if(!t.extract.output)throw new e("extract.output must be specified");if(!t.locales?.length)throw new e("locales must be specified and non-empty");if("string"==typeof t.extract.output&&!t.extract.output.includes("{{language}}")&&!t.extract.output.includes("{{lng}}"))throw new e("extract.output must contain {{language}} placeholder")}class e extends Error{file;cause;constructor(t,e,n){super(e?`${t} in file ${e}`:t),this.file=e,this.cause=n,this.name="ExtractorError"}}export{e as ExtractorError,t as validateExtractorConfig};
1
+ /**
2
+ * Validates the extractor configuration to ensure required fields are present and properly formatted.
3
+ *
4
+ * This function performs the following validations:
5
+ * - Ensures extract.input is specified and non-empty
6
+ * - Ensures extract.output is specified
7
+ * - Ensures locales array is specified and non-empty
8
+ * - Ensures extract.output contains the required {{language}} placeholder
9
+ *
10
+ * @param config - The i18next toolkit configuration object to validate
11
+ *
12
+ * @throws {ExtractorError} When any validation rule fails
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * try {
17
+ * validateExtractorConfig(config)
18
+ * console.log('Configuration is valid')
19
+ * } catch (error) {
20
+ * console.error('Invalid configuration:', error.message)
21
+ * }
22
+ * ```
23
+ */
24
+ function validateExtractorConfig(config) {
25
+ if (!config.extract.input?.length) {
26
+ throw new ExtractorError('extract.input must be specified and non-empty');
27
+ }
28
+ if (!config.extract.output) {
29
+ throw new ExtractorError('extract.output must be specified');
30
+ }
31
+ if (!config.locales?.length) {
32
+ throw new ExtractorError('locales must be specified and non-empty');
33
+ }
34
+ // If output is a function, we accept it (user is responsible for producing a valid path).
35
+ if (typeof config.extract.output === 'string') {
36
+ if (!config.extract.output.includes('{{language}}') && !config.extract.output.includes('{{lng}}')) {
37
+ throw new ExtractorError('extract.output must contain {{language}} placeholder');
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Custom error class for extraction-related errors.
43
+ * Provides additional context like file path and underlying cause.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * throw new ExtractorError('Failed to parse file', 'src/component.tsx', syntaxError)
48
+ * ```
49
+ */
50
+ class ExtractorError extends Error {
51
+ file;
52
+ cause;
53
+ /**
54
+ * Creates a new ExtractorError with optional file context and cause.
55
+ *
56
+ * @param message - The error message
57
+ * @param file - Optional file path where the error occurred
58
+ * @param cause - Optional underlying error that caused this error
59
+ */
60
+ constructor(message, file, cause) {
61
+ super(file ? `${message} in file ${file}` : message);
62
+ this.file = file;
63
+ this.cause = cause;
64
+ this.name = 'ExtractorError';
65
+ }
66
+ }
67
+
68
+ export { ExtractorError, validateExtractorConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  }
30
30
  },
31
31
  "scripts": {
32
- "build": "rm -rf dist && rollup -c && chmod +x dist/esm/cli.js && chmod +x dist/cjs/cli.js && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && rm -rf types && tsc -d --declarationDir types --declarationMap --emitDeclarationOnly",
32
+ "build": "rm -rf dist && rollup -c && rm -rf types && tsc -d --declarationDir types --declarationMap --emitDeclarationOnly",
33
33
  "lint": "eslint .",
34
34
  "test": "npm run lint && vitest run",
35
35
  "test:watch": "vitest",
@@ -1 +1 @@
1
- {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAiQnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}
1
+ {"version":3,"file":"locize.d.ts","sourceRoot":"","sources":["../src/locize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAmQnD,eAAO,MAAM,aAAa,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAiD,CAAA;AAC7H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAqD,CAAA;AACrI,eAAO,MAAM,gBAAgB,GAAI,QAAQ,oBAAoB,EAAE,aAAa,GAAG,kBAAoD,CAAA"}