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; export type TranslationKey = string; export type TranslationValue = string | [ singular: string, plural: string ]; export type Translations = Record; export interface LanguageDefinition { code: T; name: LName; readonly translations: Translations; } export class Language implements LanguageDefinition { 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( language: LanguageDefinition ): Language { return new Language(language.code, language.name, language.translations); } static createOrReuse< T extends LanguageCode = LanguageCode, LName extends LanguageName = LanguageName >( language: LanguageDefinition | Language ): Language { if (language instanceof Language) { return language; } return Language.createFromDefinitions(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 = (key: T): void => { // noinspection SuspiciousTypeOfGuard if (typeof key == 'string') { delete this._translations[key]; } } get = (key: T): Translations[T] | undefined => { return this._translations[key]; } toJSON = (): LanguageDefinition => { 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 "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 = ParameterType> = (param: TParams) => any | Promise; class LanguagesManager { private readonly languages: Partial, "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, once: boolean]> }> = {}; once = (eventName: EventName, callback: EventCallBack) => { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push([callback, true]); } on = (eventName: EventName, callback: EventCallBack, once?: boolean) => { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push([callback, once === true]); } off = (eventName: EventName, callback: EventCallBack) => { 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: EventName, param: ParameterType) => { 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 = (key: T): Language | undefined => { if (key === this.protectedLanguage) { return this.languages[this.protectedLanguage] as Language; } if (key in this.languages) { return this.languages[key] as Language; } } attach = (definition: LanguageDefinition): boolean => { if (definition.code === this.protectedLanguage) { this.emit( 'attach', { definitions: definition, language: this.languages[this.protectedLanguage], success: false } ) return false; } const lang = Language.createOrReuse(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] = lang; this.emit('attach', { definitions: definition, language: lang, success: true }) return true; } detach = (lang: T): Language | 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; } } singular = (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();