mirror of
https://github.com/computernewb/collab-vm-1.2-webapp.git
synced 2025-01-22 10:52:05 -05:00
Implement basic i18n
Not fully functional yet, so this won't/shouldn't be pushed onto the site yet (no language dropdown, there are still quite a few hardcoded strings that need to become string keys ...) but it works more than well enough to at least test and add more things as string keys that are currently hardcoded. There are two languages provided, "en-us" and "pirate". Pirate is a test language to make sure all string keys are applying properly and all that. As a bonus the new I18n system also offers a slightly less boneheaded way to do replacements, which is much more flexible than blind replace calls, and can take any type which is able to be stringified. The parsing code for this is far from my best work, but it'll do for now, and seems to work okay (and has basic idiot proofing), so eh. Supersedes #13.
This commit is contained in:
parent
7691b84073
commit
6327036283
10 changed files with 315 additions and 40 deletions
|
@ -1,5 +1,4 @@
|
|||
dist
|
||||
*.md
|
||||
*.json
|
||||
*.html
|
||||
*.css
|
|
@ -15,9 +15,7 @@
|
|||
<meta property="og:url" content="https://computernewb.com/collab-vm/"/>
|
||||
<meta property="og:description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
|
||||
<meta property="og:site_name" content="Computernewb"/>
|
||||
<!-- Using github for now until we have a canonical url -->
|
||||
<meta property="og:image" content="https://raw.githubusercontent.com/computernewb/collab-vm-1.2-webapp/master/dist/desktop.png"/>
|
||||
<!---->
|
||||
<meta property="og:image" content="https://computernewb.com/collab-vm/desktop.png"/>
|
||||
</head>
|
||||
<body class="bg-dark">
|
||||
<div class="modal fade" id="qemuMonitorModal" tabindex="-1" aria-hidden="true">
|
||||
|
@ -110,20 +108,20 @@
|
|||
</div>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">CollabVM</a>
|
||||
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> Home</a>
|
||||
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> <span id="homeBtnText"></span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://computernewb.com/collab-vm/faq/" class="nav-link"><i class="fa-solid fa-circle-question"></i> FAQ</a>
|
||||
<a href="https://computernewb.com/collab-vm/faq/" class="nav-link"><i class="fa-solid fa-circle-question"></i> <span id="faqBtnText"></span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://computernewb.com/collab-vm/rules" class="nav-link"><i class="fa-solid fa-clipboard-check"></i> Rules</a>
|
||||
<a href="https://computernewb.com/collab-vm/rules" class="nav-link"><i class="fa-solid fa-clipboard-check"></i> <span id="rulesBtnText"></span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://discord.gg/a4kqb4mGyX" class="nav-link"><i class="fa-brands fa-discord"></i> Discord</a>
|
||||
|
@ -148,20 +146,20 @@
|
|||
<div id="vmDisplay"></div>
|
||||
<p id="turnstatus" class="text-light"></p>
|
||||
<div id="voteResetPanel" class="bg-dark text-light" style="display:none;">
|
||||
Do you want to reset the vm?<br/>
|
||||
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> Yes<span class="badge bg-secondary" id="voteYesLabel"></span></button> <button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> No<span class="badge bg-secondary" id="voteNoLabel"></span></button><br/>
|
||||
Vote ends in <span id="votetime"></span> seconds<br/>
|
||||
<span id="voteResetHeaderText"></span><br/>
|
||||
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> <span id="voteYesBtnText"></span><span class="badge bg-secondary" id="voteYesLabel"></span></button> <button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> <span id="voteNoBtnText"></span><span class="badge bg-secondary" id="voteNoLabel"></span></button><br/>
|
||||
<span id="voteTimeText"></span>
|
||||
<div id="forceVotePanel">
|
||||
<button class="btn btn-info" id="forceVoteYesBtn"><i class="fa-solid fa-check"></i> Pass Vote</button>
|
||||
<button class="btn btn-info" id="forceVoteNoBtn"><i class="fa-solid fa-ban"></i> Cancel Vote</button>
|
||||
<button class="btn btn-info" id="forceVoteYesBtn"><i class="fa-solid fa-check"></i> <span id="passVoteBtnText"></span></button>
|
||||
<button class="btn btn-info" id="forceVoteNoBtn"><i class="fa-solid fa-ban"></i> <span id="cancelVoteBtnText"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="btns">
|
||||
<button class="btn btn-secondary" id="takeTurnBtn"><i class="fa-solid fa-computer-mouse"></i> <span id="turnBtnText"></span></button>
|
||||
<button class="btn btn-secondary" id="oskBtn"><i class="fa-solid fa-keyboard"></i> Keyboard</button>
|
||||
<button class="btn btn-secondary" id="changeUsernameBtn"><i class="fa-solid fa-signature"></i> Change Username</button>
|
||||
<button class="btn btn-secondary" id="voteResetButton"><i class="fa-solid fa-rotate-left"></i> Vote for Reset</button>
|
||||
<button class="btn btn-secondary" id="screenshotButton"><i class="fa-solid fa-camera"></i> Screenshot</button>
|
||||
<button class="btn btn-secondary" id="changeUsernameBtn"><i class="fa-solid fa-signature"></i> <span id="changeUsernameBtnText"></span></button>
|
||||
<button class="btn btn-secondary" id="voteResetButton"><i class="fa-solid fa-rotate-left"></i> <span id="voteForResetBtnText"></span></button>
|
||||
<button class="btn btn-secondary" id="screenshotButton"><i class="fa-solid fa-camera"></i> <span id="screenshotBtnText"></span></button>
|
||||
<button class="btn btn-secondary" id="ctrlAltDelBtn"><i class="fa-solid fa-gear"></i> Ctrl+Alt+Del</button>
|
||||
<div id="staffbtns">
|
||||
<button class="btn btn-secondary" id="restoreBtn"><i class="fa-solid fa-rotate-left"></i> Restore</button>
|
||||
|
@ -191,7 +189,7 @@
|
|||
<div class="table-responsive username-table">
|
||||
<table class="table table-hover table-dark table-borderless">
|
||||
<thead>
|
||||
<th><i class="fa-solid fa-user"></i> Users Online (<span id="onlineusercount"></span>)</th>
|
||||
<th><i class="fa-solid fa-user"></i> <span id="usersOnlineText"></span> (<span id="onlineusercount"></span>)</th>
|
||||
</thead>
|
||||
<tbody id="userlist"></tbody>
|
||||
</table>
|
||||
|
|
9
src/ts/StringLike.ts
Normal file
9
src/ts/StringLike.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
// TODO: `Object` has a toString(), but we should probably gate that off
|
||||
/// Interface for things that can be turned into strings
|
||||
export interface ToStringable {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/// A type for strings, or things that can (in a valid manner) be turned into strings
|
||||
export type StringLike = string | ToStringable;
|
209
src/ts/i18n.ts
Normal file
209
src/ts/i18n.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import { StringLike } from './StringLike';
|
||||
|
||||
// Nice little string key helper
|
||||
export enum I18nStringKey {
|
||||
kSiteName = 'kSiteName',
|
||||
kHomeButton = 'kHomeButton',
|
||||
kFAQButton = 'kFAQButton',
|
||||
kRulesButton = 'kRulesButton',
|
||||
kVMResetTitle = 'kVMResetTitle',
|
||||
kGenericYes = 'kGenericYes',
|
||||
kGenericNo = 'kGenericNo',
|
||||
kVMVoteTime = 'kVMVoteTime',
|
||||
kPassVoteButton = 'kPassVoteButton',
|
||||
kCancelVoteButton = 'kCancelVoteButton',
|
||||
kTakeTurnButton = 'kTakeTurnButton',
|
||||
kEndTurnButton = 'kEndTurnButton',
|
||||
kChangeUsernameButton = 'kChangeUsernameButton',
|
||||
kVoteButton = 'kVoteButton',
|
||||
kScreenshotButton = 'kScreenshotButton',
|
||||
kUsersOnlineHeading = 'kUsersOnlineHeading',
|
||||
kTurnTime = 'kTurnTime',
|
||||
kWaitingTurnTime = 'kWaitingTurnTime',
|
||||
kVoteCooldown = 'kVoteCooldown',
|
||||
kEnterNewUsername = 'kEnterNewUsername'
|
||||
}
|
||||
|
||||
// This models the JSON structure.
|
||||
export type Language = {
|
||||
languageName: string;
|
||||
translatedLanguageName: string;
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
// 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',
|
||||
author: 'Computernewb',
|
||||
|
||||
stringKeys: {
|
||||
kTitle: 'CollabVM',
|
||||
kHomeButton: 'Home',
|
||||
kFAQButton: 'FAQ',
|
||||
kRulesButton: 'Rules',
|
||||
kVMResetTitle: 'Do you want to reset the VM?',
|
||||
kGenericYes: 'Yes',
|
||||
kGenericNo: 'No',
|
||||
kVMVoteTime: 'Vote ends in {0} seconds',
|
||||
kPassVoteButton: 'Pass Vote',
|
||||
kCancelVoteButton: 'Cancel Vote',
|
||||
kTakeTurnButton: 'Take Turn',
|
||||
kEndTurnButton: 'End Turn',
|
||||
kChangeUsernameButton: 'Change Username',
|
||||
kVoteButton: 'Vote For Reset',
|
||||
kScreenshotButton: 'Screenshot',
|
||||
kUsersOnlineHeading: 'Users Online:',
|
||||
kTurnTime: 'Turn expires in {0} seconds.',
|
||||
kWaitingTurnTime: 'Waiting for turn in {0} seconds.',
|
||||
kVoteCooldown: 'Please wait {0} seconds before starting another vote.',
|
||||
kEnterNewUsername: 'Enter a new username, or leave the field blank to be assigned a guest username'
|
||||
}
|
||||
};
|
||||
|
||||
interface StringMap {
|
||||
[k: string]: string;
|
||||
}
|
||||
|
||||
/// our fancy internationalization helper.
|
||||
export class I18n {
|
||||
// The language data itself
|
||||
private lang: Language = fallbackLanguage;
|
||||
|
||||
// the ID of the language
|
||||
private langId: string = fallbackId;
|
||||
|
||||
async LoadLanguageFile(id: string) {
|
||||
let languageData = await I18n.LoadLanguageFileImpl(id);
|
||||
this.SetLanguage(languageData, id);
|
||||
}
|
||||
|
||||
async initWithLanguage(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 \"${id}\". Please tell a site admin this happened, and give them the following information: \"${(e as Error).message}\"`);
|
||||
// force set the language to fallback
|
||||
this.SetLanguage(fallbackLanguage, fallbackId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async LoadLanguageFileImpl(id: string): Promise<Language> {
|
||||
let path = `./lang/${id}.json`;
|
||||
let res = await fetch(path);
|
||||
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Replaces static strings that we don't recompute
|
||||
private ReplaceStaticStrings() {
|
||||
const kDomIdtoStringMap: StringMap = {
|
||||
siteNameText: I18nStringKey.kSiteName,
|
||||
homeBtnText: I18nStringKey.kHomeButton,
|
||||
faqBtnText: I18nStringKey.kFAQButton,
|
||||
rulesBtnText: I18nStringKey.kRulesButton,
|
||||
|
||||
usersOnlineText: I18nStringKey.kUsersOnlineHeading,
|
||||
|
||||
voteResetHeaderText: I18nStringKey.kVMResetTitle,
|
||||
voteYesBtnText: I18nStringKey.kGenericYes,
|
||||
voteNoBtnText: I18nStringKey.kGenericNo,
|
||||
|
||||
changeUsernameBtnText: I18nStringKey.kChangeUsernameButton,
|
||||
voteForResetBtnText: I18nStringKey.kVoteButton,
|
||||
screenshotBtnText: I18nStringKey.kScreenshotButton,
|
||||
|
||||
// admin stuff
|
||||
passVoteBtnText: I18nStringKey.kPassVoteButton,
|
||||
cancelVoteBtnText: I18nStringKey.kCancelVoteButton
|
||||
};
|
||||
|
||||
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.
|
||||
element.innerText = this.GetString(kDomIdtoStringMap[domId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a string, which also allows replacing by index with the given replacements.
|
||||
GetString(key: string, ...replacements: StringLike[]): string {
|
||||
let replacementStringArray: Array<string> = [...replacements].map((el) => {
|
||||
// This catches cases where the thing already is a string
|
||||
if (typeof el == 'string') return el as string;
|
||||
return el.toString();
|
||||
});
|
||||
|
||||
let val = this.lang.stringKeys[key];
|
||||
|
||||
if (val == null) {
|
||||
let fallback = fallbackLanguage.stringKeys[key];
|
||||
if (fallback == null) return 'UH OH WORM';
|
||||
else return fallback;
|
||||
}
|
||||
|
||||
for (let i = 0; i < val.length; ++i) {
|
||||
if (val[i] == '{') {
|
||||
let replacementStart = i;
|
||||
let foundReplacementEnd = false;
|
||||
|
||||
if (i + 1 > val.length) {
|
||||
throw new Error('Cutoff/invalid replacement');
|
||||
}
|
||||
|
||||
// Try and find the replacement end
|
||||
for (let j = i + 1; j < val.length; ++j) {
|
||||
if (val[j] == '}') {
|
||||
foundReplacementEnd = true;
|
||||
i = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundReplacementEnd) throw new Error('Invalid replacement, has no "}" to terminate it');
|
||||
|
||||
// Get the beginning and trailer
|
||||
let beginning = val.substring(0, replacementStart);
|
||||
let trailer = val.substring(replacementStart + 3);
|
||||
|
||||
let replacementIndex = parseInt(val.substring(replacementStart + 1, i));
|
||||
if (Number.isNaN(replacementIndex) || replacementIndex > replacementStringArray.length) throw new Error('Invalid replacement');
|
||||
|
||||
// This is seriously the only decent way to do this in javascript
|
||||
// thanks brendan eich (replace this thanking with more choice words in your head)
|
||||
val = beginning + replacementStringArray[replacementIndex] + trailer;
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
export let TheI18n = new I18n();
|
|
@ -11,6 +11,7 @@ import VoteStatus from './protocol/VoteStatus.js';
|
|||
import * as bootstrap from 'bootstrap';
|
||||
import MuteState from './protocol/MuteState.js';
|
||||
import { Unsubscribe } from 'nanoevents';
|
||||
import { I18nStringKey, TheI18n } from './i18n.js';
|
||||
|
||||
// Elements
|
||||
const w = window as any;
|
||||
|
@ -39,7 +40,7 @@ const elements = {
|
|||
voteNoBtn: document.getElementById('voteNoBtn') as HTMLButtonElement,
|
||||
voteYesLabel: document.getElementById('voteYesLabel') as HTMLSpanElement,
|
||||
voteNoLabel: document.getElementById('voteNoLabel') as HTMLSpanElement,
|
||||
votetime: document.getElementById('votetime') as HTMLSpanElement,
|
||||
voteTimeText: document.getElementById('voteTimeText') as HTMLSpanElement,
|
||||
loginModal: document.getElementById('loginModal') as HTMLDivElement,
|
||||
adminPassword: document.getElementById('adminPassword') as HTMLInputElement,
|
||||
loginButton: document.getElementById('loginButton') as HTMLButtonElement,
|
||||
|
@ -323,6 +324,7 @@ async function openVM(vm: VM): Promise<void> {
|
|||
unsubscribeCallbacks.push(VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename)));
|
||||
unsubscribeCallbacks.push(
|
||||
VM!.on('renamestatus', (status) => {
|
||||
// TODO: i18n these
|
||||
switch (status) {
|
||||
case 'taken':
|
||||
alert('That username is already taken');
|
||||
|
@ -339,7 +341,7 @@ async function openVM(vm: VM): Promise<void> {
|
|||
unsubscribeCallbacks.push(VM!.on('turn', (status) => turnUpdate(status)));
|
||||
unsubscribeCallbacks.push(VM!.on('vote', (status: VoteStatus) => voteUpdate(status)));
|
||||
unsubscribeCallbacks.push(VM!.on('voteend', () => voteEnd()));
|
||||
unsubscribeCallbacks.push(VM!.on('votecd', (cd) => window.alert(`Please wait ${cd} seconds before starting another vote.`)));
|
||||
unsubscribeCallbacks.push(VM!.on('votecd', (voteCooldown) => window.alert(TheI18n.GetString(I18nStringKey.kVoteCooldown, voteCooldown))));
|
||||
unsubscribeCallbacks.push(VM!.on('login', (rank: Rank, perms: Permissions) => onLogin(rank, perms)));
|
||||
unsubscribeCallbacks.push(
|
||||
VM!.on('close', () => {
|
||||
|
@ -554,7 +556,7 @@ function turnUpdate(status: TurnStatus) {
|
|||
user.element.classList.remove('user-turn', 'user-waiting');
|
||||
user.element.setAttribute('data-cvm-turn', '-1');
|
||||
}
|
||||
elements.turnBtnText.innerHTML = 'Take Turn';
|
||||
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kTakeTurnButton);
|
||||
enableOSK(false);
|
||||
|
||||
if (status.user !== null) {
|
||||
|
@ -570,14 +572,14 @@ function turnUpdate(status: TurnStatus) {
|
|||
if (status.user?.username === w.username) {
|
||||
turn = 0;
|
||||
turnTimer = status.turnTime! / 1000;
|
||||
elements.turnBtnText.innerHTML = 'End Turn';
|
||||
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kEndTurnButton);
|
||||
VM!.canvas.classList.add('focused');
|
||||
enableOSK(true);
|
||||
}
|
||||
if (status.queue.some((u) => u.username === w.username)) {
|
||||
turn = status.queue.findIndex((u) => u.username === w.username) + 1;
|
||||
turnTimer = status.queueTime! / 1000;
|
||||
elements.turnBtnText.innerHTML = 'End Turn';
|
||||
elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kEndTurnButton);
|
||||
VM!.canvas.classList.add('waiting');
|
||||
}
|
||||
if (turn === -1) elements.turnstatus.innerText = '';
|
||||
|
@ -600,7 +602,7 @@ function voteUpdate(status: VoteStatus) {
|
|||
|
||||
function updateVoteEndTime() {
|
||||
voteTimer--;
|
||||
elements.votetime.innerText = voteTimer.toString();
|
||||
elements.voteTimeText.innerText = TheI18n.GetString(I18nStringKey.kVMVoteTime, voteTimer);
|
||||
if (voteTimer === 0) clearInterval(voteInterval);
|
||||
}
|
||||
|
||||
|
@ -615,8 +617,8 @@ function turnIntervalCb() {
|
|||
}
|
||||
|
||||
function setTurnStatus() {
|
||||
if (turn === 0) elements.turnstatus.innerText = `Turn expires in ${turnTimer} seconds`;
|
||||
else elements.turnstatus.innerText = `Waiting for turn in ${turnTimer} seconds`;
|
||||
if (turn === 0) elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kTurnTime, turnTimer);
|
||||
else elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kWaitingTurnTime, turnTimer);
|
||||
}
|
||||
|
||||
function sendChat() {
|
||||
|
@ -635,7 +637,7 @@ elements.chatinput.addEventListener('keypress', (e) => {
|
|||
if (e.key === 'Enter') sendChat();
|
||||
});
|
||||
elements.changeUsernameBtn.addEventListener('click', () => {
|
||||
let newname = prompt('Enter new username, or leave blank to be assigned a guest username', w.username);
|
||||
let newname = prompt(TheI18n.GetString(I18nStringKey.kEnterNewUsername), w.username);
|
||||
if (newname === w.username) return;
|
||||
VM?.rename(newname);
|
||||
});
|
||||
|
@ -730,7 +732,7 @@ function userModOptions(user: { user: User; element: HTMLTableRowElement }) {
|
|||
td.setAttribute('aria-expanded', 'false');
|
||||
let ul = document.createElement('ul');
|
||||
ul.classList.add('dropdown-menu', 'dropdown-menu-dark', 'table-dark', 'text-light');
|
||||
if (perms.bypassturn) addUserDropdownItem(ul, 'End Turn', () => VM!.endTurn(user.user.username));
|
||||
if (perms.bypassturn) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kEndTurnButton), () => VM!.endTurn(user.user.username));
|
||||
if (perms.ban) addUserDropdownItem(ul, 'Ban', () => VM!.ban(user.user.username));
|
||||
if (perms.kick) addUserDropdownItem(ul, 'Kick', () => VM!.kick(user.user.username));
|
||||
if (perms.rename)
|
||||
|
@ -838,6 +840,16 @@ w.VMName = null;
|
|||
// Load all VMs
|
||||
loadList();
|
||||
|
||||
// Set a default internationalization language if not specified
|
||||
let lang = window.localStorage.getItem('i18n-lang');
|
||||
if (lang == null) {
|
||||
lang = 'en-us';
|
||||
window.localStorage.setItem('i18n-lang', lang);
|
||||
}
|
||||
|
||||
// Initalize the internationalization system
|
||||
TheI18n.initWithLanguage(lang);
|
||||
|
||||
// Welcome modal
|
||||
let noWelcomeModal = window.localStorage.getItem('no-welcome-modal');
|
||||
if (noWelcomeModal !== '1') {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createNanoEvents, Emitter, DefaultEvents } from 'nanoevents';
|
||||
import { createNanoEvents, Emitter, DefaultEvents, Unsubscribe } from 'nanoevents';
|
||||
import * as Guacutils from './Guacutils.js';
|
||||
import VM from './VM.js';
|
||||
import { User } from './User.js';
|
||||
|
@ -8,15 +8,7 @@ import Mouse from './mouse.js';
|
|||
import GetKeysym from '../keyboard.js';
|
||||
import VoteStatus from './VoteStatus.js';
|
||||
import MuteState from './MuteState.js';
|
||||
|
||||
// TODO: `Object` has a toString(), but we should probably gate that off
|
||||
/// Interface for things that can be turned into strings
|
||||
interface ToStringable {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/// A type for strings, or things that can (in a valid manner) be turned into strings
|
||||
type StringLike = string | ToStringable;
|
||||
import { StringLike } from '../StringLike.js';
|
||||
|
||||
export interface CollabVMClientEvents {
|
||||
open: () => void;
|
||||
|
@ -580,11 +572,11 @@ export default class CollabVMClient {
|
|||
this.send('admin', AdminOpcode.HideScreen, hidden ? '1' : '0');
|
||||
}
|
||||
|
||||
private onInternal<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]) {
|
||||
private onInternal<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
|
||||
return this.internalEmitter.on(event, callback);
|
||||
}
|
||||
|
||||
on<E extends keyof CollabVMClientEvents>(event: E, callback: CollabVMClientEvents[E]) {
|
||||
on<E extends keyof CollabVMClientEvents>(event: E, callback: CollabVMClientEvents[E]): Unsubscribe {
|
||||
return this.publicEmitter.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export function decode(string: string): string[] {
|
|||
export function encode(...string: string[]): string {
|
||||
let command = '';
|
||||
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
let current = string[i];
|
||||
command += current.toString().length + '.' + current;
|
||||
command += i < string.length - 1 ? ',' : ';';
|
||||
|
|
28
static/lang/en-us.json
Normal file
28
static/lang/en-us.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"languageName": "English (US)",
|
||||
"translatedLanguageName": "English (US)",
|
||||
"author": "Computernewb",
|
||||
|
||||
"stringKeys": {
|
||||
"kSiteName": "CollabVM",
|
||||
"kHomeButton": "Home",
|
||||
"kFAQButton": "FAQ",
|
||||
"kRulesButton": "Rules",
|
||||
"kVMResetTitle": "Do you want to reset the VM?",
|
||||
"kGenericYes": "Yes",
|
||||
"kGenericNo": "No",
|
||||
"kVMVoteTime": "Vote ends in {0} seconds",
|
||||
"kPassVoteButton": "Pass Vote",
|
||||
"kCancelVoteButton": "Cancel Vote",
|
||||
"kTakeTurnButton": "Take Turn",
|
||||
"kEndTurnButton": "End Turn",
|
||||
"kChangeUsernameButton": "Change Username",
|
||||
"kVoteButton": "Vote For Reset",
|
||||
"kScreenshotButton": "Screenshot",
|
||||
"kUsersOnlineHeading": "Users Online:",
|
||||
"kTurnTime": "Turn expires in {0} seconds.",
|
||||
"kWaitingTurnTime": "Waiting for turn in {0} seconds.",
|
||||
"kVoteCooldown": "Please wait {0} seconds before starting another vote.",
|
||||
"kEnterNewUsername": "Enter a new username, or leave the field blank to be assigned a guest username"
|
||||
}
|
||||
}
|
28
static/lang/pirate.json
Normal file
28
static/lang/pirate.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"languageName": "Pirate",
|
||||
"translatedLanguageName": "Arrrghh, matey!",
|
||||
"author": "Computernewb",
|
||||
|
||||
"stringKeys": {
|
||||
"kSiteName": "Argh, matey, it's CollabVM!",
|
||||
"kHomeButton": "Arrrghh, set sail",
|
||||
"kFAQButton": "Arrrgh, get your Map",
|
||||
"kRulesButton": "Arrrrghh, get the Etiquette Book",
|
||||
"kVMResetTitle": "Argh, matey, this VM is broken. Would you like to particpate in the VM War?",
|
||||
"kGenericYes": "Arrrghh, yay!",
|
||||
"kGenericNo": "Arrrrgh, nay!",
|
||||
"kVMVoteTime": "Argghh, the cannon fight ends in {0} seconds",
|
||||
"kPassVoteButton": "Rig the vote",
|
||||
"kCancelVoteButton": "Ignore the vote",
|
||||
"kTakeTurnButton": "Take the Wheel",
|
||||
"kEndTurnButton": "Give Up the Wheel",
|
||||
"kChangeUsernameButton": "Argh, select a new pirate name",
|
||||
"kVoteButton": "Argh, my matey, it's broken...",
|
||||
"kScreenshotButton": "Take a Polaroid",
|
||||
"kUsersOnlineHeading": "Pirate Friends Online:",
|
||||
"kTurnTime": "Arrrrgh, your wheel rights expire in {0} seconds.",
|
||||
"kWaitingTurnTime": "Waiting for the wheel in {0} seconds.",
|
||||
"kVoteCooldown": "Arrgh matey, you need to wait {0} seconds to vote again.",
|
||||
"kEnterNewUsername": "Arggghh, matey, what would you like to be known as?"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue