Created
October 18, 2025 15:39
-
-
Save ArrayIterator/5be3b05ea9c38a1766810def1734a918 to your computer and use it in GitHub Desktop.
Revisions
-
ArrayIterator created this gist
Oct 18, 2025 .There are no files selected for viewing
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 charactersOriginal 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();