Created
October 18, 2025 15:39
-
-
Save ArrayIterator/5be3b05ea9c38a1766810def1734a918 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment