monitor/core/modules/Translator.ts
2025-04-16 22:30:27 +07:00

137 lines
4.1 KiB
TypeScript

const modulename = 'Translator';
import fs from 'node:fs';
import Polyglot from 'node-polyglot';
import { txEnv, txHostConfig } from '@core/globalData';
import localeMap from '@shared/localeMap';
import consoleFactory from '@lib/console';
import fatalError from '@lib/fatalError';
import type { UpdateConfigKeySet } from './ConfigStore/utils';
import humanizeDuration, { HumanizerOptions } from 'humanize-duration';
import { msToDuration } from '@lib/misc';
import { z } from 'zod';
const console = consoleFactory(modulename);
//Schema for the custom locale file
export const localeFileSchema = z.object({
$meta: z.object({
label: z.string().min(1),
humanizer_language: z.string().min(1),
}),
}).passthrough();
/**
* Translation module built around Polyglot.js.
* The locale files are indexed by the localeMap in the shared folder.
*/
export default class Translator {
static readonly configKeysWatched = ['general.language'];
static readonly humanizerLanguages: string[] = humanizeDuration.getSupportedLanguages();
public readonly customLocalePath = txHostConfig.dataSubPath('locale.json');
public canonical: string = 'en-GB'; //Using GB instead of US due to date/time formats
#polyglot: Polyglot | null = null;
constructor() {
//Load language
this.setupTranslator(true);
}
/**
* Handle updates to the config by resetting the translator
*/
public handleConfigUpdate(updatedConfigs: UpdateConfigKeySet) {
this.setupTranslator(false);
}
/**
* Setup polyglot instance
*/
setupTranslator(isFirstTime = false) {
try {
this.canonical = Intl.getCanonicalLocales(txConfig.general.language.replace(/_/g, '-'))[0];
} catch (error) {
this.canonical = 'en-GB';
}
try {
const phrases = this.getLanguagePhrases(txConfig.general.language);
const polyglotOptions = {
allowMissing: false,
onMissingKey: (key: string) => {
console.error(`Missing key '${key}' from translation file.`, 'Translator');
return key;
},
phrases,
};
this.#polyglot = new Polyglot(polyglotOptions);
} catch (error) {
if (isFirstTime) {
fatalError.Translator(0, 'Failed to load initial language file', error);
} else {
console.dir(error);
}
}
}
/**
* Loads a language file or throws Error.
*/
getLanguagePhrases(lang: string) {
if (localeMap[lang]?.$meta) {
//If its a known language
return localeMap[lang];
} else if (lang === 'custom') {
//If its a custom language
try {
return JSON.parse(fs.readFileSync(
this.customLocalePath,
'utf8',
));
} catch (error) {
throw new Error(`Failed to load '${this.customLocalePath}'. (${(error as Error).message})`);
}
} else {
//If its an invalid language
throw new Error(`Language '${lang}' not found.`);
}
}
/**
* Perform a translation (polyglot.t)
*/
t(key: string, options = {}) {
if (!this.#polyglot) throw new Error(`polyglot not yet loaded`);
try {
return this.#polyglot.t(key, options);
} catch (error) {
console.error(`Error performing a translation with key '${key}'`);
return key;
}
}
/**
* Humanizes & translates a duration in ms
*/
tDuration(ms: number, options: HumanizerOptions = {}) {
if (!this.#polyglot) throw new Error(`polyglot not yet loaded`);
try {
const lang = this.#polyglot.t('$meta.humanizer_language')
return msToDuration(ms, { ...options, language: lang });
} catch (error) {
console.error(`Error humanizing duration`, error);
return String(ms)+'ms';
}
}
};