i18next 26.4.7 → 26.5.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.
@@ -1 +1 @@
1
- {"type":"module","version":"26.4.7"}
1
+ {"type":"module","version":"26.5.1"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next",
3
- "version": "26.4.7",
3
+ "version": "26.5.1",
4
4
  "description": "i18next internationalization framework",
5
5
  "main": "./dist/cjs/i18next.js",
6
6
  "module": "./dist/esm/i18next.js",
@@ -189,8 +189,24 @@ export type TypeOptions = $MergeBy<
189
189
  * `}}`. This flag is type-only; runtime interpolation is governed by
190
190
  * `InterpolationOptions` and is unaffected.
191
191
  *
192
- * Required by `i18next-icu` users — see that package's docs for the
193
- * recommended `CustomTypeOptions` augmentation.
192
+ * Set to `'icu'` with `i18next-icu` to type `t()` options from the ICU
193
+ * MessageFormat arguments instead: `{name}` takes `string | number`,
194
+ * `{n, number}`, `plural` and `selectordinal` take `number`, `{d, date}`
195
+ * and `{t, time}` take `Date`, `select` takes its case keys or any string
196
+ * (for `other`), and arguments nested in cases are required too.
197
+ * `interpolationFormatTypeMap` overrides a type by its format name (e.g.
198
+ * `date: Date | number`). Options become required for a message with
199
+ * arguments.
200
+ *
201
+ * Set to `'mf2'` with `i18next-mf2` to type `t()` options from the
202
+ * MessageFormat 2 variables: `{$name}` takes `string | number`, a
203
+ * variable annotated with `:number`, `:integer`, `:currency`, `:percent`
204
+ * or `:unit` (directly or through `.input`/`.local`) takes `number`,
205
+ * `:datetime`, `:date` and `:time` take `Date`, `:string` takes `string`,
206
+ * option and markup variables (`minimumFractionDigits=$digits`,
207
+ * `{#link href=$url}`) take `string | number`, and `.local` variables are
208
+ * not arguments. `interpolationFormatTypeMap` overrides a type by its
209
+ * function name.
194
210
  *
195
211
  * @default true
196
212
  */
package/typescript/t.d.ts CHANGED
@@ -74,6 +74,26 @@ interface Branded<Ns extends Namespace> {
74
74
  : never;
75
75
  }
76
76
 
77
+ /**
78
+ * `keyof Res`, built once per resource object. TypeScript does not cache
79
+ * `keyof` of an object type, so `keyof Res` inside a conditional that runs per
80
+ * key (or per `t()` call) rebuilt the union of all keys every time: quadratic
81
+ * in the namespace size. Conditional type instantiations are cached.
82
+ */
83
+ type _KeysOf<Res> = [Res] extends [unknown] ? keyof Res : never;
84
+
85
+ /**
86
+ * `Res[Key & keyof Res]`: a literal key is looked up directly instead of being
87
+ * intersected with every key; a wide key (`string`, `${string}_suffix` from a
88
+ * string context) still matches all keys it covers.
89
+ */
90
+ type _ValueOf<Res, Key> =
91
+ Key extends _KeysOf<Res>
92
+ ? Res[Key]
93
+ : _IsLiteral<Key & PropertyKey> extends true
94
+ ? never
95
+ : Res[Key & _KeysOf<Res>];
96
+
77
97
  /** ****************************************************
78
98
  * Build all keys and key prefixes based on Resources *
79
99
  ***************************************************** */
@@ -81,18 +101,20 @@ interface Branded<Ns extends Namespace> {
81
101
  // conditional types, so `KeyPrefix<Ns>` no longer resolves to a literal union
82
102
  // and `keyPrefix` inference widens to the whole namespace, polluting `t()`
83
103
  // return types with sibling keys (#2434 regression). Guard `never` in the merge.
84
- type KeysBuilderWithReturnObjects<Res, Key = keyof Res> = Key extends keyof Res
85
- ? Res[Key] extends $Dictionary | readonly unknown[]
86
- ? | JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
87
- | JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
88
- : never
89
- : never;
104
+ type KeysBuilderWithReturnObjects<Res, Key = keyof Res> =
105
+ Key extends _KeysOf<Res>
106
+ ? Res[Key] extends $Dictionary | readonly unknown[]
107
+ ? | JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
108
+ | JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
109
+ : never
110
+ : never;
90
111
 
91
- type KeysBuilderWithoutReturnObjects<Res, Key = keyof $OmitArrayKeys<Res>> = Key extends keyof Res
92
- ? Res[Key] extends $Dictionary | readonly unknown[]
93
- ? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
94
- : Key
95
- : never;
112
+ type KeysBuilderWithoutReturnObjects<Res, Key = keyof $OmitArrayKeys<Res>> =
113
+ Key extends _KeysOf<Res>
114
+ ? Res[Key] extends $Dictionary | readonly unknown[]
115
+ ? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
116
+ : Key
117
+ : never;
96
118
 
97
119
  type KeysBuilder<Res, WithReturnObjects> = $IsResourcesDefined extends true
98
120
  ? WithReturnObjects extends true
@@ -172,7 +194,7 @@ type ParseActualValue<Ret> = Ret extends `${_UnescapePrefix}${infer ActualValue}
172
194
  : Ret;
173
195
 
174
196
  /** Parses interpolation entries as `[variableName, formatSpecifier | never]` tuples. */
175
- type ParseInterpolationEntries<Ret> = [_ParseInterpolation] extends [false]
197
+ type ParseInterpolationEntries<Ret> = [_ParseInterpolation] extends [false | 'icu' | 'mf2']
176
198
  ? never
177
199
  : Ret extends `${string}${_InterpolationPrefix}${infer Value}${_InterpolationSuffix}${infer Rest}`
178
200
  ? | (Value extends `${infer ActualValue},${infer Format}`
@@ -225,23 +247,213 @@ type _InterpolationMapFromEntries<E> = _UnionToIntersection<
225
247
  : never
226
248
  >;
227
249
 
228
- export type InterpolationMap<Ret> = $PreservedValue<
229
- _InterpolationMapFromEntries<ParseInterpolationEntries<Ret>>,
230
- Record<string, unknown>
231
- >;
250
+ /** *********************************************************************
251
+ * ICU MessageFormat arguments (`parseInterpolation: 'icu'`) *
252
+ *********************************************************************** */
253
+ type _IcuWhitespace = ' ' | '\n' | '\t' | '\r';
254
+ type _IcuTrim<S> = S extends `${_IcuWhitespace}${infer R}`
255
+ ? _IcuTrim<R>
256
+ : S extends `${infer R}${_IcuWhitespace}`
257
+ ? _IcuTrim<R>
258
+ : S;
259
+
260
+ /**
261
+ * Drops ICU apostrophe quoting as intl-messageformat (i18next-icu) reads it:
262
+ * `''` is a literal apostrophe, `'{...}'` (also `'}'`, `'<'`, `'>'`, `'#'`) is
263
+ * literal text up to the next single apostrophe.
264
+ * Simplification: `'#` quotes everywhere here, at runtime only inside plurals.
265
+ */
266
+ type _IcuStripQuotes<S, Acc extends string = ''> = S extends `${infer L}'${infer R}`
267
+ ? R extends `'${infer R2}`
268
+ ? _IcuStripQuotes<R2, `${Acc}${L}`>
269
+ : R extends `${'{' | '}' | '<' | '>' | '#'}${string}`
270
+ ? _IcuStripQuotes<_IcuSkipQuoted<R>, `${Acc}${L}`>
271
+ : _IcuStripQuotes<R, `${Acc}${L}'`>
272
+ : `${Acc}${S & string}`;
273
+ type _IcuSkipQuoted<S> = S extends `${string}'${infer R}`
274
+ ? R extends `'${infer R2}`
275
+ ? _IcuSkipQuoted<R2>
276
+ : R
277
+ : '';
278
+
279
+ /** Reads up to the `}` closing an already opened `{`; returns `[content, rest]`. */
280
+ type _IcuBlock<
281
+ S,
282
+ Acc extends string = '',
283
+ Depth extends 0[] = [],
284
+ > = S extends `${infer L}}${infer R}`
285
+ ? L extends `${infer L2}{${infer R2}`
286
+ ? _IcuBlock<`${R2}}${R}`, `${Acc}${L2}{`, [...Depth, 0]>
287
+ : Depth extends [0, ...infer D extends 0[]]
288
+ ? _IcuBlock<R, `${Acc}${L}}`, D>
289
+ : [`${Acc}${L}`, R]
290
+ : never;
291
+
292
+ /** `[name, type]` entries of every argument in a message, nested cases included. */
293
+ type _IcuEntries<S, Acc = never> = S extends `${string}{${infer R}`
294
+ ? _IcuBlock<R> extends [infer Arg, infer Rest]
295
+ ? _IcuEntries<Rest, Acc | _IcuArg<Arg>>
296
+ : Acc
297
+ : Acc;
298
+
299
+ /** Walks plural/select cases: `[selector keys, entries of all case messages]`. */
300
+ type _IcuCases<S, Keys = never, Acc = never> = S extends `${infer L}{${infer R}`
301
+ ? _IcuBlock<R> extends [infer Msg, infer Rest]
302
+ ? _IcuCases<Rest, Keys | _IcuTrim<L>, Acc | _IcuEntries<Msg>>
303
+ : [Keys, Acc]
304
+ : [Keys, Acc];
305
+
306
+ type _IcuArg<A> = A extends `${infer Name},${infer Type},${infer Cases}`
307
+ ? _IcuTrim<Type> extends 'plural' | 'selectordinal'
308
+ ? [_IcuTrim<Name>, _IcuFormatType<_IcuTrim<Type>>] | _IcuCases<Cases>[1]
309
+ : _IcuTrim<Type> extends 'select'
310
+ ? _IcuCases<Cases> extends [infer Keys, infer E]
311
+ ? [_IcuTrim<Name>, 'other' extends Keys ? Exclude<Keys, 'other'> | (string & {}) : Keys] | E
312
+ : never
313
+ : [_IcuTrim<Name>, _IcuFormatType<_IcuTrim<Type>>] // `{n, number, ::currency/EUR}`
314
+ : A extends `${infer Name},${infer Type}`
315
+ ? [_IcuTrim<Name>, _IcuFormatType<_IcuTrim<Type>>]
316
+ : [_IcuTrim<A>, _IcuFormatType<never>];
317
+
318
+ /** `interpolationFormatTypeMap` wins, then ICU's built-in argument types. */
319
+ type _IcuFormatType<F> = [F] extends [never]
320
+ ? string | number
321
+ : F extends keyof _InterpolationFormatTypeMap
322
+ ? _InterpolationFormatTypeMap[F]
323
+ : F extends 'number' | 'plural' | 'selectordinal'
324
+ ? number
325
+ : F extends 'date' | 'time'
326
+ ? Date
327
+ : string | number;
328
+
329
+ /**
330
+ * Merges `[name, type]` entries into one record: a name used twice gets both
331
+ * types, a dotted MF2 name (`$user.name`) becomes a nested object.
332
+ */
333
+ type _ArgsFromEntries<E> =
334
+ _UnionToIntersection<
335
+ E extends [infer Name extends string, infer Type]
336
+ ? Name extends `${string}.${string}`
337
+ ? $StringKeyPathToRecord<Name, Type>
338
+ : { [K in Name]: Type }
339
+ : never
340
+ > extends infer M
341
+ ? [E] extends [never]
342
+ ? never
343
+ : { [K in keyof M]: M[K] }
344
+ : never;
345
+
346
+ type _IcuArgsMap<Ret> = Ret extends string
347
+ ? string extends Ret
348
+ ? never
349
+ : _ArgsFromEntries<_IcuEntries<_IcuStripQuotes<Ret>>>
350
+ : never;
351
+
352
+ /** *********************************************************************
353
+ * MessageFormat 2 variables (`parseInterpolation: 'mf2'`) *
354
+ *********************************************************************** */
355
+ /** Drops `\`-escapes (`\{`, `\}`, `\|`, `\\`), so they are neither braces nor quotes. */
356
+ type _Mf2StripEscapes<S, Acc extends string = ''> = S extends `${infer L}\\${string}${infer R}`
357
+ ? _Mf2StripEscapes<R, `${Acc}${L}`>
358
+ : `${Acc}${S & string}`;
359
+
360
+ /** Reads an expression up to its `}`, skipping `|quoted literals|`; returns `[body, rest]`. */
361
+ type _Mf2Expression<S, Acc extends string = ''> = S extends `${infer B}}${infer Rest}`
362
+ ? B extends `${infer Before}|${infer Lit}`
363
+ ? `${Lit}}${Rest}` extends `${string}|${infer After}`
364
+ ? _Mf2Expression<After, `${Acc}${Before} `>
365
+ : never
366
+ : [`${Acc}${B}`, Rest]
367
+ : never;
368
+
369
+ /** `[name, type]` entries of every variable in a message's expressions; `{{` opens a quoted pattern. */
370
+ type _Mf2Entries<S, Acc = never> = S extends `${string}{${infer R}`
371
+ ? R extends `{${infer Pattern}`
372
+ ? _Mf2Entries<Pattern, Acc>
373
+ : _Mf2Expression<R> extends [infer Body, infer Rest]
374
+ ? _Mf2Entries<Rest, Acc | _Mf2ExprEntries<_Mf2Spaces<Body>>>
375
+ : Acc
376
+ : Acc;
377
+
378
+ type _Mf2Space<S, C extends string> = S extends `${infer L}${C}${infer R}`
379
+ ? _Mf2Space<`${L} ${R}`, C>
380
+ : S;
381
+ type _Mf2Spaces<S> = _IcuTrim<_Mf2Space<_Mf2Space<_Mf2Space<S, '\n'>, '\t'>, '\r'>>;
382
+ type _Mf2Name<S> = S extends `${infer N} ${string}`
383
+ ? _Mf2Name<N>
384
+ : S extends `${infer N}/${string}`
385
+ ? N
386
+ : S;
387
+
388
+ /** `{$x :fn opt=$y}`: `$x` typed by its function, option and markup variables `string | number`. */
389
+ type _Mf2ExprEntries<Body> = Body extends `$${infer R}`
390
+ ? R extends `${infer Name} ${infer Tail}`
391
+ ? | [Name, _Mf2FunctionType<_IcuTrim<Tail> extends `:${infer F}` ? _Mf2Name<F> : never>]
392
+ | _Mf2Vars<Tail>
393
+ : [_Mf2Name<R>, _Mf2FunctionType<never>]
394
+ : _Mf2Vars<Body>;
395
+
396
+ /** Every `$name` in a string, typed `string | number`. */
397
+ type _Mf2Vars<S, Acc = never> = S extends `${string}$${infer R}`
398
+ ? _Mf2Vars<R, Acc | [_Mf2Name<R>, string | number]>
399
+ : Acc;
400
+
401
+ /** `interpolationFormatTypeMap` wins, then messageformat's built-in functions. */
402
+ type _Mf2FunctionType<F> = [F] extends [never]
403
+ ? string | number
404
+ : F extends keyof _InterpolationFormatTypeMap
405
+ ? _InterpolationFormatTypeMap[F]
406
+ : F extends 'number' | 'integer' | 'currency' | 'percent' | 'offset' | 'unit' | 'math'
407
+ ? number
408
+ : F extends 'datetime' | 'date' | 'time'
409
+ ? Date
410
+ : F extends 'string'
411
+ ? string
412
+ : string | number;
413
+
414
+ /** Names declared by `.local $name = ...` (local, not passed in). */
415
+ type _Mf2Locals<S, Acc = never> = S extends `${string}.local${infer R}`
416
+ ? _Mf2Locals<R, Acc | (_IcuTrim<R> extends `$${infer N}=${string}` ? _IcuTrim<N> : never)>
417
+ : Acc;
418
+ /** `.match $a $b`: selectors, normally also declared by `.input` or `.local`. */
419
+ type _Mf2Selectors<S> = S extends `${string}.match${infer R}`
420
+ ? _Mf2Vars<_Mf2Spaces<R extends `${infer Sel}{${string}` ? Sel : R>>
421
+ : never;
422
+
423
+ type _Mf2ArgsMap<Ret> = Ret extends string
424
+ ? string extends Ret
425
+ ? never
426
+ : _Mf2StripEscapes<Ret> extends infer S
427
+ ? _ArgsFromEntries<
428
+ Exclude<
429
+ _Mf2Entries<S> | _Mf2Selectors<S>,
430
+ [_IcuTrim<S> extends `.${string}` ? _Mf2Locals<S> : never, unknown]
431
+ >
432
+ >
433
+ : never
434
+ : never;
435
+
436
+ export type InterpolationMap<Ret> = [_ParseInterpolation] extends ['icu']
437
+ ? $PreservedValue<_IcuArgsMap<Ret>, Record<string, unknown>>
438
+ : [_ParseInterpolation] extends ['mf2']
439
+ ? $PreservedValue<_Mf2ArgsMap<Ret>, Record<string, unknown>>
440
+ : $PreservedValue<
441
+ _InterpolationMapFromEntries<ParseInterpolationEntries<Ret>>,
442
+ Record<string, unknown>
443
+ >;
232
444
 
233
445
  type ParseTReturnPlural<
234
446
  Res,
235
447
  Key,
236
448
  KeyWithPlural = `${Key & string}${_PluralSeparator}${PluralSuffix}`,
237
- > = Res[(KeyWithPlural | Key) & keyof Res];
449
+ > = _ValueOf<Res, KeyWithPlural | Key>;
238
450
 
239
451
  type ParseTReturnPluralOrdinal<
240
452
  Res,
241
453
  Key,
242
454
  KeyWithOrdinalPlural = `${Key &
243
455
  string}${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`,
244
- > = Res[(KeyWithOrdinalPlural | Key) & keyof Res];
456
+ > = _ValueOf<Res, KeyWithOrdinalPlural | Key>;
245
457
 
246
458
  type ParseTReturnWithFallback<Key, Val> = Val extends ''
247
459
  ? _ReturnEmptyString extends true
@@ -256,7 +468,7 @@ type ParseTReturnWithFallback<Key, Val> = Val extends ''
256
468
  type ParseTReturn<Key, Res, TOpt extends TOptions = {}> = ParseTReturnWithFallback<
257
469
  Key,
258
470
  Key extends `${infer K1}${_KeySeparator}${infer RestKey}`
259
- ? ParseTReturn<RestKey, Res[K1 & keyof Res], TOpt>
471
+ ? ParseTReturn<RestKey, _ValueOf<Res, K1>, TOpt>
260
472
  : // Process plurals only if count is provided inside options
261
473
  TOpt['count'] extends number
262
474
  ? TOpt['ordinal'] extends boolean
@@ -267,7 +479,7 @@ type ParseTReturn<Key, Res, TOpt extends TOptions = {}> = ParseTReturnWithFallba
267
479
  ? Key extends `${infer NKey extends number}`
268
480
  ? Res[NKey]
269
481
  : never
270
- : Res[Key & keyof Res]
482
+ : _ValueOf<Res, Key>
271
483
  >;
272
484
 
273
485
  type TReturnOptionalNull = _ReturnNull extends true ? null : never;
@@ -393,6 +605,28 @@ type EffectiveKPrefix<KPrefix, TOpt> = TOpt extends { keyPrefix: infer OptKP ext
393
605
  ? OptKP
394
606
  : KPrefix;
395
607
 
608
+ /** `parseInterpolation: 'icu'` or `'mf2'`: messages are typed by their arguments. */
609
+ type _ParsesMessageFormat = [_ParseInterpolation] extends ['icu'] | ['mf2'] ? true : false;
610
+
611
+ /**
612
+ * `t()` options are optional, except for a message with arguments under
613
+ * `parseInterpolation: 'icu'` or `'mf2'` (on a missing one, i18next-icu renders
614
+ * the raw message and i18next-mf2 a `{$name}` fallback). The conditional
615
+ * resolves eagerly; a generic one here would break inference.
616
+ */
617
+ type _KeyAndOptions<Key, Ret, Opts> = _ParsesMessageFormat extends true
618
+ ? [key: Key | Key[], options: Opts] | [key: (Key | Key[]) & _IfOptionalOptions<Ret>]
619
+ : [key: Key | Key[], options?: Opts];
620
+ /** `unknown` when options may be left out, else a type naming the missing arguments. */
621
+ type _IfOptionalOptions<Ret> = _ParsesMessageFormat extends true
622
+ ? {} extends InterpolationMap<Ret>
623
+ ? unknown
624
+ : { 'pass these arguments as t() options': InterpolationMap<Ret> }
625
+ : unknown;
626
+ /** Options parameter of a selector overload: required for a message with arguments. */
627
+ type _SelectorOptionsArg<Ret, Opts> =
628
+ unknown extends _IfOptionalOptions<Ret> ? [options?: Opts] : [options: Opts];
629
+
396
630
  /** ************************
397
631
  * T function declaration *
398
632
  ************************* */
@@ -407,7 +641,7 @@ interface TFunctionStrict<
407
641
  Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, EffectiveKPrefix<KPrefix, TOpt>>, TOpt>,
408
642
  >(
409
643
  key: Key | Key[],
410
- options?: TOpt &
644
+ options: TOpt &
411
645
  InterpolationMap<Ret> & {
412
646
  context?: Key extends string
413
647
  ? unknown extends TOpt['context']
@@ -416,6 +650,13 @@ interface TFunctionStrict<
416
650
  : never;
417
651
  },
418
652
  ): TFunctionReturnOptionalDetails<TFunctionProcessReturnValue<$NoInfer<Ret>, never>, TOpt>;
653
+ <
654
+ const Key extends ParseKeys<Ns, TOpt, EffectiveKPrefix<KPrefix, TOpt>> | TemplateStringsArray,
655
+ const TOpt extends TOptions,
656
+ Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, EffectiveKPrefix<KPrefix, TOpt>>, TOpt>,
657
+ >(
658
+ key: (Key | Key[]) & _IfOptionalOptions<$NoInfer<Ret>>,
659
+ ): TFunctionReturnOptionalDetails<TFunctionProcessReturnValue<$NoInfer<Ret>, never>, TOpt>;
419
660
  <
420
661
  const Key extends ParseKeys<Ns, TOpt, EffectiveKPrefix<KPrefix, TOpt>> | TemplateStringsArray,
421
662
  const TOpt extends TOptions,
@@ -460,9 +701,9 @@ interface TFunctionNonStrict<
460
701
  DefaultValue extends string = never,
461
702
  >(
462
703
  ...args:
463
- | [key: Key | Key[], options?: ActualOptions]
464
704
  | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: DefaultValue }]
465
705
  | [key: string | string[], defaultValue: DefaultValue, options?: TOpt & $Dictionary]
706
+ | _KeyAndOptions<Key, $NoInfer<Ret>, ActualOptions>
466
707
  ): TFunctionReturnOptionalDetails<TFunctionProcessReturnValue<$NoInfer<Ret>, DefaultValue>, TOpt>;
467
708
  }
