i18next-cli 1.42.8 → 1.42.10
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/dist/cjs/cli.js +1 -1
- package/dist/cjs/config.js +2 -1
- package/dist/cjs/extractor/core/translation-manager.js +2 -8
- package/dist/cjs/linter.js +118 -41
- package/dist/cjs/utils/file-utils.js +0 -38
- package/dist/esm/cli.js +1 -1
- package/dist/esm/config.js +2 -1
- package/dist/esm/extractor/core/translation-manager.js +4 -10
- package/dist/esm/linter.js +119 -42
- package/dist/esm/utils/file-utils.js +1 -38
- package/package.json +6 -6
- package/types/config.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/linter.d.ts.map +1 -1
- package/types/utils/file-utils.d.ts +0 -16
- package/types/utils/file-utils.d.ts.map +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ const program = new commander.Command();
|
|
|
28
28
|
program
|
|
29
29
|
.name('i18next-cli')
|
|
30
30
|
.description('A unified, high-performance i18next CLI.')
|
|
31
|
-
.version('1.42.
|
|
31
|
+
.version('1.42.10'); // This string is replaced with the actual version at build time by rollup
|
|
32
32
|
// new: global config override option
|
|
33
33
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
34
34
|
program
|
package/dist/cjs/config.js
CHANGED
|
@@ -114,7 +114,8 @@ async function loadConfig(configPath, logger$1 = new logger.ConsoleLogger()) {
|
|
|
114
114
|
catch (error) {
|
|
115
115
|
logger$1.error(`Error loading configuration from ${configPathFound}`);
|
|
116
116
|
logger$1.error(error);
|
|
117
|
-
|
|
117
|
+
// Returning null causes ensureConfig to prompt "would you like to create one?"
|
|
118
|
+
throw new Error(`Error loading configuration from ${configPathFound}: ${error?.message || error?.name}`);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
/**
|
|
@@ -826,19 +826,13 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
826
826
|
}
|
|
827
827
|
else {
|
|
828
828
|
// Find all namespaces that exist on disk for this locale.
|
|
829
|
-
// Use '**' so the glob crosses directory boundaries — namespaces
|
|
830
|
-
// can contain '/' and span multiple directory levels.
|
|
831
829
|
const namespacesToProcess = new Set(keysByNS.keys());
|
|
832
|
-
const existingNsPattern = fileUtils.getOutputPath(config.extract.output, locale, '
|
|
830
|
+
const existingNsPattern = fileUtils.getOutputPath(config.extract.output, locale, '*');
|
|
833
831
|
// Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
|
|
834
832
|
const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/');
|
|
835
833
|
const existingNsFiles = await glob.glob(existingNsGlobPattern, { ignore: userIgnore });
|
|
836
834
|
for (const file of existingNsFiles) {
|
|
837
|
-
|
|
838
|
-
// by matching it against the output template.
|
|
839
|
-
const ns = typeof config.extract.output === 'string'
|
|
840
|
-
? fileUtils.extractNamespaceFromPath(config.extract.output, locale, file)
|
|
841
|
-
: undefined;
|
|
835
|
+
const ns = node_path.basename(file, node_path.extname(file));
|
|
842
836
|
if (ns) {
|
|
843
837
|
namespacesToProcess.add(ns);
|
|
844
838
|
}
|
package/dist/cjs/linter.js
CHANGED
|
@@ -9,6 +9,67 @@ var node_util = require('node:util');
|
|
|
9
9
|
var logger = require('./utils/logger.js');
|
|
10
10
|
var wrapOra = require('./utils/wrap-ora.js');
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Loads all translation values from the primary locale's JSON files and returns
|
|
14
|
+
* a flat Map of key -> translated string. Used by the interpolation linter so it
|
|
15
|
+
* can resolve a lookup key (e.g. "ABC") to its actual value ("hello {{name}}")
|
|
16
|
+
* and check interpolation parameters against the real string, not the key.
|
|
17
|
+
*/
|
|
18
|
+
async function loadPrimaryTranslationValues(config) {
|
|
19
|
+
const result = new Map();
|
|
20
|
+
const output = config.extract?.output;
|
|
21
|
+
if (!output || typeof output !== 'string')
|
|
22
|
+
return result;
|
|
23
|
+
const primaryLang = config.extract?.primaryLanguage ?? config.locales?.[0] ?? 'en';
|
|
24
|
+
// Candidate paths: substitute language + try common namespace names (including no namespace).
|
|
25
|
+
// This covers patterns like:
|
|
26
|
+
// locales/{{language}}.json -> no namespace token
|
|
27
|
+
// locales/{{language}}/{{namespace}}.json -> substitute 'translation' (i18next default)
|
|
28
|
+
// Deliberately uses readFile directly (not glob) so it works correctly in test
|
|
29
|
+
// environments that mock fs/promises via memfs.
|
|
30
|
+
const candidatePaths = [];
|
|
31
|
+
if (!output.includes('{{namespace}}')) {
|
|
32
|
+
candidatePaths.push(output.replace(/\{\{language\}\}/g, primaryLang));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
for (const ns of ['translation', 'common', 'default']) {
|
|
36
|
+
candidatePaths.push(output
|
|
37
|
+
.replace(/\{\{language\}\}/g, primaryLang)
|
|
38
|
+
.replace(/\{\{namespace\}\}/g, ns));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// For each candidate, try: (1) the path as-is resolved from cwd, and
|
|
42
|
+
// (2) an absolute path with a leading '/' — the latter ensures we hit
|
|
43
|
+
// memfs in test environments where vol.fromJSON uses absolute paths like
|
|
44
|
+
// '/locales/en.json' but the output template is relative ('locales/...').
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const resolvedPaths = [];
|
|
47
|
+
for (const p of candidatePaths) {
|
|
48
|
+
const abs = node_path.resolve(p);
|
|
49
|
+
const leadingSlash = '/' + p.replace(/^\//, '');
|
|
50
|
+
for (const candidate of [abs, leadingSlash]) {
|
|
51
|
+
if (!seen.has(candidate)) {
|
|
52
|
+
seen.add(candidate);
|
|
53
|
+
resolvedPaths.push(candidate);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const filePath of resolvedPaths) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await promises.readFile(filePath, 'utf-8');
|
|
60
|
+
const json = JSON.parse(raw);
|
|
61
|
+
for (const [k, v] of Object.entries(json)) {
|
|
62
|
+
if (typeof v === 'string')
|
|
63
|
+
result.set(k, v);
|
|
64
|
+
}
|
|
65
|
+
break; // stop after first successful read
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// file doesn't exist or is malformed — try next candidate
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
12
73
|
// Helper to extract interpolation keys from a translation string
|
|
13
74
|
function extractInterpolationKeys(str, config) {
|
|
14
75
|
const prefix = config.extract.interpolationPrefix ?? '{{';
|
|
@@ -40,12 +101,17 @@ function isI18nextOptionKey(key) {
|
|
|
40
101
|
return false;
|
|
41
102
|
}
|
|
42
103
|
// Helper to lint interpolation parameter errors in t() calls
|
|
43
|
-
function lintInterpolationParams(ast, code, config) {
|
|
104
|
+
function lintInterpolationParams(ast, code, config, translationValues) {
|
|
44
105
|
const issues = [];
|
|
45
106
|
// Only run if enabled (default true)
|
|
46
107
|
const enabled = config.lint?.checkInterpolationParams !== false;
|
|
47
108
|
if (!enabled)
|
|
48
109
|
return issues;
|
|
110
|
+
// Helper for line number
|
|
111
|
+
const getLineNumber = (pos) => {
|
|
112
|
+
return code.substring(0, pos).split('\n').length;
|
|
113
|
+
};
|
|
114
|
+
const callSites = [];
|
|
49
115
|
// Traverse AST for CallExpressions matching t() or i18n.t()
|
|
50
116
|
function walk(node, ancestors) {
|
|
51
117
|
if (!node || typeof node !== 'object')
|
|
@@ -103,10 +169,9 @@ function lintInterpolationParams(ast, code, config) {
|
|
|
103
169
|
const arg1raw = node.arguments?.[1];
|
|
104
170
|
const arg0 = arg0raw?.expression ?? arg0raw;
|
|
105
171
|
const arg1 = arg1raw?.expression ?? arg1raw;
|
|
106
|
-
// Only check interpolation params if the first argument is a string literal (translation string)
|
|
172
|
+
// Only check interpolation params if the first argument is a string literal (translation key or string)
|
|
107
173
|
if (arg0?.type === 'StringLiteral') {
|
|
108
|
-
const
|
|
109
|
-
const keys = extractInterpolationKeys(str, config);
|
|
174
|
+
const keyOrStr = arg0.value;
|
|
110
175
|
let paramKeys = [];
|
|
111
176
|
if (arg1?.type === 'ObjectExpression') {
|
|
112
177
|
paramKeys = arg1.properties
|
|
@@ -129,47 +194,56 @@ function lintInterpolationParams(ast, code, config) {
|
|
|
129
194
|
}
|
|
130
195
|
if (!Array.isArray(paramKeys))
|
|
131
196
|
paramKeys = [];
|
|
132
|
-
//
|
|
197
|
+
// Resolve the actual translation string to check against:
|
|
198
|
+
// fix — if the first arg is a lookup key (no interpolation markers of its own)
|
|
199
|
+
// and we have a loaded translation map, use the translated value instead.
|
|
200
|
+
const resolvedStr = translationValues?.get(keyOrStr) ?? keyOrStr;
|
|
133
201
|
const searchText = arg0.raw ?? `"${arg0.value}"`;
|
|
134
|
-
|
|
135
|
-
const issueLineNumber = position > -1 ? getLineNumber(position) : 1;
|
|
136
|
-
// Only check for unused parameters if there is at least one interpolation key in the string
|
|
137
|
-
if (keys.length > 0) {
|
|
138
|
-
// i18next supports nested object access via dot notation (e.g. {{author.name}} with { author }).
|
|
139
|
-
// For each interpolation key, check if the root (part before the first dot) matches a provided param.
|
|
140
|
-
for (const k of keys) {
|
|
141
|
-
const root = k.split('.')[0];
|
|
142
|
-
if (!paramKeys.includes(k) && !paramKeys.includes(root)) {
|
|
143
|
-
issues.push({
|
|
144
|
-
text: `Interpolation parameter "${k}" was not provided`,
|
|
145
|
-
line: issueLineNumber,
|
|
146
|
-
type: 'interpolation',
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// For each provided param, check if it is used either directly or as the root of a dotted key.
|
|
151
|
-
// Skip known i18next t() option keys that are not interpolation parameters.
|
|
152
|
-
for (const pk of paramKeys) {
|
|
153
|
-
if (isI18nextOptionKey(pk))
|
|
154
|
-
continue;
|
|
155
|
-
const isUsed = keys.some(k => k === pk || k.split('.')[0] === pk);
|
|
156
|
-
if (!isUsed) {
|
|
157
|
-
issues.push({
|
|
158
|
-
text: `Parameter "${pk}" is not used in translation string`,
|
|
159
|
-
line: issueLineNumber,
|
|
160
|
-
type: 'interpolation',
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
202
|
+
callSites.push({ translationStr: resolvedStr, searchText, paramKeys });
|
|
165
203
|
}
|
|
166
204
|
}
|
|
167
205
|
}
|
|
168
|
-
// Helper for line number
|
|
169
|
-
const getLineNumber = (pos) => {
|
|
170
|
-
return code.substring(0, pos).split('\n').length;
|
|
171
|
-
};
|
|
172
206
|
walk(ast, []);
|
|
207
|
+
// 187 fix — process call sites in source order, advancing lastSearchIndex so that
|
|
208
|
+
// each occurrence of the same string literal finds its own line, not always the first.
|
|
209
|
+
let lastSearchIndex = 0;
|
|
210
|
+
for (const { translationStr, searchText, paramKeys } of callSites) {
|
|
211
|
+
const position = code.indexOf(searchText, lastSearchIndex);
|
|
212
|
+
if (position === -1)
|
|
213
|
+
continue;
|
|
214
|
+
lastSearchIndex = position + searchText.length;
|
|
215
|
+
const issueLineNumber = getLineNumber(position);
|
|
216
|
+
const keys = extractInterpolationKeys(translationStr, config);
|
|
217
|
+
// Only check for unused parameters if there is at least one interpolation key in the string
|
|
218
|
+
if (keys.length > 0) {
|
|
219
|
+
// i18next supports nested object access via dot notation (e.g. {{author.name}} with { author }).
|
|
220
|
+
// For each interpolation key, check if the root (part before the first dot) matches a provided param.
|
|
221
|
+
for (const k of keys) {
|
|
222
|
+
const root = k.split('.')[0];
|
|
223
|
+
if (!paramKeys.includes(k) && !paramKeys.includes(root)) {
|
|
224
|
+
issues.push({
|
|
225
|
+
text: `Interpolation parameter "${k}" was not provided`,
|
|
226
|
+
line: issueLineNumber,
|
|
227
|
+
type: 'interpolation',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// For each provided param, check if it is used either directly or as the root of a dotted key.
|
|
232
|
+
// Skip known i18next t() option keys that are not interpolation parameters.
|
|
233
|
+
for (const pk of paramKeys) {
|
|
234
|
+
if (isI18nextOptionKey(pk))
|
|
235
|
+
continue;
|
|
236
|
+
const isUsed = keys.some(k => k === pk || k.split('.')[0] === pk);
|
|
237
|
+
if (!isUsed) {
|
|
238
|
+
issues.push({
|
|
239
|
+
text: `Parameter "${pk}" is not used in translation string`,
|
|
240
|
+
line: issueLineNumber,
|
|
241
|
+
type: 'interpolation',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
173
247
|
return issues;
|
|
174
248
|
}
|
|
175
249
|
const recommendedAcceptedTags = [
|
|
@@ -211,6 +285,9 @@ class Linter extends node_events.EventEmitter {
|
|
|
211
285
|
ignore: [...defaultIgnore, ...extractIgnore, ...lintIgnore]
|
|
212
286
|
});
|
|
213
287
|
this.emit('progress', { message: `Analyzing ${sourceFiles.length} source files...` });
|
|
288
|
+
// Load translation values once so the interpolation linter can resolve lookup keys
|
|
289
|
+
// to their translated strings (fixes: key != value interpolation not detected)
|
|
290
|
+
const translationValues = await loadPrimaryTranslationValues(config);
|
|
214
291
|
let totalIssues = 0;
|
|
215
292
|
const issuesByFile = new Map();
|
|
216
293
|
for (const file of sourceFiles) {
|
|
@@ -271,7 +348,7 @@ class Linter extends node_events.EventEmitter {
|
|
|
271
348
|
// Collect hardcoded string issues
|
|
272
349
|
const hardcodedStrings = findHardcodedStrings(ast, code, config);
|
|
273
350
|
// Collect interpolation parameter issues
|
|
274
|
-
const interpolationIssues = lintInterpolationParams(ast, code, config);
|
|
351
|
+
const interpolationIssues = lintInterpolationParams(ast, code, config, translationValues);
|
|
275
352
|
const allIssues = [...hardcodedStrings, ...interpolationIssues];
|
|
276
353
|
if (allIssues.length > 0) {
|
|
277
354
|
totalIssues += allIssues.length;
|
|
@@ -45,43 +45,6 @@ function getOutputPath(outputTemplate, language, namespace) {
|
|
|
45
45
|
out = out.replace(/\/\/+/g, '/');
|
|
46
46
|
return node_path.normalize(out);
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Extracts the namespace value from a concrete file path by matching it against
|
|
50
|
-
* the output template.
|
|
51
|
-
*
|
|
52
|
-
* Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
|
|
53
|
-
* path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
|
|
54
|
-
*
|
|
55
|
-
* This handles multi-segment namespaces (namespaces containing `/`) which
|
|
56
|
-
* `basename()` cannot recover.
|
|
57
|
-
*
|
|
58
|
-
* @param outputTemplate - The output path template string (must contain `{{namespace}}`)
|
|
59
|
-
* @param language - The language value used when the file was generated
|
|
60
|
-
* @param filePath - The concrete file path to extract the namespace from
|
|
61
|
-
* @returns The namespace string, or `undefined` if the path doesn't match the template
|
|
62
|
-
*/
|
|
63
|
-
function extractNamespaceFromPath(outputTemplate, language, filePath) {
|
|
64
|
-
// Build a regex from the template by escaping everything except the placeholders.
|
|
65
|
-
// Replace {{language}}/{{lng}} with the literal language value and
|
|
66
|
-
// {{namespace}} with a named capture group that matches one or more path segments.
|
|
67
|
-
const pattern = outputTemplate
|
|
68
|
-
// Normalise to forward slashes for matching
|
|
69
|
-
.replace(/\\/g, '/');
|
|
70
|
-
// Escape regex-special characters (but keep our placeholders intact first)
|
|
71
|
-
const nsPlaceholder = '{{namespace}}';
|
|
72
|
-
const parts = pattern.split(nsPlaceholder);
|
|
73
|
-
// Escape each part individually then rejoin with the capture group
|
|
74
|
-
const escaped = parts.map(p => p
|
|
75
|
-
.replace(/\{\{language\}\}|\{\{lng\}\}/g, () => escapeForRegex(language))
|
|
76
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
77
|
-
// Don't anchor the start — the glob may return absolute or prefixed paths.
|
|
78
|
-
// Anchor only the end so that the namespace capture is unambiguous.
|
|
79
|
-
const regexStr = escaped.join('(.+)') + '$';
|
|
80
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
81
|
-
const m = new RegExp(regexStr).exec(normalized);
|
|
82
|
-
return m?.[1];
|
|
83
|
-
}
|
|
84
|
-
const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
48
|
/**
|
|
86
49
|
* Dynamically loads a translation file, supporting .json, .js, and .ts formats.
|
|
87
50
|
* @param filePath - The path to the translation file.
|
|
@@ -193,7 +156,6 @@ function inferFormatFromPath(filePath, defaultFormat = 'json') {
|
|
|
193
156
|
return defaultFormat || 'json';
|
|
194
157
|
}
|
|
195
158
|
|
|
196
|
-
exports.extractNamespaceFromPath = extractNamespaceFromPath;
|
|
197
159
|
exports.getOutputPath = getOutputPath;
|
|
198
160
|
exports.inferFormatFromPath = inferFormatFromPath;
|
|
199
161
|
exports.loadRawJson5Content = loadRawJson5Content;
|
package/dist/esm/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const program = new Command();
|
|
|
26
26
|
program
|
|
27
27
|
.name('i18next-cli')
|
|
28
28
|
.description('A unified, high-performance i18next CLI.')
|
|
29
|
-
.version('1.42.
|
|
29
|
+
.version('1.42.10'); // This string is replaced with the actual version at build time by rollup
|
|
30
30
|
// new: global config override option
|
|
31
31
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
32
32
|
program
|
package/dist/esm/config.js
CHANGED
|
@@ -112,7 +112,8 @@ async function loadConfig(configPath, logger = new ConsoleLogger()) {
|
|
|
112
112
|
catch (error) {
|
|
113
113
|
logger.error(`Error loading configuration from ${configPathFound}`);
|
|
114
114
|
logger.error(error);
|
|
115
|
-
|
|
115
|
+
// Returning null causes ensureConfig to prompt "would you like to create one?"
|
|
116
|
+
throw new Error(`Error loading configuration from ${configPathFound}: ${error?.message || error?.name}`);
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { resolve, basename, extname } from 'node:path';
|
|
2
2
|
import { glob } from 'glob';
|
|
3
3
|
import { getNestedKeys, getNestedValue, setNestedValue } from '../../utils/nested-object.js';
|
|
4
|
-
import { getOutputPath, loadTranslationFile
|
|
4
|
+
import { getOutputPath, loadTranslationFile } from '../../utils/file-utils.js';
|
|
5
5
|
import { resolveDefaultValue } from '../../utils/default-value.js';
|
|
6
6
|
|
|
7
7
|
// used for natural language check
|
|
@@ -824,19 +824,13 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
824
824
|
}
|
|
825
825
|
else {
|
|
826
826
|
// Find all namespaces that exist on disk for this locale.
|
|
827
|
-
// Use '**' so the glob crosses directory boundaries — namespaces
|
|
828
|
-
// can contain '/' and span multiple directory levels.
|
|
829
827
|
const namespacesToProcess = new Set(keysByNS.keys());
|
|
830
|
-
const existingNsPattern = getOutputPath(config.extract.output, locale, '
|
|
828
|
+
const existingNsPattern = getOutputPath(config.extract.output, locale, '*');
|
|
831
829
|
// Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
|
|
832
830
|
const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/');
|
|
833
831
|
const existingNsFiles = await glob(existingNsGlobPattern, { ignore: userIgnore });
|
|
834
832
|
for (const file of existingNsFiles) {
|
|
835
|
-
|
|
836
|
-
// by matching it against the output template.
|
|
837
|
-
const ns = typeof config.extract.output === 'string'
|
|
838
|
-
? extractNamespaceFromPath(config.extract.output, locale, file)
|
|
839
|
-
: undefined;
|
|
833
|
+
const ns = basename(file, extname(file));
|
|
840
834
|
if (ns) {
|
|
841
835
|
namespacesToProcess.add(ns);
|
|
842
836
|
}
|
package/dist/esm/linter.js
CHANGED
|
@@ -1,12 +1,73 @@
|
|
|
1
1
|
import { glob } from 'glob';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { parse } from '@swc/core';
|
|
4
|
-
import { extname } from 'node:path';
|
|
4
|
+
import { extname, resolve } from 'node:path';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
6
|
import { styleText } from 'node:util';
|
|
7
7
|
import { ConsoleLogger } from './utils/logger.js';
|
|
8
8
|
import { createSpinnerLike } from './utils/wrap-ora.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Loads all translation values from the primary locale's JSON files and returns
|
|
12
|
+
* a flat Map of key -> translated string. Used by the interpolation linter so it
|
|
13
|
+
* can resolve a lookup key (e.g. "ABC") to its actual value ("hello {{name}}")
|
|
14
|
+
* and check interpolation parameters against the real string, not the key.
|
|
15
|
+
*/
|
|
16
|
+
async function loadPrimaryTranslationValues(config) {
|
|
17
|
+
const result = new Map();
|
|
18
|
+
const output = config.extract?.output;
|
|
19
|
+
if (!output || typeof output !== 'string')
|
|
20
|
+
return result;
|
|
21
|
+
const primaryLang = config.extract?.primaryLanguage ?? config.locales?.[0] ?? 'en';
|
|
22
|
+
// Candidate paths: substitute language + try common namespace names (including no namespace).
|
|
23
|
+
// This covers patterns like:
|
|
24
|
+
// locales/{{language}}.json -> no namespace token
|
|
25
|
+
// locales/{{language}}/{{namespace}}.json -> substitute 'translation' (i18next default)
|
|
26
|
+
// Deliberately uses readFile directly (not glob) so it works correctly in test
|
|
27
|
+
// environments that mock fs/promises via memfs.
|
|
28
|
+
const candidatePaths = [];
|
|
29
|
+
if (!output.includes('{{namespace}}')) {
|
|
30
|
+
candidatePaths.push(output.replace(/\{\{language\}\}/g, primaryLang));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const ns of ['translation', 'common', 'default']) {
|
|
34
|
+
candidatePaths.push(output
|
|
35
|
+
.replace(/\{\{language\}\}/g, primaryLang)
|
|
36
|
+
.replace(/\{\{namespace\}\}/g, ns));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// For each candidate, try: (1) the path as-is resolved from cwd, and
|
|
40
|
+
// (2) an absolute path with a leading '/' — the latter ensures we hit
|
|
41
|
+
// memfs in test environments where vol.fromJSON uses absolute paths like
|
|
42
|
+
// '/locales/en.json' but the output template is relative ('locales/...').
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const resolvedPaths = [];
|
|
45
|
+
for (const p of candidatePaths) {
|
|
46
|
+
const abs = resolve(p);
|
|
47
|
+
const leadingSlash = '/' + p.replace(/^\//, '');
|
|
48
|
+
for (const candidate of [abs, leadingSlash]) {
|
|
49
|
+
if (!seen.has(candidate)) {
|
|
50
|
+
seen.add(candidate);
|
|
51
|
+
resolvedPaths.push(candidate);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const filePath of resolvedPaths) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
58
|
+
const json = JSON.parse(raw);
|
|
59
|
+
for (const [k, v] of Object.entries(json)) {
|
|
60
|
+
if (typeof v === 'string')
|
|
61
|
+
result.set(k, v);
|
|
62
|
+
}
|
|
63
|
+
break; // stop after first successful read
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// file doesn't exist or is malformed — try next candidate
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
10
71
|
// Helper to extract interpolation keys from a translation string
|
|
11
72
|
function extractInterpolationKeys(str, config) {
|
|
12
73
|
const prefix = config.extract.interpolationPrefix ?? '{{';
|
|
@@ -38,12 +99,17 @@ function isI18nextOptionKey(key) {
|
|
|
38
99
|
return false;
|
|
39
100
|
}
|
|
40
101
|
// Helper to lint interpolation parameter errors in t() calls
|
|
41
|
-
function lintInterpolationParams(ast, code, config) {
|
|
102
|
+
function lintInterpolationParams(ast, code, config, translationValues) {
|
|
42
103
|
const issues = [];
|
|
43
104
|
// Only run if enabled (default true)
|
|
44
105
|
const enabled = config.lint?.checkInterpolationParams !== false;
|
|
45
106
|
if (!enabled)
|
|
46
107
|
return issues;
|
|
108
|
+
// Helper for line number
|
|
109
|
+
const getLineNumber = (pos) => {
|
|
110
|
+
return code.substring(0, pos).split('\n').length;
|
|
111
|
+
};
|
|
112
|
+
const callSites = [];
|
|
47
113
|
// Traverse AST for CallExpressions matching t() or i18n.t()
|
|
48
114
|
function walk(node, ancestors) {
|
|
49
115
|
if (!node || typeof node !== 'object')
|
|
@@ -101,10 +167,9 @@ function lintInterpolationParams(ast, code, config) {
|
|
|
101
167
|
const arg1raw = node.arguments?.[1];
|
|
102
168
|
const arg0 = arg0raw?.expression ?? arg0raw;
|
|
103
169
|
const arg1 = arg1raw?.expression ?? arg1raw;
|
|
104
|
-
// Only check interpolation params if the first argument is a string literal (translation string)
|
|
170
|
+
// Only check interpolation params if the first argument is a string literal (translation key or string)
|
|
105
171
|
if (arg0?.type === 'StringLiteral') {
|
|
106
|
-
const
|
|
107
|
-
const keys = extractInterpolationKeys(str, config);
|
|
172
|
+
const keyOrStr = arg0.value;
|
|
108
173
|
let paramKeys = [];
|
|
109
174
|
if (arg1?.type === 'ObjectExpression') {
|
|
110
175
|
paramKeys = arg1.properties
|
|
@@ -127,47 +192,56 @@ function lintInterpolationParams(ast, code, config) {
|
|
|
127
192
|
}
|
|
128
193
|
if (!Array.isArray(paramKeys))
|
|
129
194
|
paramKeys = [];
|
|
130
|
-
//
|
|
195
|
+
// Resolve the actual translation string to check against:
|
|
196
|
+
// fix — if the first arg is a lookup key (no interpolation markers of its own)
|
|
197
|
+
// and we have a loaded translation map, use the translated value instead.
|
|
198
|
+
const resolvedStr = translationValues?.get(keyOrStr) ?? keyOrStr;
|
|
131
199
|
const searchText = arg0.raw ?? `"${arg0.value}"`;
|
|
132
|
-
|
|
133
|
-
const issueLineNumber = position > -1 ? getLineNumber(position) : 1;
|
|
134
|
-
// Only check for unused parameters if there is at least one interpolation key in the string
|
|
135
|
-
if (keys.length > 0) {
|
|
136
|
-
// i18next supports nested object access via dot notation (e.g. {{author.name}} with { author }).
|
|
137
|
-
// For each interpolation key, check if the root (part before the first dot) matches a provided param.
|
|
138
|
-
for (const k of keys) {
|
|
139
|
-
const root = k.split('.')[0];
|
|
140
|
-
if (!paramKeys.includes(k) && !paramKeys.includes(root)) {
|
|
141
|
-
issues.push({
|
|
142
|
-
text: `Interpolation parameter "${k}" was not provided`,
|
|
143
|
-
line: issueLineNumber,
|
|
144
|
-
type: 'interpolation',
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// For each provided param, check if it is used either directly or as the root of a dotted key.
|
|
149
|
-
// Skip known i18next t() option keys that are not interpolation parameters.
|
|
150
|
-
for (const pk of paramKeys) {
|
|
151
|
-
if (isI18nextOptionKey(pk))
|
|
152
|
-
continue;
|
|
153
|
-
const isUsed = keys.some(k => k === pk || k.split('.')[0] === pk);
|
|
154
|
-
if (!isUsed) {
|
|
155
|
-
issues.push({
|
|
156
|
-
text: `Parameter "${pk}" is not used in translation string`,
|
|
157
|
-
line: issueLineNumber,
|
|
158
|
-
type: 'interpolation',
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
200
|
+
callSites.push({ translationStr: resolvedStr, searchText, paramKeys });
|
|
163
201
|
}
|
|
164
202
|
}
|
|
165
203
|
}
|
|
166
|
-
// Helper for line number
|
|
167
|
-
const getLineNumber = (pos) => {
|
|
168
|
-
return code.substring(0, pos).split('\n').length;
|
|
169
|
-
};
|
|
170
204
|
walk(ast, []);
|
|
205
|
+
// 187 fix — process call sites in source order, advancing lastSearchIndex so that
|
|
206
|
+
// each occurrence of the same string literal finds its own line, not always the first.
|
|
207
|
+
let lastSearchIndex = 0;
|
|
208
|
+
for (const { translationStr, searchText, paramKeys } of callSites) {
|
|
209
|
+
const position = code.indexOf(searchText, lastSearchIndex);
|
|
210
|
+
if (position === -1)
|
|
211
|
+
continue;
|
|
212
|
+
lastSearchIndex = position + searchText.length;
|
|
213
|
+
const issueLineNumber = getLineNumber(position);
|
|
214
|
+
const keys = extractInterpolationKeys(translationStr, config);
|
|
215
|
+
// Only check for unused parameters if there is at least one interpolation key in the string
|
|
216
|
+
if (keys.length > 0) {
|
|
217
|
+
// i18next supports nested object access via dot notation (e.g. {{author.name}} with { author }).
|
|
218
|
+
// For each interpolation key, check if the root (part before the first dot) matches a provided param.
|
|
219
|
+
for (const k of keys) {
|
|
220
|
+
const root = k.split('.')[0];
|
|
221
|
+
if (!paramKeys.includes(k) && !paramKeys.includes(root)) {
|
|
222
|
+
issues.push({
|
|
223
|
+
text: `Interpolation parameter "${k}" was not provided`,
|
|
224
|
+
line: issueLineNumber,
|
|
225
|
+
type: 'interpolation',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// For each provided param, check if it is used either directly or as the root of a dotted key.
|
|
230
|
+
// Skip known i18next t() option keys that are not interpolation parameters.
|
|
231
|
+
for (const pk of paramKeys) {
|
|
232
|
+
if (isI18nextOptionKey(pk))
|
|
233
|
+
continue;
|
|
234
|
+
const isUsed = keys.some(k => k === pk || k.split('.')[0] === pk);
|
|
235
|
+
if (!isUsed) {
|
|
236
|
+
issues.push({
|
|
237
|
+
text: `Parameter "${pk}" is not used in translation string`,
|
|
238
|
+
line: issueLineNumber,
|
|
239
|
+
type: 'interpolation',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
171
245
|
return issues;
|
|
172
246
|
}
|
|
173
247
|
const recommendedAcceptedTags = [
|
|
@@ -209,6 +283,9 @@ class Linter extends EventEmitter {
|
|
|
209
283
|
ignore: [...defaultIgnore, ...extractIgnore, ...lintIgnore]
|
|
210
284
|
});
|
|
211
285
|
this.emit('progress', { message: `Analyzing ${sourceFiles.length} source files...` });
|
|
286
|
+
// Load translation values once so the interpolation linter can resolve lookup keys
|
|
287
|
+
// to their translated strings (fixes: key != value interpolation not detected)
|
|
288
|
+
const translationValues = await loadPrimaryTranslationValues(config);
|
|
212
289
|
let totalIssues = 0;
|
|
213
290
|
const issuesByFile = new Map();
|
|
214
291
|
for (const file of sourceFiles) {
|
|
@@ -269,7 +346,7 @@ class Linter extends EventEmitter {
|
|
|
269
346
|
// Collect hardcoded string issues
|
|
270
347
|
const hardcodedStrings = findHardcodedStrings(ast, code, config);
|
|
271
348
|
// Collect interpolation parameter issues
|
|
272
|
-
const interpolationIssues = lintInterpolationParams(ast, code, config);
|
|
349
|
+
const interpolationIssues = lintInterpolationParams(ast, code, config, translationValues);
|
|
273
350
|
const allIssues = [...hardcodedStrings, ...interpolationIssues];
|
|
274
351
|
if (allIssues.length > 0) {
|
|
275
352
|
totalIssues += allIssues.length;
|
|
@@ -43,43 +43,6 @@ function getOutputPath(outputTemplate, language, namespace) {
|
|
|
43
43
|
out = out.replace(/\/\/+/g, '/');
|
|
44
44
|
return normalize(out);
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Extracts the namespace value from a concrete file path by matching it against
|
|
48
|
-
* the output template.
|
|
49
|
-
*
|
|
50
|
-
* Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
|
|
51
|
-
* path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
|
|
52
|
-
*
|
|
53
|
-
* This handles multi-segment namespaces (namespaces containing `/`) which
|
|
54
|
-
* `basename()` cannot recover.
|
|
55
|
-
*
|
|
56
|
-
* @param outputTemplate - The output path template string (must contain `{{namespace}}`)
|
|
57
|
-
* @param language - The language value used when the file was generated
|
|
58
|
-
* @param filePath - The concrete file path to extract the namespace from
|
|
59
|
-
* @returns The namespace string, or `undefined` if the path doesn't match the template
|
|
60
|
-
*/
|
|
61
|
-
function extractNamespaceFromPath(outputTemplate, language, filePath) {
|
|
62
|
-
// Build a regex from the template by escaping everything except the placeholders.
|
|
63
|
-
// Replace {{language}}/{{lng}} with the literal language value and
|
|
64
|
-
// {{namespace}} with a named capture group that matches one or more path segments.
|
|
65
|
-
const pattern = outputTemplate
|
|
66
|
-
// Normalise to forward slashes for matching
|
|
67
|
-
.replace(/\\/g, '/');
|
|
68
|
-
// Escape regex-special characters (but keep our placeholders intact first)
|
|
69
|
-
const nsPlaceholder = '{{namespace}}';
|
|
70
|
-
const parts = pattern.split(nsPlaceholder);
|
|
71
|
-
// Escape each part individually then rejoin with the capture group
|
|
72
|
-
const escaped = parts.map(p => p
|
|
73
|
-
.replace(/\{\{language\}\}|\{\{lng\}\}/g, () => escapeForRegex(language))
|
|
74
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
75
|
-
// Don't anchor the start — the glob may return absolute or prefixed paths.
|
|
76
|
-
// Anchor only the end so that the namespace capture is unambiguous.
|
|
77
|
-
const regexStr = escaped.join('(.+)') + '$';
|
|
78
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
79
|
-
const m = new RegExp(regexStr).exec(normalized);
|
|
80
|
-
return m?.[1];
|
|
81
|
-
}
|
|
82
|
-
const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
83
46
|
/**
|
|
84
47
|
* Dynamically loads a translation file, supporting .json, .js, and .ts formats.
|
|
85
48
|
* @param filePath - The path to the translation file.
|
|
@@ -191,4 +154,4 @@ function inferFormatFromPath(filePath, defaultFormat = 'json') {
|
|
|
191
154
|
return defaultFormat || 'json';
|
|
192
155
|
}
|
|
193
156
|
|
|
194
|
-
export {
|
|
157
|
+
export { getOutputPath, inferFormatFromPath, loadRawJson5Content, loadTranslationFile, serializeTranslationFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18next-cli",
|
|
3
|
-
"version": "1.42.
|
|
3
|
+
"version": "1.42.10",
|
|
4
4
|
"description": "A unified, high-performance i18next CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"@rollup/plugin-replace": "6.0.3",
|
|
56
56
|
"@rollup/plugin-terser": "0.4.4",
|
|
57
57
|
"@types/inquirer": "9.0.9",
|
|
58
|
-
"@types/node": "25.
|
|
59
|
-
"@types/react": "19.2.
|
|
58
|
+
"@types/node": "25.3.0",
|
|
59
|
+
"@types/react": "19.2.14",
|
|
60
60
|
"@vitest/coverage-v8": "4.0.18",
|
|
61
61
|
"eslint": "9.39.2",
|
|
62
62
|
"eslint-plugin-import": "2.32.0",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"chokidar": "5.0.0",
|
|
75
75
|
"commander": "14.0.3",
|
|
76
76
|
"execa": "9.6.1",
|
|
77
|
-
"glob": "13.0.
|
|
77
|
+
"glob": "13.0.6",
|
|
78
78
|
"i18next-resources-for-ts": "2.0.0",
|
|
79
|
-
"inquirer": "13.2.
|
|
79
|
+
"inquirer": "13.2.5",
|
|
80
80
|
"jiti": "2.6.1",
|
|
81
81
|
"jsonc-parser": "3.3.1",
|
|
82
|
-
"minimatch": "10.
|
|
82
|
+
"minimatch": "10.2.2",
|
|
83
83
|
"ora": "9.3.0",
|
|
84
84
|
"react": "^19.2.4",
|
|
85
85
|
"react-i18next": "^16.5.4"
|
package/types/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAc3D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAE,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAEhF;AAgCD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAc3D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAE,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAEhF;AAgCD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA+CjI;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA8B5H;AAyBD;;;GAGG;AACH,wBAAsB,kBAAkB,IAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyB3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAgyBnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA8J9B"}
|
package/types/linter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAwP3D,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE;QAAC;YACT,OAAO,EAAE,MAAM,CAAC;SACjB;KAAC,CAAC;IACH,IAAI,EAAE;QAAC;YACL,OAAO,EAAE,OAAO,CAAC;YACjB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;SAC1C;KAAC,CAAC;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CACvB,CAAA;AAED,eAAO,MAAM,uBAAuB,UAET,CAAA;AAC3B,eAAO,MAAM,6BAA6B,UAAgN,CAAA;AAK1P,qBAAa,MAAO,SAAQ,YAAY,CAAC,cAAc,CAAC;IACtD,OAAO,CAAC,MAAM,CAAsB;gBAEvB,MAAM,EAAE,oBAAoB;IAKzC,SAAS,CAAE,KAAK,EAAE,OAAO;IAanB,GAAG;;;;;;;CAgGV;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB;;;;;;GAE5D;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,iBAkCnD;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,+GAA+G;IAC/G,IAAI,CAAC,EAAE,WAAW,GAAG,eAAe,CAAC;CACtC"}
|
|
@@ -48,22 +48,6 @@ export declare function writeFileAsync(filePath: string, data: string): Promise<
|
|
|
48
48
|
* - Normalizes duplicate slashes and returns a platform-correct path.
|
|
49
49
|
*/
|
|
50
50
|
export declare function getOutputPath(outputTemplate: string | ((language: string, namespace?: string) => string) | undefined, language: string, namespace?: string): string;
|
|
51
|
-
/**
|
|
52
|
-
* Extracts the namespace value from a concrete file path by matching it against
|
|
53
|
-
* the output template.
|
|
54
|
-
*
|
|
55
|
-
* Given a template like `src/{{namespace}}/locales/{{language}}.json` and a file
|
|
56
|
-
* path like `src/widgets/component/locales/en.json`, this returns `widgets/component`.
|
|
57
|
-
*
|
|
58
|
-
* This handles multi-segment namespaces (namespaces containing `/`) which
|
|
59
|
-
* `basename()` cannot recover.
|
|
60
|
-
*
|
|
61
|
-
* @param outputTemplate - The output path template string (must contain `{{namespace}}`)
|
|
62
|
-
* @param language - The language value used when the file was generated
|
|
63
|
-
* @param filePath - The concrete file path to extract the namespace from
|
|
64
|
-
* @returns The namespace string, or `undefined` if the path doesn't match the template
|
|
65
|
-
*/
|
|
66
|
-
export declare function extractNamespaceFromPath(outputTemplate: string, language: string, filePath: string): string | undefined;
|
|
67
51
|
/**
|
|
68
52
|
* Dynamically loads a translation file, supporting .json, .js, and .ts formats.
|
|
69
53
|
* @param filePath - The path to the translation file.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAKpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EACvF,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CA8BR;AAED
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAKpD;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EACvF,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CA8BR;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAwChG;AAGD,wBAAsB,mBAAmB,CAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQnF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,EAChE,WAAW,GAAE,MAAM,GAAG,MAAU,EAChC,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CA6BR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,GAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAU,GACtE,WAAW,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC,CAO9D"}
|