All files / src i18n.ts

100% Statements 38/38
100% Branches 20/20
100% Functions 14/14
100% Lines 34/34

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 140                                                        2x               5x             24x                   3x               3x             22x   22x   22x   22x   29x   28x   27x   36x 13x 13x 10x 10x 6x     13x 12x 12x 12x 8x       28x                     6x 2x   6x             3x                 3x       1x 1x 1x 1x 1x      
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();
  }
}