Skip to content

Instantly share code, notes, and snippets.

@ArrayIterator
Created October 18, 2025 15:39
Show Gist options
  • Save ArrayIterator/5be3b05ea9c38a1766810def1734a918 to your computer and use it in GitHub Desktop.
Save ArrayIterator/5be3b05ea9c38a1766810def1734a918 to your computer and use it in GitHub Desktop.

Revisions

  1. ArrayIterator created this gist Oct 18, 2025.
    387 changes: 387 additions & 0 deletions LanguagesManager.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,387 @@
    type UppercaseAlphabet =
    'A'
    | 'B'
    | 'C'
    | 'D'
    | 'E'
    | 'F'
    | 'G'
    | 'H'
    | 'I'
    | 'J'
    | 'K'
    | 'L'
    | 'M'
    | 'N'
    | 'O'
    | 'P'
    | 'Q'
    | 'R'
    | 'S'
    | 'T'
    | 'U'
    | 'V'
    | 'W'
    | 'X'
    | 'Y'
    | 'Z';

    export type LanguageCode<
    S extends string = `${UppercaseAlphabet}${UppercaseAlphabet}`,
    A extends `${UppercaseAlphabet}${UppercaseAlphabet}` = `${UppercaseAlphabet}${UppercaseAlphabet}`
    > = S extends A ? S : never;

    export type LanguageName = Required<string>;
    export type TranslationKey = string;
    export type TranslationValue = string | [
    singular: string,
    plural: string
    ];

    export type Translations = Record<TranslationKey, TranslationValue>;

    export interface LanguageDefinition<T extends LanguageCode = LanguageCode, LName extends LanguageName = LanguageName> {
    code: T;
    name: LName;
    readonly translations: Translations;
    }

    export class Language<T extends LanguageCode = LanguageCode, LName extends LanguageName = LanguageName> implements LanguageDefinition<T, LName> {
    public readonly code: T;
    public readonly name: LName;
    private _translations: Translations = {};
    static readonly PLACEHOLDER_DIGIT = '%d';

    constructor(languageCode: T, languageName: LName, translations?: Translations) {
    this.code = languageCode;
    this.name = languageName;
    this.replace(translations || {});
    }

    static createFromDefinitions<T extends LanguageCode = LanguageCode, LName extends LanguageName = LanguageName>(
    language: LanguageDefinition<T, LName>
    ): Language<T, LName> {
    return new Language(language.code, language.name, language.translations);
    }

    static createOrReuse<
    T extends LanguageCode = LanguageCode,
    LName extends LanguageName = LanguageName
    >(
    language: LanguageDefinition<T, LName> | Language<T, LName>
    ): Language<T, LName> {
    if (language instanceof Language) {
    return language;
    }
    return Language.createFromDefinitions<T, LName>(language);
    }

    get translations() {
    return {...this._translations}; // make a copy
    }

    clear = (): void => {
    this._translations = {};
    }

    replace = (translations: Translations): void => {
    this.clear();
    this._translations = translations;
    }

    addAll = (translations: Translations): void => {
    for (let [key, value] of Object.entries(translations)) {
    // noinspection SuspiciousTypeOfGuard
    if (typeof value === 'string') {
    this._translations[key] = value;
    continue;
    }
    if (!Array.isArray(value)) {
    continue;
    }
    // noinspection SuspiciousTypeOfGuard
    if (value.every(e => typeof e === 'string')) {
    this._translations[key] = [...value]; // make a copy
    }
    }
    }

    add = <
    T extends TranslationKey = TranslationKey,
    V extends TranslationValue = TranslationValue,
    >(key: T, value: V): void => {
    // noinspection SuspiciousTypeOfGuard
    if (typeof key !== 'string') {
    return;
    }
    // noinspection SuspiciousTypeOfGuard
    if (Array.isArray(value) && !value.every(e => typeof e === 'string')) {
    return;
    } else if (typeof value !== 'string') {
    return;
    }
    this._translations[key] = value;
    }

    remove = <T extends TranslationKey = TranslationKey>(key: T): void => {
    // noinspection SuspiciousTypeOfGuard
    if (typeof key == 'string') {
    delete this._translations[key];
    }
    }

    get = <T extends TranslationKey = TranslationKey>(key: T): Translations[T] | undefined => {
    return this._translations[key];
    }

    toJSON = (): LanguageDefinition<T, LName> => {
    return {
    code: this.code,
    name: this.name,
    translations: this.translations
    }
    }
    }

    class English extends Language<'EN', 'English'> {
    constructor() {
    super('EN', 'English');
    }

    add = () => undefined;
    remove = () => undefined;
    get = () => undefined;
    }

    type EventList = "translate" | "attach" | "detach";
    type ParameterType<T extends EventList, IsPlural extends boolean = boolean> = (
    T extends "translate" ?
    {
    pluralTranslation: IsPlural;
    singular: TranslationKey;
    plural: IsPlural extends true ? TranslationKey : undefined;
    n: IsPlural extends true ? number : undefined;
    translations?: TranslationValue;
    translation: string
    } :
    T extends "attach" ? {
    definitions: LanguageDefinition;
    language?: Language;
    success: boolean;
    } :
    T extends "detach" ? {
    languageCode: LanguageCode;
    language?: Language;
    success: boolean;
    } : never
    );
    type EventCallBack<EventName extends EventList, TParams extends ParameterType<EventName> = ParameterType<EventName>> = (param: TParams) => any | Promise<any>;

    class LanguagesManager {

    private readonly languages: Partial<Exclude<Record<LanguageCode, Language>, "EN">> & Record<"EN", Language<"EN", "English">> = {
    EN: new English(),
    };

    public readonly protectedLanguage: LanguageCode<"EN"> = 'EN';
    private selectedLanguage: LanguageCode = this.protectedLanguage;

    private events: Partial<{
    [K in EventList]: Array<[callback: EventCallBack<K>, once: boolean]>
    }> = {};

    once = <EventName extends EventList>(eventName: EventName, callback: EventCallBack<EventName>) => {
    if (!this.events[eventName]) {
    this.events[eventName] = [];
    }
    this.events[eventName].push([callback, true]);
    }

    on = <EventName extends EventList>(eventName: EventName, callback: EventCallBack<EventName>, once?: boolean) => {
    if (!this.events[eventName]) {
    this.events[eventName] = [];
    }
    this.events[eventName].push([callback, once === true]);
    }

    off = <EventName extends EventList>(eventName: EventName, callback: EventCallBack<EventName>) => {
    if (!this.events[eventName]) {
    return;
    }
    const index = this.events[eventName].findIndex(e => e[0] === callback);
    if (index === -1) {
    return;
    }
    this.events[eventName].splice(index, 1);
    if (this.events[eventName].length === 0) {
    delete this.events[eventName];
    }
    }

    emit = <EventName extends EventList>(eventName: EventName, param: ParameterType<EventName>) => {
    if (!this.events[eventName]) {
    return;
    }
    for (let [callback, once] of this.events[eventName]) {
    const result = callback(param);
    if (once) {
    this.off(eventName, callback);
    }
    if (result instanceof Promise) {
    result.catch(e => console.error(e));
    }
    }
    if (this.events[eventName]!.length === 0) {
    delete this.events[eventName];
    }
    }

    get activeLanguage(): LanguageCode {
    if (this.selectedLanguage === this.protectedLanguage) {
    return this.protectedLanguage;
    }
    if (!this.languages[this.selectedLanguage]) {
    this.selectedLanguage = this.protectedLanguage;
    }
    return this.selectedLanguage as keyof typeof this.languages;
    }

    get translation(): Language {
    const activeLanguage = this.activeLanguage;
    return this.languages[activeLanguage] ? this.languages[activeLanguage] : this.languages[this.protectedLanguage];
    }

    get = <T extends LanguageCode = LanguageCode>(key: T): Language<T> | undefined => {
    if (key === this.protectedLanguage) {
    return this.languages[this.protectedLanguage] as Language<T>;
    }
    if (key in this.languages) {
    return this.languages[key] as Language<T>;
    }
    }

    attach = <T extends LanguageCode = LanguageCode>(definition: LanguageDefinition<T>): boolean => {
    if (definition.code === this.protectedLanguage) {
    this.emit(
    'attach',
    {
    definitions: definition,
    language: this.languages[this.protectedLanguage],
    success: false
    }
    )
    return false;
    }
    const lang = Language.createOrReuse<T>(definition);
    if (lang.code === this.protectedLanguage) {
    this.emit(
    'attach',
    {
    definitions: definition,
    language: this.languages[this.protectedLanguage],
    success: false
    }
    )
    return false;
    }
    this.languages[lang.code as Exclude<LanguageCode, "EN">] = lang;
    this.emit('attach', {
    definitions: definition,
    language: lang,
    success: true
    })
    return true;
    }

    detach = <T extends LanguageCode = LanguageCode>(lang: T): Language<T> | undefined => {
    if (lang === this.protectedLanguage) {
    this.emit(
    'detach',
    {
    languageCode: lang,
    language: this.languages[this.protectedLanguage],
    success: false
    }
    )
    return;
    }
    if (lang in this.languages) {
    const language = this.languages[lang]!;
    delete this.languages[lang];
    if (language.code === this.selectedLanguage) {
    this.selectedLanguage = this.protectedLanguage;
    }
    this.emit(
    'detach',
    {
    languageCode: lang,
    language,
    success: true
    }
    )
    return language as Language<T>;
    }
    }

    singular = <T extends TranslationKey = TranslationKey>(key: T): string => {
    let translations = this.translation.get(key);
    let translation: string;
    if (!translations) {
    translation = key;
    } else if (Array.isArray(translations)) {
    translation = typeof translations[0] === 'string' ? (
    // auto resolve if both are empty
    key.trim() !== '' && translations[0].trim() !== '' ? translations[0] : key
    ) : key;
    } else {
    translation = translations;
    }
    this.emit('translate', {
    pluralTranslation: false,
    singular: key,
    plural: undefined,
    n: undefined,
    translations,
    translation
    })
    return translation;
    }

    plural = <
    S extends TranslationKey = TranslationKey,
    P extends TranslationKey = TranslationKey
    >(singular: S, plural: P, n: number) => {
    const offset = n > 1 ? 1 : 0;
    const translations = this.translation.get(singular);
    let translation: string;
    if (translations === undefined) {
    translation = offset === 0 ? singular : plural;
    } else if (Array.isArray(translations)) {
    translation = offset === 0 ? (translations[0] === undefined ? singular : (
    // auto resolve if both are empty
    singular.trim() !== '' && translations[0].trim() !== ''
    ? translations[0]
    : singular // if both are empty, use singular
    )) : (translations[1] === undefined ? plural : (
    // auto resolve if both are empty
    plural.trim() !== '' && translations[1].trim() !== ''
    ? translations[1]
    : plural
    ));
    } else {
    translation = offset === 0 ? translations : plural;
    }
    this.emit('translate', {
    pluralTranslation: true,
    singular,
    plural,
    n,
    translations,
    translation
    })
    return translation;
    }
    }

    export default new LanguagesManager();