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