468
709
 
@@ -596,10 +837,13 @@ interface TFunctionSelector<Ns extends Namespace, KPrefix, Source> extends Brand
596
837
  const Opts extends SelectorOptions<Ns[number]> = SelectorOptions<Ns[number]>,
597
838
  >(
598
839
  selectors: Fns,
599
- options?: Opts &
600
- InterpolationMap<
601
- DeepUnwrapPlural<Fns[number] extends (...args: any[]) => infer R ? R : never>
602
- >,
840
+ ...args: _SelectorOptionsArg<
841
+ DeepUnwrapPlural<Fns[number] extends (...args: any[]) => infer R ? R : never>,
842
+ Opts &
843
+ InterpolationMap<
844
+ DeepUnwrapPlural<Fns[number] extends (...args: any[]) => infer R ? R : never>
845
+ >
846
+ >
603
847
  ): SelectorReturn<Fns[number] extends (...args: any[]) => infer R ? R : never, Opts>;
604
848
 
605
849
  // ── Single selector with context + returnObjects ────────────────────────────
@@ -667,7 +911,7 @@ interface TFunctionSelector<Ns extends Namespace, KPrefix, Source> extends Brand
667
911
  selector: Fn,
668
912
  ...args: ReturnType<Fn> extends PluralValue<string>
