i18next-cli 1.34.0 → 1.35.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 (66) 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 +372 -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 +942 -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 +370 -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 +940 -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/extractor/core/ast-visitors.d.ts.map +1 -1
  64. package/types/extractor/parsers/call-expression-handler.d.ts +3 -2
  65. package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
  66. package/types/locize.d.ts.map +1 -1
@@ -1 +1,375 @@
1
- "use strict";function e(e,t,n,s,r,a=!1){try{const o=a?"ordinal":"cardinal",l=new Set;for(const e of r.locales)try{const t=new Intl.PluralRules(e,{type:o});t.resolvedOptions().pluralCategories.forEach(e=>l.add(e))}catch(e){const t=new Intl.PluralRules("en",{type:o});t.resolvedOptions().pluralCategories.forEach(e=>l.add(e))}const c=Array.from(l).sort(),u=r.extract.pluralSeparator??"_";if(1===c.length&&"other"===c[0])return void s.addKey({key:e,ns:n,defaultValue:t,hasCount:!0});for(const r of c){const o=a?`${e}${u}ordinal${u}${r}`:`${e}${u}${r}`;s.addKey({key:o,ns:n,defaultValue:t,hasCount:!0,isOrdinal:a})}}catch(r){s.addKey({key:e,ns:n,defaultValue:t})}}function t(e,t,n,s,r,a,o=!1){try{const l=o?"ordinal":"cardinal",c=new Set;for(const e of a.locales)try{const t=new Intl.PluralRules(e,{type:l});t.resolvedOptions().pluralCategories.forEach(e=>c.add(e))}catch(e){const t=new Intl.PluralRules(a.extract.primaryLanguage||"en",{type:l});t.resolvedOptions().pluralCategories.forEach(e=>c.add(e))}const u=Array.from(c).sort(),i=a.extract.pluralSeparator??"_";for(const a of u){const l=o?`${e}_${s}${i}ordinal${i}${a}`:`${e}_${s}${i}${a}`;r.addKey({key:l,ns:n,defaultValue:t,hasCount:!0,isOrdinal:o})}}catch(a){r.addKey({key:`${e}_${s}`,ns:n,defaultValue:t})}}function n(e){const t=/^\s*,\s*(['"])(.*?)\1/.exec(e);if(t)return t[2];const n=/^\s*,\s*\{[^}]*defaultValue\s*:\s*(['"])(.*?)\1/.exec(e);return n?n[2]:void 0}function s(e){const t=/^\s*,\s*\{[^}]*ns\s*:\s*(['"])(.*?)\1/.exec(e);if(t)return t[2]}function r(e){const t=/^\s*,\s*\{[^}]*context\s*:\s*(['"])(.*?)\1/.exec(e);if(t)return t[2]}function a(e){const t=/^\s*,\s*\{[^}]*count\s*:\s*(\d+)/.exec(e);if(t)return parseInt(t[1],10)}function o(e){const t=/^\s*,\s*\{[^}]*ordinal\s*:\s*(true|false)/.exec(e);if(t)return"true"===t[1]}function l(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}exports.extractKeysFromComments=function(c,u,i,f){const d=new RegExp("\\bt\\s*\\(\\s*(['\"`])(.*?)\\1","g"),y=i.extract.preservePatterns||[],p=y.map(l),$=i.extract.nsSeparator??":",x=(e,t)=>{if(p.some(t=>t.test(e)))return!0;for(const e of y)if("string"==typeof e&&e.endsWith(`${$}*`)){const n="string"==typeof $&&$.length>0?e.slice(0,-($.length+1)):e.slice(0,-1);if("*"===n||t&&n===t)return!0}return!1},h=function(e){const t=[],n=new Set,s=/\/\/(.*)|\/\*([\s\S]*?)\*\//g;let r;for(;null!==(r=s.exec(e));){const e=(r[1]??r[2]).trim();e&&!n.has(e)&&(n.add(e),t.push(e))}return t}(c);for(const l of h){let c;for(;null!==(c=d.exec(l));){let d,y=c[2];if(!y||""===y.trim())continue;const p=l.slice(c.index+c[0].length),$=n(p),h=r(p),g=a(p),m=o(p);let K=!1;const V=i.extract.pluralSeparator??"_";if(y.endsWith(`${V}ordinal`)){if(K=!0,y=y.slice(0,-(V.length+7)),!y||""===y.trim())continue;if(x(y,d))continue}const k=!0===m||K;d=s(p);const S=i.extract.nsSeparator??":";if(!d&&S&&y.includes(S)){const e=y.split(S);if(d=e.shift(),y=e.join(S),!y||""===y.trim())continue;if(x(y,d))continue}if(!d&&f){const e=f("t");e?.defaultNs&&(d=e.defaultNs)}if(!x(y,d))if(d||(d=i.extract.defaultNS),i.extract.disablePlurals)h?u.addKey({key:`${y}_${h}`,ns:d,defaultValue:$??y}):u.addKey({key:y,ns:d,defaultValue:$??y});else if(h&&g){t(y,$??y,d,h,u,i,k);!1!==i.extract?.generateBasePluralForms&&e(y,$??y,d,u,i,k)}else h?(u.addKey({key:y,ns:d,defaultValue:$??y}),u.addKey({key:`${y}_${h}`,ns:d,defaultValue:$??y})):g?e(y,$??y,d,u,i,k):u.addKey({key:y,ns:d,defaultValue:$??y})}}};
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extracts translation keys from comments in source code using regex patterns.
5
+ * Supports extraction from single-line (//) and multi-line comments.
6
+ *
7
+ * @param code - The source code to analyze
8
+ * @param pluginContext - Context object with helper methods to add found keys
9
+ * @param config - Configuration object containing extraction settings
10
+ * @param scopeResolver - Function to resolve scope information for variables (optional)
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const code = `
15
+ * // t('user.name', 'User Name')
16
+ * /* t('app.title', { defaultValue: 'My App', ns: 'common' }) *\/
17
+ * `
18
+ *
19
+ * const context = createPluginContext(allKeys)
20
+ * extractKeysFromComments(code, context, config, scopeResolver)
21
+ * // Extracts: user.name and app.title with their respective settings
22
+ * ```
23
+ */
24
+ function extractKeysFromComments(code, pluginContext, config, scopeResolver) {
25
+ // Hardcode the function name to 't' to prevent parsing other functions like 'test()'.
26
+ const functionNameToFind = 't';
27
+ // Use a reliable word boundary (\b) to match 't(...)' but not 'http.get(...)'.
28
+ const keyRegex = new RegExp(`\\b${functionNameToFind}\\s*\\(\\s*(['"\`])(.*?)\\1`, 'g');
29
+ // Prepare preservePatterns for filtering
30
+ const rawPreservePatterns = config.extract.preservePatterns || [];
31
+ const preservePatterns = rawPreservePatterns.map(globToRegex);
32
+ const nsSeparator = config.extract.nsSeparator ?? ':';
33
+ const matchesPreserve = (key, ns) => {
34
+ // 1) regex-style matches (existing behavior)
35
+ if (preservePatterns.some(re => re.test(key)))
36
+ return true;
37
+ // 2) namespace:* style patterns => preserve entire namespace
38
+ for (const rp of rawPreservePatterns) {
39
+ if (typeof rp !== 'string')
40
+ continue;
41
+ if (rp.endsWith(`${nsSeparator}*`)) {
42
+ const nsPrefix = (typeof nsSeparator === 'string' && nsSeparator.length > 0)
43
+ ? rp.slice(0, -(nsSeparator.length + 1))
44
+ : rp.slice(0, -1);
45
+ // support '*' as a wildcard namespace
46
+ if (nsPrefix === '*' || (ns && nsPrefix === ns))
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ };
52
+ const commentTexts = collectCommentTexts(code);
53
+ for (const text of commentTexts) {
54
+ let match;
55
+ while ((match = keyRegex.exec(text)) !== null) {
56
+ let key = match[2];
57
+ // Validate that the key is not empty or whitespace-only
58
+ if (!key || key.trim() === '') {
59
+ continue; // Skip empty keys
60
+ }
61
+ // We'll check preservePatterns after namespace resolution below
62
+ let ns;
63
+ const remainder = text.slice(match.index + match[0].length);
64
+ const defaultValue = parseDefaultValueFromComment(remainder);
65
+ const context = parseContextFromComment(remainder);
66
+ const count = parseCountFromComment(remainder);
67
+ const ordinal = parseOrdinalFromComment(remainder);
68
+ // Check if key ends with _ordinal suffix (like in ast-visitors)
69
+ let isOrdinalByKey = false;
70
+ const pluralSeparator = config.extract.pluralSeparator ?? '_';
71
+ if (key.endsWith(`${pluralSeparator}ordinal`)) {
72
+ isOrdinalByKey = true;
73
+ // Normalize the key by stripping the suffix
74
+ key = key.slice(0, -(pluralSeparator.length + 7)); // Remove "_ordinal"
75
+ // Validate that the key is still not empty after normalization
76
+ if (!key || key.trim() === '') {
77
+ continue; // Skip keys that become empty after normalization
78
+ }
79
+ // Re-check preservePatterns after key normalization (will check namespace-aware helper)
80
+ if (matchesPreserve(key, ns)) {
81
+ continue; // Skip normalized keys that match preserve patterns
82
+ }
83
+ }
84
+ const isOrdinal = ordinal === true || isOrdinalByKey;
85
+ // 1. Check for namespace in options object first (e.g., { ns: 'common' })
86
+ ns = parseNsFromComment(remainder);
87
+ // 2. If not in options, check for separator in key (e.g., 'common:button.save')
88
+ const nsSeparator = config.extract.nsSeparator ?? ':';
89
+ if (!ns && nsSeparator && key.includes(nsSeparator)) {
90
+ const parts = key.split(nsSeparator);
91
+ ns = parts.shift();
92
+ key = parts.join(nsSeparator);
93
+ // Validate that the key didn't become empty after namespace removal
94
+ if (!key || key.trim() === '') {
95
+ continue; // Skip keys that become empty after namespace removal
96
+ }
97
+ // Re-check preservePatterns after namespace processing (namespace-aware)
98
+ if (matchesPreserve(key, ns)) {
99
+ continue; // Skip processed keys that match preserve patterns
100
+ }
101
+ }
102
+ // 3. If no explicit namespace found, try to resolve from scope
103
+ // This allows commented t() calls to inherit namespace from useTranslation scope
104
+ if (!ns && scopeResolver) {
105
+ const scopeInfo = scopeResolver('t');
106
+ if (scopeInfo?.defaultNs) {
107
+ ns = scopeInfo.defaultNs;
108
+ }
109
+ }
110
+ // Final preserve check for keys without prior namespace normalization
111
+ if (matchesPreserve(key, ns)) {
112
+ continue;
113
+ }
114
+ // 4. Final fallback to configured default namespace
115
+ if (!ns)
116
+ ns = config.extract.defaultNS;
117
+ // 5. Handle context and count combinations based on disablePlurals setting
118
+ if (config.extract.disablePlurals) {
119
+ // When plurals are disabled, ignore count for key generation
120
+ if (context) {
121
+ // Only generate context variants (no base key when context is static)
122
+ pluginContext.addKey({ key: `${key}_${context}`, ns, defaultValue: defaultValue ?? key });
123
+ }
124
+ else {
125
+ // Simple key (ignore count)
126
+ pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key });
127
+ }
128
+ }
129
+ else {
130
+ // Original plural handling logic when plurals are enabled
131
+ if (context && count) {
132
+ // Generate context+plural combinations
133
+ generateContextPluralKeys(key, defaultValue ?? key, ns, context, pluginContext, config, isOrdinal);
134
+ // Only generate base plural forms if generateBasePluralForms is not disabled
135
+ const shouldGenerateBaseForms = config.extract?.generateBasePluralForms !== false;
136
+ if (shouldGenerateBaseForms) {
137
+ generatePluralKeys(key, defaultValue ?? key, ns, pluginContext, config, isOrdinal);
138
+ }
139
+ }
140
+ else if (context) {
141
+ // Just context variants
142
+ pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key });
143
+ pluginContext.addKey({ key: `${key}_${context}`, ns, defaultValue: defaultValue ?? key });
144
+ }
145
+ else if (count) {
146
+ // Just plural variants
147
+ generatePluralKeys(key, defaultValue ?? key, ns, pluginContext, config, isOrdinal);
148
+ }
149
+ else {
150
+ // Simple key
151
+ pluginContext.addKey({ key, ns, defaultValue: defaultValue ?? key });
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Generates plural keys for a given base key
159
+ */
160
+ function generatePluralKeys(key, defaultValue, ns, pluginContext, config, isOrdinal = false) {
161
+ try {
162
+ const type = isOrdinal ? 'ordinal' : 'cardinal';
163
+ // Generate plural forms for ALL target languages to ensure we have all necessary keys
164
+ const allPluralCategories = new Set();
165
+ for (const locale of config.locales) {
166
+ try {
167
+ const pluralRules = new Intl.PluralRules(locale, { type });
168
+ const categories = pluralRules.resolvedOptions().pluralCategories;
169
+ categories.forEach(cat => allPluralCategories.add(cat));
170
+ }
171
+ catch (e) {
172
+ // If a locale is invalid, fall back to English rules
173
+ const englishRules = new Intl.PluralRules('en', { type });
174
+ const categories = englishRules.resolvedOptions().pluralCategories;
175
+ categories.forEach(cat => allPluralCategories.add(cat));
176
+ }
177
+ }
178
+ const pluralCategories = Array.from(allPluralCategories).sort();
179
+ const pluralSeparator = config.extract.pluralSeparator ?? '_';
180
+ // If the only plural category is "other", prefer emitting the base key instead of "key_other"
181
+ if (pluralCategories.length === 1 && pluralCategories[0] === 'other') {
182
+ // Emit base key only
183
+ pluginContext.addKey({
184
+ key,
185
+ ns,
186
+ defaultValue,
187
+ hasCount: true
188
+ });
189
+ return;
190
+ }
191
+ // Generate keys for each plural category
192
+ for (const category of pluralCategories) {
193
+ const finalKey = isOrdinal
194
+ ? `${key}${pluralSeparator}ordinal${pluralSeparator}${category}`
195
+ : `${key}${pluralSeparator}${category}`;
196
+ pluginContext.addKey({
197
+ key: finalKey,
198
+ ns,
199
+ defaultValue,
200
+ hasCount: true,
201
+ isOrdinal
202
+ });
203
+ }
204
+ }
205
+ catch (e) {
206
+ // Fallback if Intl API fails
207
+ pluginContext.addKey({ key, ns, defaultValue });
208
+ }
209
+ }
210
+ /**
211
+ * Generates context + plural combination keys
212
+ */
213
+ function generateContextPluralKeys(key, defaultValue, ns, context, pluginContext, config, isOrdinal = false) {
214
+ try {
215
+ const type = isOrdinal ? 'ordinal' : 'cardinal';
216
+ // Generate plural forms for ALL target languages to ensure we have all necessary keys
217
+ const allPluralCategories = new Set();
218
+ for (const locale of config.locales) {
219
+ try {
220
+ const pluralRules = new Intl.PluralRules(locale, { type });
221
+ const categories = pluralRules.resolvedOptions().pluralCategories;
222
+ categories.forEach(cat => allPluralCategories.add(cat));
223
+ }
224
+ catch (e) {
225
+ // If a locale is invalid, fall back to English rules
226
+ const englishRules = new Intl.PluralRules(config.extract.primaryLanguage || 'en', { type });
227
+ const categories = englishRules.resolvedOptions().pluralCategories;
228
+ categories.forEach(cat => allPluralCategories.add(cat));
229
+ }
230
+ }
231
+ const pluralCategories = Array.from(allPluralCategories).sort();
232
+ const pluralSeparator = config.extract.pluralSeparator ?? '_';
233
+ // Generate keys for each context + plural combination
234
+ for (const category of pluralCategories) {
235
+ const finalKey = isOrdinal
236
+ ? `${key}_${context}${pluralSeparator}ordinal${pluralSeparator}${category}`
237
+ : `${key}_${context}${pluralSeparator}${category}`;
238
+ pluginContext.addKey({
239
+ key: finalKey,
240
+ ns,
241
+ defaultValue,
242
+ hasCount: true,
243
+ isOrdinal
244
+ });
245
+ }
246
+ }
247
+ catch (e) {
248
+ // Fallback if Intl API fails
249
+ pluginContext.addKey({ key: `${key}_${context}`, ns, defaultValue });
250
+ }
251
+ }
252
+ /**
253
+ * Parses default value from the remainder of a comment after a translation function call.
254
+ * Supports both string literals and object syntax with defaultValue property.
255
+ *
256
+ * @param remainder - The remaining text after the translation key
257
+ * @returns The parsed default value or undefined if none found
258
+ *
259
+ * @internal
260
+ */
261
+ function parseDefaultValueFromComment(remainder) {
262
+ // Simple string default: , 'VALUE' or , "VALUE"
263
+ const dvString = /^\s*,\s*(['"])(.*?)\1/.exec(remainder);
264
+ if (dvString)
265
+ return dvString[2];
266
+ // Object with defaultValue: , { defaultValue: 'VALUE', ... }
267
+ const dvObj = /^\s*,\s*\{[^}]*defaultValue\s*:\s*(['"])(.*?)\1/.exec(remainder);
268
+ if (dvObj)
269
+ return dvObj[2];
270
+ return undefined;
271
+ }
272
+ /**
273
+ * Parses namespace from the remainder of a comment after a translation function call.
274
+ * Looks for namespace specified in options object syntax.
275
+ *
276
+ * @param remainder - The remaining text after the translation key
277
+ * @returns The parsed namespace or undefined if none found
278
+ *
279
+ * @internal
280
+ */
281
+ function parseNsFromComment(remainder) {
282
+ // Look for ns in an options object, e.g., { ns: 'common' }
283
+ const nsObj = /^\s*,\s*\{[^}]*ns\s*:\s*(['"])(.*?)\1/.exec(remainder);
284
+ if (nsObj)
285
+ return nsObj[2];
286
+ return undefined;
287
+ }
288
+ /**
289
+ * Collects all comment texts from source code, both single-line and multi-line.
290
+ * Deduplicates comments to avoid processing the same text multiple times.
291
+ *
292
+ * @param src - The source code to extract comments from
293
+ * @returns Array of unique comment text content
294
+ *
295
+ * @internal
296
+ */
297
+ function collectCommentTexts(src) {
298
+ const texts = [];
299
+ const seen = new Set();
300
+ const commentRegex = /\/\/(.*)|\/\*([\s\S]*?)\*\//g;
301
+ let cmatch;
302
+ while ((cmatch = commentRegex.exec(src)) !== null) {
303
+ const content = cmatch[1] ?? cmatch[2];
304
+ const s = content.trim();
305
+ if (s && !seen.has(s)) {
306
+ seen.add(s);
307
+ texts.push(s);
308
+ }
309
+ }
310
+ return texts;
311
+ }
312
+ /**
313
+ * Parses context from the remainder of a comment after a translation function call.
314
+ * Looks for context specified in options object syntax.
315
+ *
316
+ * @param remainder - The remaining text after the translation key
317
+ * @returns The parsed context value or undefined if none found
318
+ *
319
+ * @internal
320
+ */
321
+ function parseContextFromComment(remainder) {
322
+ // Look for context in an options object, e.g., { context: 'male' }
323
+ const contextObj = /^\s*,\s*\{[^}]*context\s*:\s*(['"])(.*?)\1/.exec(remainder);
324
+ if (contextObj)
325
+ return contextObj[2];
326
+ return undefined;
327
+ }
328
+ /**
329
+ * Parses count from the remainder of a comment after a translation function call.
330
+ * Looks for count specified in options object syntax.
331
+ *
332
+ * @param remainder - The remaining text after the translation key
333
+ * @returns The parsed count value or undefined if none found
334
+ *
335
+ * @internal
336
+ */
337
+ function parseCountFromComment(remainder) {
338
+ // Look for count in an options object, e.g., { count: 1 }
339
+ const countObj = /^\s*,\s*\{[^}]*count\s*:\s*(\d+)/.exec(remainder);
340
+ if (countObj)
341
+ return parseInt(countObj[1], 10);
342
+ return undefined;
343
+ }
344
+ /**
345
+ * Parses ordinal flag from the remainder of a comment after a translation function call.
346
+ * Looks for ordinal specified in options object syntax.
347
+ *
348
+ * @param remainder - The remaining text after the translation key
349
+ * @returns The parsed ordinal value or undefined if none found
350
+ *
351
+ * @internal
352
+ */
353
+ function parseOrdinalFromComment(remainder) {
354
+ // Look for ordinal in an options object, e.g., { ordinal: true }
355
+ const ordinalObj = /^\s*,\s*\{[^}]*ordinal\s*:\s*(true|false)/.exec(remainder);
356
+ if (ordinalObj)
357
+ return ordinalObj[1] === 'true';
358
+ return undefined;
359
+ }
360
+ /**
361
+ * Converts a glob pattern to a regular expression.
362
+ * Supports basic glob patterns with * wildcards.
363
+ *
364
+ * @param glob - The glob pattern to convert
365
+ * @returns A RegExp that matches the glob pattern
366
+ *
367
+ * @internal
368
+ */
369
+ function globToRegex(glob) {
370
+ const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
371
+ const regexString = `^${escaped.replace(/\*/g, '.*')}$`;
372
+ return new RegExp(regexString);
373
+ }
374
+
375
+ exports.extractKeysFromComments = extractKeysFromComments;