All files / src i18n.ts

100% Statements 44/44
100% Branches 30/30
100% Functions 15/15
100% Lines 40/40

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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                                                      6x 6x 6x 1x   6x               6x             46x                   4x               3x             35x 35x   35x   35x   43x   41x   35x   35x 41x                           7x 2x   7x             3x                 4x       1x 1x 1x 1x 1x                     67x 21x 21x 16x 16x 8x     21x 16x 16x 16x 10x       57x    
import {
  compute,
  type OwnedReadable,
  type OwnedWritable,
  type Readable,
  type Writable,
  writable,
} from "@embra/reactivity";
 
import { type FlatLocale, flattenLocale } from "./flat-locales";
import type { Locale, LocaleFetcher, LocaleLang, Locales, TFunction, TFunctionArgs } from "./interface";
import { createTemplateMessageFn, type LocaleTemplateMessageFns } from "./template-message";
 
export interface I18nOptions {
  /** Fetch locale of the specified lang. */
  fetcher?: LocaleFetcher;
  /** Fallback language if the current language doesn't have the requested key. */
  fallback?: LocaleLang;
}
 
export class I18n {
  /** Fetch locale of `initialLang` and return an I18n instance with the locale. */
  public static async preload(
    initialLang: LocaleLang,
    fetcher: LocaleFetcher,
    options?: Omit<I18nOptions, "fetcher">,
  ): Promise<I18n> {
    const locales = { [initialLang]: await fetcher(initialLang) };
    const fallback = options?.fallback;
    if (fallback && fallback !== initialLang) {
      locales[fallback] = await fetcher(fallback);
    }
    return new I18n(initialLang, locales, { ...options, 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 fallback = options?.fallback;
 
    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$)));
 
    const fallbackLocale$ = fallback && compute((get) => fallback !== get(this.lang$) && get(this.locales$)[fallback]);
 
    this.t$ = compute((get) =>
      translate.bind(
        get(this._flatLocale$_),
        new Map(),
        get(fallbackLocale$) && translate.bind(flattenLocale(get(fallbackLocale$) as Locale), new Map(), ""),
      ),
    );
  }
 
  /**
   * 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();
  }
}
 
function translate(
  this: FlatLocale,
  localeFns: LocaleTemplateMessageFns,
  fallbackT: TFunction | null | "" | false | undefined,
  key: string,
  args?: TFunctionArgs,
): string {
  if (args) {
    const modifier = args["@"];
    if (modifier != null) {
      const modifierKey = `${key}@${modifier}`;
      if (this[modifierKey]) {
        key = modifierKey;
      }
    }
    if (this[key]) {
      let fn = localeFns.get(key);
      fn ?? localeFns.set(key, (fn = createTemplateMessageFn(this[key])));
      if (fn) {
        return fn(args);
      }
    }
  }
  return this[key] || (fallbackT && fallbackT(key, args)) || key;
}