669
913
  ? [options: Opts & { count: number } & InterpolationMap<DeepUnwrapPlural<ReturnType<Fn>>>]
670
- : [options?: Opts & InterpolationMap<ReturnType<Fn>>]
914
+ : _SelectorOptionsArg<ReturnType<Fn>, Opts & InterpolationMap<ReturnType<Fn>>>
671
915
  ): SelectorReturn<ReturnType<Fn>, Opts>;
672
916
  }
673
917
 
@@ -751,26 +995,97 @@ type _DefinedContextKeys<T, Pattern extends string> = {
751
995
  [P in keyof T as P extends Pattern ? ([T[P]] extends [undefined] ? never : P) : never]: true;
752
996
  };
753
997
 
754
- type _HasContextVariant<T, K extends string, Context> = [
755
- keyof _DefinedContextKeys<
756
- T,
757
- | `${K}${_ContextSeparator}${Context & string}`
758
- | `${K}${_ContextSeparator}${Context & string}${_PluralSeparator}${PluralSuffix}`
759
- >,
998
+ /**
999
+ * Keys of T whose value is not `undefined` (JSON-union phantoms are typed as
1000
+ * optional `undefined`), built once per object.
1001
+ */
1002
+ type _DefinedKeys<T> = [T] extends [unknown]
1003
+ ? keyof { [P in keyof T as [T[P]] extends [undefined] ? never : P]: true }
1004
+ : never;
1005
+
1006
+ /**
1007
+ * For every defined key, the prefixes K before each context separator, as
1008
+ * `[all, bases]`: `all` has every K with a defined key `${K}${_ContextSeparator}...`,
1009
+ * `bases` only those where the rest is not just a plural suffix. Jumps from one
1010
+ * separator occurrence to the next (so the recursion depth is the number of
1011
+ * separators, not the key length, which can be a whole sentence) and resumes one
1012
+ * character after the match, so overlapping occurrences (`a___b` with `__`) count.
1013
+ */
1014
+ type _ContextPrefixesOf<
1015
+ P,
1016
+ Head extends string = '',
1017
+ All = never,
1018
+ Bases = never,
1019
+ > = P extends `${infer B}${_ContextSeparator}${string}`
1020
+ ? P extends `${B}${infer FromSep}`
1021
+ ? FromSep extends `${infer C}${infer Rest}`
1022
+ ? _ContextPrefixesOf<
1023
+ Rest,
1024
+ `${Head}${B}${C}`,
1025
+ All | `${Head}${B}`,
1026
+ FromSep extends
1027
+ | `${_PluralSeparator}${PluralSuffix}`
1028
+ | `${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`
1029
+ ? Bases
1030
+ : Bases | `${Head}${B}`
1031
+ >
1032
+ : [All, Bases]
1033
+ : [All, Bases]
1034
+ : [All, Bases];
1035
+
1036
+ /**
1037
+ * The context prefixes of an object, built once per object (conditional type
1038
+ * instantiations are cached): scanning all keys for every key was quadratic
1039
+ * in the object size, TS2589 at about 1,000 keys with context variants.
1040
+ */
1041
+ type _ContextPrefixes<T> = [T] extends [unknown]
1042
+ ? _ContextPrefixesOf<_DefinedKeys<T> & string>
1043
+ : never;
1044
+ type _AllContextPrefixes<T> = [T] extends [unknown] ? _ContextPrefixes<T>[0] : never;
1045
+ type _ContextBases<T> = [T] extends [unknown] ? _ContextPrefixes<T>[1] : never;
1046
+
1047
+ /** Whether a string type is a single literal (not `string` or a `${string}` pattern). */
1048
+ type _IsLiteral<S extends PropertyKey> = {} extends Record<S, unknown> ? false : true;
1049
+
1050
+ /**
1051
+ * Whether T has a defined key matching Pattern: literal keys are looked up, a
1052
+ * pattern with a wide member scans the keys.
1053
+ */
1054
+ type _HasDefinedKey<T, Pattern extends string> = [
1055
+ false extends (Pattern extends unknown ? _IsLiteral<Pattern> : never)
1056
+ ? keyof _DefinedContextKeys<T, Pattern>
1057
+ : Extract<Pattern, _DefinedKeys<T>>,
760
1058
  ] extends [never]
761
1059
  ? false
762
1060
  : true;
763
1061
 
1062
+ type _HasContextVariant<T, K extends string, Context> = string extends Context
1063
+ ? _IsLiteral<K> extends true
1064
+ ? K extends _AllContextPrefixes<T>
1065
+ ? true
1066
+ : false
1067
+ : _HasDefinedKey<T, `${K}${_ContextSeparator}${string}`>
1068
+ : _HasDefinedKey<
1069
+ T,
1070
+ | `${K}${_ContextSeparator}${Context & string}`
1071
+ | `${K}${_ContextSeparator}${Context & string}${_PluralSeparator}${PluralSuffix}`
1072
+ >;
1073
+
764
1074
  /** Checks whether key K has **any** context variant in T (excluding pure plural suffixes). */
765
- type _IsContextualKey<T, K extends string> = [
766
- Exclude<
767
- keyof _DefinedContextKeys<T, `${K}${_ContextSeparator}${string}`>,
768
- | `${K}${_PluralSeparator}${PluralSuffix}`
769
- | `${K}${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`
770
- >,
771
- ] extends [never]
772
- ? false
773
- : true;
1075
+ type _IsContextualKey<T, K extends string> =
1076
+ _IsLiteral<K> extends true
1077
+ ? K extends _ContextBases<T>
1078
+ ? true
1079
+ : false
1080
+ : [
1081
+ Exclude<
1082
+ keyof _DefinedContextKeys<T, `${K}${_ContextSeparator}${string}`>,
1083
+ | `${K}${_PluralSeparator}${PluralSuffix}`
1084
+ | `${K}${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`
1085
+ >,
1086
+ ] extends [never]
1087
+ ? false
1088
+ : true;
774
1089
 
775
1090
  /**
776
1091
  * Distributes {@link FilterKeysObject} over unions so heterogeneous JSON array