All files / src i18n.ts

100% Statements 72/72
100% Branches 27/27
100% Functions 11/11
100% Lines 72/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 1402x                 2x                 2x             2x   22x 2x 2x     22x     22x 5x 5x     22x     22x     22x     22x     22x 3x 3x     22x     22x 3x 3x     22x   22x 22x   22x   22x   22x   22x   22x   22x 27x 36x 13x 13x 10x 10x 6x 6x 10x 13x 12x 12x 12x 8x 8x 12x 13x 36x 27x 22x 22x             22x 6x 2x 2x 6x 6x         22x 3x 3x             22x 3x 3x   22x 1x 1x 1x 1x 1x 1x 22x  
import {
  compute,
  type OwnedReadable,
  type OwnedWritable,
  type Readable,
  writable,
  type Writable,
} from "@embra/reactivity";
 
import { type FlatLocale, flattenLocale } from "./flat-locales";
import {
  type Locale,
  type LocaleFetcher,
  type LocaleLang,
  type Locales,
  type TFunction,
  type TFunctionArgs,
} from "./interface";
import { createTemplateMessageFn, type LocaleTemplateMessageFns } from "./template-message";
 
export interface I18nOptions {
  /** Fetch locale of the specified lang. */
  fetcher: LocaleFetcher;
}
 
export class I18n {
  /** Fetch locale of `initialLang` and return an I18n instance with the locale. */
  public static async preload(initialLang: LocaleLang, fetcher: LocaleFetcher): Promise<I18n> {
    return new I18n(initialLang, { [initialLang]: await fetcher(initialLang) }, { fetcher });
  }
 
  /** A {@link Readable} of current lang. */
  public readonly lang$: Readable<LocaleLang>;
 
  /** Current lang. */
  public get lang(): LocaleLang {
    return this.lang$.get();
  }
 
  /** A {@link Readable} of current translate function. */
  public readonly t$: Readable<TFunction>;
 
  /** Translation function that uses the current `t$` function. */
  public readonly t: TFunction = (keyPath, args) => this.t$.get()(keyPath, args);
 
  /** Fetch locale of the specified lang. */
  public fetcher?: LocaleFetcher;
 
  /** A {@link Writable} of all loaded locales. */
  public readonly locales$: Writable<Locales>;
 
  /** All loaded locales. */
  public get locales(): Locales {
    return this.locales$.get();
  }
 
  /** A {@link Readable} of current locale. */
  public readonly locale$: Readable<Locale>;
 
  /** Current locale */
  public get locale(): Locale {
    return this.locale$.get();
  }
 
  /** @internal */
  private readonly _flatLocale$_: Readable<FlatLocale>;
 
  public constructor(initialLang: LocaleLang, locales: Locales, options?: I18nOptions) {
    this.fetcher = options?.fetcher;
 
    const localeFns: LocaleTemplateMessageFns = new Map();
 
    this.locales$ = writable(locales);
 
    this.lang$ = writable(initialLang);
 
    this.locale$ = compute(get => get(this.locales$)[get(this.lang$)] || {});
 
    this._flatLocale$_ = compute(get => flattenLocale(get(this.locale$)));
 
    this.t$ = compute(get =>
      ((flatLocale: FlatLocale, key: string, args?: TFunctionArgs): string => {
        if (args) {
          const modifier = args["@"];
          if (modifier != null) {
            const modifierKey = `${key}@${modifier}`;
            if (flatLocale[modifierKey]) {
              key = modifierKey;
            }
          }
          if (flatLocale[key]) {
            let fn = localeFns.get(key);
            fn ?? localeFns.set(key, (fn = createTemplateMessageFn(flatLocale[key])));
            if (fn) {
              return fn(args);
            }
          }
        }
        return flatLocale[key] || key;
      }).bind(localeFns.clear(), get(this._flatLocale$_)),
    );
  }
 
  /**
   * Change language.
   *
   * @returns — a promise that resolves when the new locale is fetched.
   */
  public async switchLang(lang: LocaleLang): Promise<void> {
    if (!this.locales$.get()[lang] && this.fetcher) {
      this.addLocale(lang, await this.fetcher(lang));
    }
    (this.lang$ as Writable<LocaleLang>).set(lang);
  }
 
  /**
   * @returns — boolean indicating whether a message with the specified key in current language exists or not.
   */
  public hasKey(key: string): boolean {
    return !!this._flatLocale$_.get()[key];
  }
 
  /**
   * Add a locale to the locales.
   *
   * Use `i18n.locales$.set()` for more control.
   */
  public addLocale(lang: LocaleLang, locale: Locale): void {
    this.locales$.set({ ...this.locales, [lang]: locale });
  }
 
  public dispose(): void {
    (this.lang$ as OwnedReadable).dispose();
    (this.t$ as OwnedReadable).dispose();
    (this.locales$ as OwnedWritable).dispose();
    (this.locale$ as OwnedReadable).dispose();
    (this._flatLocale$_ as OwnedReadable).dispose();
  }
}