webapp/src/ts/i18n.ts
2024-04-05 20:05:49 -04:00

272 lines
8.8 KiB
TypeScript

import { StringLike } from './StringLike';
import { Format } from './format';
/// All string keys.
export enum I18nStringKey {
// Generic things
kGeneric_CollabVM = 'kGeneric_CollabVM',
kGeneric_Yes = 'kGeneric_Yes',
kGeneric_No = 'kGeneric_No',
kGeneric_Ok = 'kGeneric_Ok',
kGeneric_Cancel = 'kGeneric_Cancel',
kSiteButtons_Home = 'kSiteButtons_Home',
kSiteButtons_FAQ = 'kSiteButtons_FAQ',
kSiteButtons_Rules = 'kSiteButtons_Rules',
kVM_UsersOnlineText = 'kVM_UsersOnlineText',
kVM_TurnTimeTimer = 'kVM_TurnTimeTimer',
kVM_WaitingTurnTimer = 'kVM_WaitingTurnTimer',
kVM_VoteCooldownTimer = 'kVM_VoteCooldownTimer',
kVM_VoteForResetTitle = 'kVM_VoteForResetTitle',
kVM_VoteForResetTimer = 'kVM_VoteForResetTimer',
kVMButtons_TakeTurn = 'kVMButtons_TakeTurn',
kVMButtons_EndTurn = 'kVMButtons_EndTurn',
kVMButtons_ChangeUsername = 'kVMButtons_ChangeUsername',
kVMButtons_VoteForReset = 'kVMButtons_VoteForReset',
kVMButtons_Screenshot = 'kVMButtons_Screenshot',
// Admin VM buttons
kAdminVMButtons_PassVote = 'kAdminVMButtons_PassVote',
kAdminVMButtons_CancelVote = 'kAdminVMButtons_CancelVote',
// prompts
kVMPrompts_EnterNewUsernamePrompt = 'kVMPrompts_EnterNewUsernamePrompt',
// error messages
kError_UnexpectedDisconnection = 'kError_UnexpectedDisconnection',
kError_UsernameTaken = 'kError_UsernameTaken',
kError_UsernameInvalid = 'kError_UsernameInvalid',
kError_UsernameBlacklisted = 'kError_UsernameBlacklisted',
// Auth
kAccountModal_Login = 'kAccountModal_Login',
kAccountModal_Register = 'kAccountModal_Register',
kAccountModal_Verify = 'kAccountModal_Verify',
kAccountModal_AccountSettings = 'kAccountModal_AccountSettings',
kAccountModal_ResetPassword = 'kAccountModal_ResetPassword',
kAccountModal_VerifyText = 'kAccountModal_VerifyText',
kAccountModal_VerifyPasswordResetText = 'kAccountModal_VerifyPasswordResetText',
kAccountModal_PasswordResetSuccess = 'kAccountModal_PasswordResetSuccess',
kMissingCaptcha = 'kMissingCaptcha',
kPasswordsMustMatch = 'kPasswordsMustMatch',
kNotLoggedIn = 'kNotLoggedIn',
}
// This models the JSON structure.
type Language = {
languageName: string;
translatedLanguageName: string;
flag: string; // country flag, can be blank if not applicable. will be displayed in language dropdown
author: string;
stringKeys: {
// This is fancy typescript speak for
// "any string index returns a string",
// which is our expectation.
// See https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures if this is confusing.
[key: string]: string;
};
};
// `languages.json`
type LanguagesJson = {
// Array of language IDs to allow loading
languages: Array<string>;
// The default language (set if a invalid language not in the languages array is set, or no language is set)
defaultLanguage: string;
};
// ID for fallback language
const fallbackId = '!!fallback';
// This language is provided in the webapp itself just in case language stuff fails
const fallbackLanguage: Language = {
languageName: 'Fallback',
translatedLanguageName: 'Fallback',
flag: 'no',
author: 'Computernewb',
stringKeys: {
kGeneric_CollabVM: 'CollabVM',
kGeneric_Yes: 'Yes',
kGeneric_No: 'No',
kGeneric_Ok: 'OK',
kGeneric_Cancel: 'Cancel',
kSiteButtons_Home: 'Home',
kSiteButtons_FAQ: 'FAQ',
kSiteButtons_Rules: 'Rules',
kVM_UsersOnlineText: 'Users Online:',
kVM_TurnTimeTimer: 'Turn expires in {0} seconds.',
kVM_WaitingTurnTimer: 'Waiting for turn in {0} seconds.',
kVM_VoteCooldownTimer: 'Please wait {0} seconds before starting another vote.',
kVM_VoteForResetTitle: 'Do you want to reset the VM?',
kVM_VoteForResetTimer: 'Vote ends in {0} seconds',
kVMButtons_TakeTurn: 'Take Turn',
kVMButtons_EndTurn: 'End Turn',
kVMButtons_ChangeUsername: 'Change Username',
kVMButtons_VoteForReset: 'Vote For Reset',
kVMButtons_Screenshot: 'Screenshot',
kAdminVMButtons_PassVoteButton: 'Pass Vote',
kAdminVMButtons_CancelVoteButton: 'Cancel Vote',
kVMPrompts_EnterNewUsernamePrompt: 'Enter a new username, or leave the field blank to be assigned a guest username',
kError_UnexpectedDisconnection: 'You have been disconnected from the server.',
kError_UsernameTaken: 'That username is already taken',
kError_UsernameInvalid: 'Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.',
kError_UsernameBlacklisted: 'That username has been blacklisted.'
}
};
interface StringKeyMap {
[k: string]: I18nStringKey;
}
/// our fancy internationalization helper.
export class I18n {
// The language data itself
private lang: Language = fallbackLanguage;
// the ID of the language
private langId: string = fallbackId;
private async LoadLanguageFile(id: string) {
let languageData = await I18n.LoadLanguageFileImpl(id);
this.SetLanguage(languageData, id);
}
async LoadAndSetLanguage(id: string) {
try {
await this.LoadLanguageFile(id);
console.log('i18n initalized for', id, 'sucessfully!');
} catch (e) {
alert(
`There was an error loading the language file for the language \"${id}\". Please tell a site admin this happened, and give them the following information: \"${(e as Error).message}\"`
);
// force set the language to fallback and replace all strings.
// (this is done because we initialize with fallback, so SetLanguage will
// refuse to replace static strings. Hacky but it should work)
this.SetLanguage(fallbackLanguage, fallbackId);
this.ReplaceStaticStrings();
}
}
async Init() {
// TODO: load languages.json, add selections, and if an invalid language (not in the languages array) is specified,
// set it to the defaultLanguage in there.
let lang = window.localStorage.getItem('i18n-lang');
// Set a default language if not specified
if (lang == null) {
lang = 'en-us';
window.localStorage.setItem('i18n-lang', lang);
}
await this.LoadAndSetLanguage(lang);
}
private static async LoadLanguageFileImpl(id: string): Promise<Language> {
let path = `./lang/${id}.json`;
let res = await fetch(path);
if (!res.ok) {
if (res.statusText != '') throw new Error(`Failed to load lang/${id}.json: ${res.statusText}`);
else throw new Error(`Failed to load lang/${id}.json: HTTP status code ${res.status}`);
}
return (await res.json()) as Language;
}
private SetLanguage(lang: Language, id: string) {
let lastId = this.langId;
this.langId = id;
this.lang = lang;
// Only replace static strings
if (this.langId != lastId) this.ReplaceStaticStrings();
// Set the language ID localstorage entry
if (this.langId !== fallbackId) {
window.localStorage.setItem('i18n-lang', this.langId);
}
}
// Replaces static strings that we don't recompute
private ReplaceStaticStrings() {
const kDomIdtoStringMap: StringKeyMap = {
siteNameText: I18nStringKey.kGeneric_CollabVM,
homeBtnText: I18nStringKey.kSiteButtons_Home,
faqBtnText: I18nStringKey.kSiteButtons_FAQ,
rulesBtnText: I18nStringKey.kSiteButtons_Rules,
usersOnlineText: I18nStringKey.kVM_UsersOnlineText,
voteResetHeaderText: I18nStringKey.kVM_VoteForResetTitle,
voteYesBtnText: I18nStringKey.kGeneric_Yes,
voteNoBtnText: I18nStringKey.kGeneric_No,
changeUsernameBtnText: I18nStringKey.kVMButtons_ChangeUsername,
voteForResetBtnText: I18nStringKey.kVMButtons_VoteForReset,
screenshotBtnText: I18nStringKey.kVMButtons_Screenshot,
// admin stuff
passVoteBtnText: I18nStringKey.kAdminVMButtons_PassVote,
cancelVoteBtnText: I18nStringKey.kAdminVMButtons_CancelVote,
endTurnBtnText: I18nStringKey.kVMButtons_EndTurn
};
for (let domId of Object.keys(kDomIdtoStringMap)) {
let element = document.getElementById(domId);
if (element == null) {
alert('Uhh!! THIS SHOULD NOT BE SEEN!! IF YOU DO YELL LOUDLY');
return;
}
// Do the magic.
// N.B: For now, we assume all strings in this map are not formatted.
// If this assumption changes, then we should just use GetString() again
// and maybe include arguments, but for now this is okay
element.innerText = this.GetStringRaw(kDomIdtoStringMap[domId]);
}
}
// Returns a (raw, unformatted) string. Currently only used if we don't need formatting.
GetStringRaw(key: I18nStringKey): string {
let val = this.lang.stringKeys[key];
// Look up the fallback language by default if the language doesn't
// have that string key yet; if the fallback doesn't have it either,
// then just return the string key and a bit of a notice things have gone wrong
if (val == undefined) {
let fallback = fallbackLanguage.stringKeys[key];
if (fallback !== undefined) val = fallback;
else return `${key} (ERROR LOOKING UP TRANSLATION!!!)`;
}
return val;
}
// Returns a formatted localized string.
GetString(key: I18nStringKey, ...replacements: StringLike[]): string {
return Format(this.GetStringRaw(key), ...replacements);
}
}
export let TheI18n = new I18n();