Implement typesafe-i18n

This commit is contained in:
Alexis Faizeau 2022-01-21 17:06:03 +01:00
parent 0be77164ec
commit 446b4639c7
97 changed files with 1162 additions and 1341 deletions

View file

@ -1,107 +1,76 @@
# How to translate WorkAdventure
## How the translation files work
In the `front/translations` folder, all json files of the form `[namespace].[language code].json` are interpreted to create languages by the language code in the file name.
The only mandatory file (the entry point) is the `index.[language code].json` which must contain the properties `language`, `country` and `default` so that the language can be taken into account.
Example:
```json
{
"language": "English", # Language that will be used
"country": "United States", # Country specification (e.g. British English and American English are not the same thing)
"default": true # In some cases WorkAdventure only knows the language, not the country language of the browser. In this case it takes the language with the default property at true.
}
```
It does not matter where the file is placed in this folder. However, we have set up the following architecture in order to have a simple reading:
- translations/
- [language code]/
- [namepace].[language code].json
Example:
- translations/
- en-US/
- index.en-US.json
- main-menu.en-US.json
- chat.en-US.json
- fr-FR/
- index.fr-FR.json
- main-menu.fr-FR.json
- chat.fr-FR.json
If a key isn't found then it will be searched in the fallback language and if it isn't found then the key will be returned. By default the fallback language is `en-US` but you can set another one with the `FALLBACK_LANGUAGE` environment variable.
We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation.
## Add a new language
It is very easy to add a new language!
First, in the translations folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)).
First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)).
In the previously created folder, add a file named as index.[language code].json with the following content:
In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example):
```json
{
"language": "Language Name",
"country": "Country Name",
"default": true
}
```ts
import type { Translation } from "../i18n-types";
const fr_FR: Translation = {
...en_US,
language: "Français",
country: "France",
};
export default fr_FR;
```
See the section above (*How the translation files work*) for more information on how this works.
BE CAREFUL if your language has variants by country don't forget to give attention to the default language used. If several languages are default the first one in the list will be used.
## Add a new key
### Add a simple key
The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object.
Please use [kebab-case](https://en.wikipedia.org/wiki/Letter_case#Kebab_case) to name your keys!
Please use kamelcase to name your keys!
Example:
```json
```ts
{
"messages": {
"coffe-machine":{
"start": "Coffe machine has been started!"
}
messages: {
coffeMachine: {
start: "Coffe machine has been started!";
}
}
}
```
In the code you can translate using `translator.translate`, or the shortcut alias: `_`.
In the code you can translate using `$LL`:
```js
import { _ } from "../../Translator/Translator";
```ts
import LL from "../../i18n/i18n-svelte";
console.log(_('messages.coffe-machine.start'));
console.log($LL.messages.coffeMachine.start());
```
### Add a key with parameters
You can also use parameters to make the translation dynamic.
Use the tag {{ [parameter name] }} to apply your parameters in the translations
Use the tag { [parameter name] } to apply your parameters in the translations
Example:
```json
```ts
{
"messages": {
"coffe-machine":{
"player-start": "{{ playerName }} started the coffee machine!"
}
messages: {
coffeMachine: {
playerStart: "{ playerName } started the coffee machine!";
}
}
}
```
In the code you can use it like this:
```js
_('messages.coffe-machine.player-start', {
playerName: "John"
```ts
$LL.messages.coffeMachine.playerStart.start({
playerName: "John",
});
```

View file

@ -0,0 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json",
"baseLocale": "en-US",
"adapter": "svelte"
}

View file

@ -21,7 +21,6 @@
"html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"merge-jsons-webpack-plugin": "^2.0.1",
"mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5",
@ -65,11 +64,12 @@
"socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4",
"ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0",
"uuidv4": "^6.2.10",
"zod": "^3.11.6"
},
"scripts": {
"start": "run-p templater serve svelte-check-watch",
"start": "run-p templater serve svelte-check-watch typesafe-i18n",
"templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@ -81,7 +81,8 @@
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'"
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n"
},
"lint-staged": {
"*.svelte": [

View file

@ -5,7 +5,7 @@
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { onDestroy, onMount } from "svelte";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
@ -145,7 +145,7 @@
</div>
<div class="audio-manager-reduce-conversation">
<label>
{_("audio.manager.reduce")}
{$LL.audio.manager.reduce()}
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label>
<section class="audio-manager-file">

View file

@ -5,7 +5,7 @@
import ChatElement from "./ChatElement.svelte";
import { afterUpdate, beforeUpdate, onMount } from "svelte";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let listDom: HTMLElement;
let chatWindowElement: HTMLElement;
@ -46,7 +46,7 @@
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">{_("chat.intro")}</p></li>
<li><p class="system-text">{$LL.chat.intro()}</p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement {message} line={i} /></li>
{/each}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
import { _ } from "../../Translator/Translator";
export const handleForm = {
blur() {
@ -28,7 +28,7 @@
<input
type="text"
bind:value={newMessageText}
placeholder={_("chat.enter")}
placeholder={$LL.chat.enter()}
on:focus={onFocus}
on:blur={onBlur}
bind:this={inputElement}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import { requestVisitCardsStore } from "../../Stores/GameStore";
import { _ } from "../../Translator/Translator";
export let player: PlayerInterface;
@ -15,10 +15,10 @@
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
<li>
<button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}
>{_("chat.menu.visit-card")}</button
>{$LL.chat.menu.visitCard()}</button
>
</li>
<li><button class="text-btn" disabled>{_("chat.menu.add-friend")}</button></li>
<li><button class="text-btn" disabled>{$LL.chat.menu.addFriend}</button></li>
</ul>
<style lang="scss">

View file

@ -2,7 +2,7 @@
import type { Game } from "../../Phaser/Game/Game";
import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
import { activeRowStore } from "../../Stores/CustomCharacterStore";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
@ -35,7 +35,7 @@
<form class="customCharacterScene">
<section class="text-center">
<h2>{_("custom-character.title")}</h2>
<h2>{$LL.woka.customWoka.title()}</h2>
</section>
<section class="action action-move">
<button
@ -54,12 +54,12 @@
<section class="action">
{#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
>{_("custom-character.navigation.return")}</button
>{$LL.woka.customWoka.navigation.return()}</button
>
{/if}
{#if $activeRowStore !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
>{_("custom-character.navigation.back")}
>{$LL.woka.customWoka.navigation.back()}
<img src="resources/objects/arrow_up_black.png" alt="" /></button
>
{/if}
@ -67,7 +67,7 @@
<button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={finish}>{_("custom-character.navigation.finish")}</button
on:click|preventDefault={finish}>{$LL.woka.customWoka.navigation.finish()}</button
>
{/if}
{#if $activeRowStore !== 5}
@ -75,7 +75,7 @@
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectDown}
>{_("custom-character.navigation.next")}
>{$LL.woka.customWoka.navigation.next()}
<img src="resources/objects/arrow_down.png" alt="" /></button
>
{/if}

View file

@ -13,7 +13,7 @@
import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
let selectedCamera: string | undefined = undefined;
@ -77,7 +77,7 @@
<form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center">
<h2>{_("camera.enable.title")}</h2>
<h2>{$LL.camera.enable.title()}</h2>
</section>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
@ -122,7 +122,7 @@
{/if}
</section>
<section class="action">
<button type="submit" class="nes-btn is-primary letsgo">{_("camera.enable.start")}</button>
<button type="submit" class="nes-btn is-primary letsgo">{$LL.camera.enable.start()}</button>
</section>
</form>

View file

@ -4,9 +4,8 @@ vim: ft=typescript
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
const gameScene = gameManager.getCurrentGameScene();
@ -44,14 +43,14 @@ vim: ft=typescript
{#if $followStateStore === "requesting" && $followRoleStore === "follower"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>{_("follow.interact-menu.title.follow", { leader: name($followUsersStore[0]) })}</h2>
<h2>{$LL.follow.interactMenu.title.follow({ leader: name($followUsersStore[0]) })}</h2>
</section>
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}
>{_("follow.interact-menu.yes")}</button
>{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}
>{_("follow.interact-menu.no")}</button
>{$LL.follow.interactMenu.no()}</button
>
</section>
</div>
@ -60,23 +59,23 @@ vim: ft=typescript
{#if $followStateStore === "ending"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>{_("follow.interact-menu.title.interact")}</h2>
<h2>{$LL.follow.interactMenu.title.interact()}</h2>
</section>
{#if $followRoleStore === "follower"}
<section class="interact-menu-question">
<p>{_("follow.interact-menu.stop.follower", { leader: name($followUsersStore[0]) })}</p>
<p>{$LL.follow.interactMenu.stop.follower({ leader: name($followUsersStore[0]) })}</p>
</section>
{:else if $followRoleStore === "leader"}
<section class="interact-menu-question">
<p>{_("follow.interact-menu.stop.leader")}</p>
<p>{$LL.follow.interactMenu.stop.leader()}</p>
</section>
{/if}
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}
>{_("follow.interact-menu.yes")}</button
>{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}
>{_("follow.interact-menu.no")}</button
>{$LL.follow.interactMenu.no()}</button
>
</section>
</div>
@ -86,21 +85,21 @@ vim: ft=typescript
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
{#if $followRoleStore === "follower"}
<p>{_("follow.interact-status.following", { leader: name($followUsersStore[0]) })}</p>
<p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0}
<p>{_("follow.interact-status.waiting-followers")}</p>
<p>{$LL.follow.interactStatus.waitingFollowers()}</p>
{:else if $followUsersStore.length === 1}
<p>{_("follow.interact-status.followed.one", { follower: name($followUsersStore[0]) })}</p>
<p>{$LL.follow.interactStatus.followed.one({ follower: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 2}
<p>
{_("follow.interact-status.followed.one", {
{$LL.follow.interactStatus.followed.two({
firstFollower: name($followUsersStore[0]),
secondFollower: name($followUsersStore[1]),
})}
</p>
{:else}
<p>
{_("follow.interact-status.followed.many", {
{$LL.follow.interactStatus.followed.many({
followers: $followUsersStore.slice(0, -1).map(name).join(", "),
lastFollower: name($followUsersStore[$followUsersStore.length - 1]),
})}

View file

@ -4,7 +4,7 @@
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox;
@ -25,13 +25,13 @@
transition:fly={{ y: -900, duration: 500 }}
>
<section>
<h2>{_("camera.help.title")}</h2>
<p class="err">{_("camera.help.permission-denied")}</p>
<p>{_("camera.help.content")}</p>
<h2>{$LL.camera.help.title()}</h2>
<p class="err">{$LL.camera.help.permissionDenied()}</p>
<p>{$LL.camera.help.content()}</p>
<p>
{#if isFirefox}
<p class="err">
{_("camera.help.firefox-content")}
{$LL.camera.help.firefoxContent()}
</p>
<img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid}
@ -41,10 +41,10 @@
</section>
<section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}
>{_("camera.help.refresh")}</button
>{$LL.camera.help.refresh()}</button
>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
>{_("camera.help.continue")}</button
>{$LL.camera.help.continue()}</button
>
</section>
</form>

View file

@ -4,7 +4,7 @@
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png";
import { gameManager } from "../../Phaser/Game/GameManager";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
export let game: Game;
@ -28,7 +28,7 @@
<img src={logoImg} alt="WorkAdventure logo" />
</section>
<section class="text-center">
<h2>{_("login.input.name.placeholder")}</h2>
<h2>{$LL.login.input.name.placeholder()}</h2>
</section>
<!-- svelte-ignore a11y-autofocus -->
<input
@ -45,7 +45,7 @@
/>
<section class="error-section">
{#if name.trim() === "" && startValidating}
<p class="err">{_("login.input.name.empty")}</p>
<p class="err">{$LL.login.input.name.empty()}</p>
{/if}
</section>
@ -53,12 +53,12 @@
<section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a>
<p>
{_("login.terms")}
{$LL.login.terms()}
</p>
</section>
{/if}
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">{_("login.continue")}</button>
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">{$LL.login.continue()}</button>
</section>
</form>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import { onMount } from "svelte";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let gameScene = gameManager.getCurrentGameScene();
@ -12,7 +12,7 @@
let mapName: string = "";
let mapLink: string = "";
let mapDescription: string = "";
let mapCopyright: string = _("menu.about.copyrights.map.empty");
let mapCopyright: string = $LL.menu.about.copyrights.map.empty();
let tilesetCopyright: string[] = [];
let audioCopyright: string[] = [];
@ -63,37 +63,37 @@
</script>
<div class="about-room-main">
<h2>{_("menu.about.map-info")}</h2>
<h2>{$LL.menu.about.mapInfo()}</h2>
<section class="container-overflow">
<h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p>
{#if mapLink}
<p class="string-HTML">
&gt; <a href={mapLink} target="_blank">{_("menu.about.map-link")}</a> &lt;
&gt; <a href={mapLink} target="_blank">{$LL.menu.about.mapLink()}</a> &lt;
</p>
{/if}
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
{_("menu.about.copyrights.map.title")}
{$LL.menu.about.copyrights.map.title()}
</h3>
<p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
{_("menu.about.copyrights.tileset.title")}
{$LL.menu.about.copyrights.tileset.title()}
</h3>
<section hidden={!expandedTilesetCopyright}>
{#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>{_("menu.about.copyrights.tileset.empty")}</p>
<p>{$LL.menu.about.copyrights.tileset.empty()}</p>
{/each}
</section>
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
{_("menu.about.copyrights.audio.title")}
{$LL.menu.about.copyrights.audio.title()}
</h3>
<section hidden={!expandedAudioCopyright}>
{#each audioCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>{_("menu.about.copyrights.audio.empty")}</p>
<p>{$LL.menu.about.copyrights.audio.empty()}</p>
{/each}
</section>
</section>

View file

@ -4,7 +4,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
@ -77,7 +77,7 @@
<img
class="nes-pointer"
src={uploadFile}
alt={_("menu.global-audio.upload-info")}
alt={$LL.menu.globalAudio.uploadInfo()}
on:click|preventDefault={() => {
fileInput.click();
}}
@ -86,7 +86,7 @@
<p>{fileName} : {fileSize}</p>
{/if}
{#if errorFile}
<p class="err">{_("menu.global-audio.error")}</p>
<p class="err">{$LL.menu.globalAudio.error()}</p>
{/if}
<input
type="file"

View file

@ -1,6 +1,6 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { contactPageStore } from "../../Stores/MenuStore";
import { _ } from "../../Translator/Translator";
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
@ -16,18 +16,18 @@
<div class="create-map-main">
<section class="container-overflow">
<section>
<h3>{_("menu.contact.getting-started.title")}</h3>
<p>{_("menu.contact.getting-started.description")}</p>
<h3>{$LL.menu.contact.gettingStarted.title()}</h3>
<p>{$LL.menu.contact.gettingStarted.description()}</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}
>{_("menu.contact.getting-started.title")}</button
>{$LL.menu.contact.gettingStarted.title()}</button
>
</section>
<section>
<h3>{_("menu.contact.create-map.title")}</h3>
<p>{_("menu.contact.create-map.description")}</p>
<h3>{$LL.menu.contact.createMap.title()}</h3>
<p>{$LL.menu.contact.createMap.description()}</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}
>{_("menu.contact.create-map.title")}</button
>{$LL.menu.contact.createMap.title()}</button
>
</section>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import TextGlobalMessage from "./TextGlobalMessage.svelte";
import AudioGlobalMessage from "./AudioGlobalMessage.svelte";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
@ -36,14 +36,14 @@
<button
type="button"
class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateInputText}>{_("menu.global-message.text")}</button
on:click|preventDefault={activateInputText}>{$LL.menu.globalMessage.text()}</button
>
</section>
<section>
<button
type="button"
class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateUploadAudio}>{_("menu.global-message.audio")}</button
on:click|preventDefault={activateUploadAudio}>{$LL.menu.globalMessage.audio()}</button
>
</section>
</div>
@ -58,10 +58,10 @@
<div class="global-message-footer">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} />
<span>{_("menu.global-message.warning")}</span>
<span>{$LL.menu.globalMessage.warning()}</span>
</label>
<section>
<button class="nes-btn is-primary" on:click|preventDefault={send}>{_("menu.global-message.send")}</button>
<button class="nes-btn is-primary" on:click|preventDefault={send}>{$LL.menu.globalMessage.send()}</button>
</section>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@ -23,14 +23,14 @@
<div class="guest-main">
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>{_("menu.invite.description")}</h3>
<h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{_("menu.invite.copy")}</button>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
<section class="is-mobile">
<h3>{_("menu.invite.description")}</h3>
<h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{_("menu.invite.share")}</button>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
</section>
</div>

View file

@ -14,27 +14,27 @@
SubMenusInterface,
subMenusStore,
} from "../../Stores/MenuStore";
import type { MenuItem } from "../../Stores/MenuStore";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let activeSubMenu: string = SubMenusInterface.profile;
let activeSubMenu: MenuItem = $subMenusStore[0];
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string; allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if (!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.profile);
if (!$subMenusStore.includes(activeSubMenu)) {
switchMenu($subMenusStore[0]);
}
});
checkSubMenuToShow();
switchMenu(SubMenusInterface.profile);
switchMenu($subMenusStore[0]);
});
onDestroy(() => {
@ -43,10 +43,10 @@
}
});
function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
function switchMenu(menu: MenuItem) {
if (menu.type === "translated") {
activeSubMenu = menu;
switch (menu) {
switch (menu.key) {
case SubMenusInterface.settings:
activeComponent = SettingsSubMenu;
break;
@ -65,19 +65,17 @@
case SubMenusInterface.contact:
activeComponent = ContactSubMenu;
break;
default: {
const customMenu = customMenuIframe.get(menu);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);
menuVisiblilityStore.set(false);
}
break;
}
}
} else throw new Error("There is no menu called " + menu);
} else {
const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu.label);
menuVisiblilityStore.set(false);
}
}
}
function closeMenu() {
@ -90,11 +88,15 @@
}
}
function translateMenuName(menuName: string) {
const nameFormatted = "menu.sub." + menuName.toLowerCase().replaceAll(" ", "-");
const translation = _(nameFormatted);
function translateMenuName(menu: MenuItem) {
if (menu.type === "scripting") {
return menu.label;
}
return translation === nameFormatted ? menuName : translation;
// Bypass the proxy of typesafe for getting the menu name : https://github.com/ivanhofer/typesafe-i18n/issues/156
const getMenuName = $LL.menu.sub[menu.key];
return getMenuName();
}
</script>
@ -102,7 +104,7 @@
<div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>{_("menu.title")}</h2>
<h2>{$LL.menu.title()}</h2>
<nav>
{#each $subMenusStore as submenu}
<button

View file

@ -9,7 +9,7 @@
import { get } from "svelte/store";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore));
@ -32,19 +32,19 @@
{#if $limitMapStore}
<img
src={logoInvite}
alt={_("menu.icon.open.invite")}
alt={$LL.menu.icon.open.invite()}
class="nes-pointer"
on:click|preventDefault={showInvite}
/>
<img
src={logoRegister}
alt={_("menu.icon.open.register")}
alt={$LL.menu.icon.open.register()}
class="nes-pointer"
on:click|preventDefault={register}
/>
{:else}
<img src={logoWA} alt={_("menu.icon.open.menu")} class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt={_("menu.icon.open.chat")} class="nes-pointer" on:click|preventDefault={showChat} />
<img src={logoWA} alt={$LL.menu.icon.open.menu()} class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt={$LL.menu.icon.open.chat()} class="nes-pointer" on:click|preventDefault={showChat} />
{/if}
</main>

View file

@ -17,7 +17,7 @@
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
function disableMenuStores() {
menuVisiblilityStore.set(false);
@ -63,20 +63,20 @@
<div class="submenu">
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt={_("menu.profile.edit.name")} />
<span class="btn-hover">{_("menu.profile.edit.name")}</span>
<img src={btnProfileSubMenuIdentity} alt={$LL.menu.profile.edit.name()} />
<span class="btn-hover">{$LL.menu.profile.edit.name()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">{_("menu.profile.edit.woka")}</span>
<span class="btn-hover">{$LL.menu.profile.edit.woka()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">{_("menu.profile.edit.companion")}</span>
<span class="btn-hover">{$LL.menu.profile.edit.companion()}</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt={_("menu.profile.edit.camera")} />
<span class="btn-hover">{_("menu.profile.edit.camera")}</span>
<img src={btnProfileSubMenuCamera} alt={$LL.menu.profile.edit.camera()} />
<span class="btn-hover">{$LL.menu.profile.edit.camera()}</span>
</button>
</section>
</div>
@ -90,12 +90,12 @@
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}
>{_("menu.profile.logout")}</button
>{$LL.menu.profile.logout()}</button
>
</section>
{:else}
<section>
<a type="button" class="nes-btn" href="/login">{_("menu.profile.login")}</a>
<a type="button" class="nes-btn" href="/login">{$LL.menu.profile.login()}</a>
</section>
{/if}
</div>

View file

@ -4,7 +4,9 @@
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
import { languages, translator, _ } from "../../Translator/Translator";
import LL, { locale } from "../../i18n/i18n-svelte";
import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
@ -12,18 +14,17 @@
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue();
let valueLanguage: string = translator.getStringByLanguage(translator.getCurrentLanguage()) ?? "en-US";
let valueLocale: string = $locale;
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
let previewValueLanguage = valueLanguage;
let previewValueLocale = valueLocale;
function saveSetting() {
let change = false;
if (valueLanguage !== previewValueLanguage) {
previewValueLanguage = valueLanguage;
translator.switchLanguage(previewValueLanguage);
change = true;
if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales);
}
if (valueVideo !== previewValueVideo) {
@ -88,74 +89,73 @@
<div class="settings-main" on:submit|preventDefault={saveSetting}>
<section>
<h3>{_("menu.settings.game-quality.title")}</h3>
<h3>{$LL.menu.settings.gameQuality.title()}</h3>
<div class="nes-select is-dark">
<select bind:value={valueGame}>
<option value={120}
>{isMobile()
? _("menu.settings.game-quality.short.high")
: _("menu.settings.game-quality.long.high")}</option
? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option
>
<option value={60}
>{isMobile()
? _("menu.settings.game-quality.short.medium")
: _("menu.settings.game-quality.long.medium")}</option
? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option
>
<option value={40}
>{isMobile()
? _("menu.settings.game-quality.short.minimum")
: _("menu.settings.game-quality.long.minimum")}</option
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
<option value={20}
>{isMobile()
? _("menu.settings.game-quality.short.small")
: _("menu.settings.game-quality.long.small")}</option
? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option
>
</select>
</div>
</section>
<section>
<h3>{_("menu.settings.video-quality.title")}</h3>
<h3>{$LL.menu.settings.videoQuality.title()}</h3>
<div class="nes-select is-dark">
<select bind:value={valueVideo}>
<option value={30}
>{isMobile()
? _("menu.settings.video-quality.short.high")
: _("menu.settings.video-quality.long.high")}</option
? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option
>
<option value={20}
>{isMobile()
? _("menu.settings.video-quality.short.medium")
: _("menu.settings.video-quality.long.medium")}</option
? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option
>
<option value={10}
>{isMobile()
? _("menu.settings.video-quality.short.minimum")
: _("menu.settings.video-quality.long.minimum")}</option
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
<option value={5}
>{isMobile()
? _("menu.settings.video-quality.short.small")
: _("menu.settings.video-quality.long.small")}</option
? $LL.menu.settings.videoQuality.short.small()
: $LL.menu.settings.videoQuality.long.small()}</option
>
</select>
</div>
</section>
<section>
<h3>{_("menu.settings.language.title")}</h3>
<h3>{$LL.menu.settings.language.title()}</h3>
<div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLanguage}>
<!-- svelte-ignore missing-declaration -->
{#each languages as language}
<option value={language.id}>{`${language.language} (${language.country})`}</option>
<select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option>
{/each}
</select>
</div>
</section>
<section class="settings-section-save">
<p>{_("menu.settings.save.warning")}</p>
<p>{$LL.menu.settings.save.warning()}</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}
>{_("menu.settings.save.button")}</button
>{$LL.menu.settings.save.button()}</button
>
</section>
<section class="settings-section-noSaveOption">
@ -166,7 +166,7 @@
bind:checked={fullscreen}
on:change={changeFullscreen}
/>
<span>{_("menu.settings.fullscreen")}</span>
<span>{$LL.menu.settings.fullscreen()}</span>
</label>
<label>
<input
@ -175,7 +175,7 @@
bind:checked={notification}
on:change={changeNotification}
/>
<span>{_("menu.settings.notifications")}</span>
<span>{$LL.menu.settings.notifications()}</span>
</label>
<label>
<input
@ -184,7 +184,7 @@
bind:checked={forceCowebsiteTrigger}
on:change={changeForceCowebsiteTrigger}
/>
<span>{_("menu.settings.cowebsite-trigger")}</span>
<span>{$LL.menu.settings.cowebsiteTrigger()}</span>
</label>
<label>
<input
@ -193,7 +193,7 @@
bind:checked={ignoreFollowRequests}
on:change={changeIgnoreFollowRequests}
/>
<span>{_("menu.settings.ignore-follow-request")}</span>
<span>{$LL.menu.settings.ignoreFollowRequest()}</span>
</label>
</section>
</div>

View file

@ -5,7 +5,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
//toolbar
const toolbarOptions = [
@ -59,7 +59,7 @@
const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(QUILL_EDITOR, {
placeholder: _("menu.global-message.enter"),
placeholder: $LL.menu.globalMessage.enter(),
theme: "snow",
modules: {
toolbar: toolbarOptions,

View file

@ -4,7 +4,7 @@
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte";
import { srcObject } from "./Video/utils";
import { _ } from "../Translator/Translator";
import LL from "../i18n/i18n-svelte";
let stream: MediaStream | null;
@ -33,5 +33,5 @@
<SoundMeterWidget {stream} />
{/if}
</div>
<div class="is-silent" class:hide={isSilent}>{_("camera.my.silent-zone")}</div>
<div class="is-silent" class:hide={isSilent}>{$LL.camera.my.silentZone()}</div>
</div>

View file

@ -2,7 +2,7 @@
import { blackListManager } from "../../WebRtc/BlackListManager";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { onMount } from "svelte";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined;
export let userName: string;
@ -30,10 +30,10 @@
</script>
<div class="block-container">
<h3>{_("report.block.title")}</h3>
<p>{_("report.block.content", { userName })}</p>
<h3>{$LL.report.block.title()}</h3>
<p>{$LL.report.block.content({ userName })}</p>
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
{userIsBlocked ? _("report.block.unblock") : _("report.block.block")}
{userIsBlocked ? $LL.report.block.unblock() : $LL.report.block.block()}
</button>
</div>

View file

@ -7,7 +7,7 @@
import { playersStore } from "../../Stores/PlayersStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { get } from "svelte/store";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
let blockActive = true;
let reportActive = !blockActive;
@ -60,7 +60,7 @@
<div class="report-menu-main nes-container is-rounded">
<section class="report-menu-title">
<h2>{_("moderate.title", { userName })}</h2>
<h2>{$LL.report.moderate.title({ userName })}</h2>
<section class="justify-center">
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
</section>
@ -70,14 +70,14 @@
<button
type="button"
class="nes-btn {blockActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateBlock}>{_("moderate.block")}</button
on:click|preventDefault={activateBlock}>{$LL.report.moderate.block()}</button
>
</section>
<section class="justify-center">
<button
type="button"
class="nes-btn {reportActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateReport}>{_("moderate.report")}</button
on:click|preventDefault={activateReport}>{$LL.report.moderate.report()}</button
>
</section>
</section>
@ -87,7 +87,7 @@
{:else if reportActive}
<ReportSubMenu {userUUID} />
{:else}
<p>{_("moderate.no-select")}</p>
<p>{$LL.report.moderate.noSelect()}</p>
{/if}
</section>
</div>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager";
import { _ } from "../../Translator/Translator";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined;
let reportMessage: string;
@ -23,18 +23,18 @@
</script>
<div class="report-container-main">
<h3>{_("report.title")}</h3>
<p>{_("report.message")}</p>
<h3>{$LL.report.title()}</h3>
<p