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.
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