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 to translate WorkAdventure
## How the translation files work We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation.
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.
## Add a new language ## Add a new language
It is very easy to 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 ```ts
{ import type { Translation } from "../i18n-types";
"language": "Language Name",
"country": "Country Name", const fr_FR: Translation = {
"default": true ...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 new key
### Add a simple 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. 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: Example:
```json ```ts
{ {
"messages": { messages: {
"coffe-machine":{ coffeMachine: {
"start": "Coffe machine has been started!" 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 ```ts
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
console.log(_('messages.coffe-machine.start')); console.log($LL.messages.coffeMachine.start());
``` ```
### Add a key with parameters ### Add a key with parameters
You can also use parameters to make the translation dynamic. 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: Example:
```json ```ts
{ {
"messages": { messages: {
"coffe-machine":{ coffeMachine: {
"player-start": "{{ playerName }} started the coffee machine!" playerStart: "{ playerName } started the coffee machine!";
}
} }
}
} }
``` ```
In the code you can use it like this: In the code you can use it like this:
```js ```ts
_('messages.coffe-machine.player-start', { $LL.messages.coffeMachine.playerStart.start({
playerName: "John" 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", "html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"merge-jsons-webpack-plugin": "^2.0.1",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2", "node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
@ -65,11 +64,12 @@
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0",
"uuidv4": "^6.2.10", "uuidv4": "^6.2.10",
"zod": "^3.11.6" "zod": "^3.11.6"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch", "start": "run-p templater serve svelte-check-watch typesafe-i18n",
"templater": "cross-env ./templater.sh", "templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "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", "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-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\"", "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": "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": { "lint-staged": {
"*.svelte": [ "*.svelte": [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
function copyLink() { function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@ -23,14 +23,14 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> <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()} /> <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>
<section class="is-mobile"> <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()} /> <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>
</section> </section>
</div> </div>

View file

@ -14,27 +14,27 @@
SubMenusInterface, SubMenusInterface,
subMenusStore, subMenusStore,
} from "../../Stores/MenuStore"; } from "../../Stores/MenuStore";
import type { MenuItem } from "../../Stores/MenuStore";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem"; 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 activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string; allowApi: boolean }; let props: { url: string; allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber; let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => { onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => { unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if (!get(subMenusStore).includes(activeSubMenu)) { if (!$subMenusStore.includes(activeSubMenu)) {
switchMenu(SubMenusInterface.profile); switchMenu($subMenusStore[0]);
} }
}); });
checkSubMenuToShow(); checkSubMenuToShow();
switchMenu(SubMenusInterface.profile); switchMenu($subMenusStore[0]);
}); });
onDestroy(() => { onDestroy(() => {
@ -43,10 +43,10 @@
} }
}); });
function switchMenu(menu: string) { function switchMenu(menu: MenuItem) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) { if (menu.type === "translated") {
activeSubMenu = menu; activeSubMenu = menu;
switch (menu) { switch (menu.key) {
case SubMenusInterface.settings: case SubMenusInterface.settings:
activeComponent = SettingsSubMenu; activeComponent = SettingsSubMenu;
break; break;
@ -65,19 +65,17 @@
case SubMenusInterface.contact: case SubMenusInterface.contact:
activeComponent = ContactSubMenu; activeComponent = ContactSubMenu;
break; 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() { function closeMenu() {
@ -90,11 +88,15 @@
} }
} }
function translateMenuName(menuName: string) { function translateMenuName(menu: MenuItem) {
const nameFormatted = "menu.sub." + menuName.toLowerCase().replaceAll(" ", "-"); if (menu.type === "scripting") {
const translation = _(nameFormatted); 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> </script>
@ -102,7 +104,7 @@
<div class="menu-container-main"> <div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}> <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> <nav>
{#each $subMenusStore as submenu} {#each $subMenusStore as submenu}
<button <button

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
import SoundMeterWidget from "./SoundMeterWidget.svelte"; import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { srcObject } from "./Video/utils"; import { srcObject } from "./Video/utils";
import { _ } from "../Translator/Translator"; import LL from "../i18n/i18n-svelte";
let stream: MediaStream | null; let stream: MediaStream | null;
@ -33,5 +33,5 @@
<SoundMeterWidget {stream} /> <SoundMeterWidget {stream} />
{/if} {/if}
</div> </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> </div>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined; export let userUUID: string | undefined;
let reportMessage: string; let reportMessage: string;
@ -23,18 +23,18 @@
</script> </script>
<div class="report-container-main"> <div class="report-container-main">
<h3>{_("report.title")}</h3> <h3>{$LL.report.title()}</h3>
<p>{_("report.message")}</p> <p>{$LL.report.content()}</p>
<form> <form>
<section> <section>
<label> <label>
<span>{_("report.message.title")}</span> <span>{$LL.report.message.title()}</span>
<textarea type="text" class="nes-textarea" bind:value={reportMessage} /> <textarea type="text" class="nes-textarea" bind:value={reportMessage} />
</label> </label>
<p hidden={hiddenError}>{_("report.message.empty")}</p> <p hidden={hiddenError}>{$LL.report.message.empty()}</p>
</section> </section>
<section> <section>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>{_("report.submit")}</button> <button type="submit" class="nes-btn is-error" on:click={submitReport}>{$LL.report.submit()}</button>
</section> </section>
</form> </form>
</div> </div>

View file

@ -1,7 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import LL from "../../i18n/i18n-svelte";
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
import { _ } from "../../Translator/Translator";
export let game: Game; export let game: Game;
@ -26,7 +26,7 @@
<form class="selectCompanionScene"> <form class="selectCompanionScene">
<section class="text-center"> <section class="text-center">
<h2>{_("companion.select.title")}</h2> <h2>{$LL.companion.select.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt; &lt;
</button> </button>
@ -36,12 +36,12 @@
</section> </section>
<section class="action"> <section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion} <button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
>{_("companion.select.any")}</button >{$LL.companion.select.any()}</button
> >
<button <button
type="submit" type="submit"
class="selectCompanionSceneFormSubmit nes-btn is-primary" class="selectCompanionSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectCompanion}>{_("companion.select.continue")}</button on:click|preventDefault={selectCompanion}>{$LL.companion.select.continue()}</button
> >
</section> </section>
</form> </form>

View file

@ -3,7 +3,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore"; import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore"; import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
export let message: Message; export let message: Message;
@ -39,7 +39,7 @@
> >
<h2 class="title-ban-message"> <h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> <img src="resources/logos/report.svg" alt="***" />
{_("important-message")} {$LL.warning.importantMessage()}
<img src="resources/logos/report.svg" alt="***" /> <img src="resources/logos/report.svg" alt="***" />
</h2> </h2>
<div class="content-ban-message"> <div class="content-ban-message">

View file

@ -3,7 +3,7 @@
import megaphoneImg from "./images/megaphone.svg"; import megaphoneImg from "./images/megaphone.svg";
import { soundPlayingStore } from "../../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
import { afterUpdate } from "svelte"; import { afterUpdate } from "svelte";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
export let url: string; export let url: string;
let audio: HTMLAudioElement; let audio: HTMLAudioElement;
@ -19,7 +19,7 @@
<div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}> <div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
<img src={megaphoneImg} alt="Audio playing" /> <img src={megaphoneImg} alt="Audio playing" />
<p>{_("audio.message")}</p> <p>{$LL.audio.message()}</p>
<audio bind:this={audio} src={url} on:ended={soundEnded}> <audio bind:this={audio} src={url} on:ended={soundEnded}>
<track kind="captions" /> <track kind="captions" />
</audio> </audio>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore"; import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
function close(): boolean { function close(): boolean {
errorStore.clearClosableMessages(); errorStore.clearClosableMessages();
@ -9,7 +9,7 @@
</script> </script>
<div class="error-div nes-container is-dark is-rounded" open> <div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">{_("error.error")}</p> <p class="nes-text is-error title">{$LL.error.error()}</p>
<div class="body"> <div class="body">
{#each $errorStore as error} {#each $errorStore as error}
<p>{error.message}</p> <p>{error.message}</p>

View file

@ -2,7 +2,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { requestVisitCardsStore } from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
export let visitCardUrl: string; export let visitCardUrl: string;
let w = "500px"; let w = "500px";
@ -41,7 +41,7 @@
/> />
{#if !hidden} {#if !hidden}
<div class="buttonContainer"> <div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>{_("menu.visit-card.close")}</button> <button class="nes-btn is-popUpElement" on:click={closeCard}>{$LL.menu.visitCard.close()}</button>
</div> </div>
{/if} {/if}
</section> </section>

View file

@ -2,7 +2,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore"; import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
const upgradeLink = ADMIN_URL + "/pricing"; const upgradeLink = ADMIN_URL + "/pricing";
const registerLink = ADMIN_URL + "/second-step-register"; const registerLink = ADMIN_URL + "/second-step-register";
@ -10,17 +10,17 @@
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}> <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
{#if $userIsAdminStore} {#if $userIsAdminStore}
<h2>{_("warning.title")}</h2> <h2>{$LL.warning.title()}</h2>
<p> <p>
{_("warning.content", { upgradeLink })} {$LL.warning.content({ upgradeLink })}
</p> </p>
{:else if $limitMapStore} {:else if $limitMapStore}
<p> <p>
This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>! This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
</p> </p>
{:else} {:else}
<h2>{_("warning.title")}</h2> <h2>{$LL.warning.title()}</h2>
<p>{_("warning.limit")}</p> <p>{$LL.warning.limit()}</p>
{/if} {/if}
</main> </main>

View file

@ -1,7 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import { _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
export let game: Game; export let game: Game;
@ -26,7 +26,7 @@
<form class="selectCharacterScene"> <form class="selectCharacterScene">
<section class="text-center"> <section class="text-center">
<h2>{_("select-woka.title")}</h2> <h2>{$LL.woka.selectWoka.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt; &lt;
</button> </button>
@ -38,12 +38,12 @@
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary" class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>{_("select-woka.continue")}</button on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
> >
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>{_("select-woka.customize")}</button on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
> >
</section> </section>
</form> </form>

View file

@ -1,7 +1,8 @@
import axios from "axios"; import axios from "axios";
import * as rax from "retry-axios"; import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore"; import { errorStore } from "../Stores/ErrorStore";
import { _ } from "../Translator/Translator"; import LL from "../i18n/i18n-svelte";
import { get } from "svelte/store";
/** /**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay. * This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
@ -27,13 +28,10 @@ axiosWithRetry.defaults.raxConfig = {
console.log(err); console.log(err);
console.log(cfg); console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`); console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage( errorStore.addErrorMessage(get(LL).error.connectionRetry.unableConnect(), {
_("error.connection-retry.unable-to-connect-to-workAdventure-are-you-connected-to-internet"), closable: false,
{ id: "axios_retry",
closable: false, });
id: "axios_retry",
}
);
}, },
}; };

View file

@ -25,7 +25,7 @@ export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) |
export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; export const POSTHOG_URL = process.env.POSTHOG_URL || undefined;
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true"; export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER;
const FALLBACK_LANGUAGE: string = process.env.FALLBACK_LANGUAGE || "en-US"; const FALLBACK_LOCALE = process.env.FALLBACK_LOCALE || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
@ -45,5 +45,5 @@ export {
TURN_PASSWORD, TURN_PASSWORD,
JITSI_URL, JITSI_URL,
JITSI_PRIVATE_MODE, JITSI_PRIVATE_MODE,
FALLBACK_LANGUAGE, FALLBACK_LOCALE,
}; };

View file

@ -76,7 +76,6 @@ import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore"; import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { translator } from "../../Translator/Translator";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
@ -92,6 +91,7 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore"; import { followUsersColorStore } from "../../Stores/FollowStore";
import Camera = Phaser.Cameras.Scene2D.Camera; import Camera = Phaser.Cameras.Scene2D.Camera;
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -1322,7 +1322,7 @@ ${escapedMessage}
startLayerName: this.startPositionCalculator.startLayerName, startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid, uuid: localUserStore.getLocalUser()?.uuid,
nickname: this.playerName, nickname: this.playerName,
language: translator.getStringByLanguage(translator.getCurrentLanguage()), language: get(locale),
roomId: this.roomUrl, roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [], tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables, variables: this.sharedVariablesManager.variables,

View file

@ -4,7 +4,11 @@ import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError"; import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
import { translator, _ } from "../../Translator/Translator"; import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store";
import { localeDetector } from "../../i18n/locales";
const $LL = get(LL);
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -25,16 +29,11 @@ export class EntryScene extends Scene {
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml"); this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 }); this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
translator.loadCurrentLanguageFile(this.load);
} }
create() { create() {
translator localeDetector()
.loadCurrentLanguageObject(this.cache) .then(() => {
.catch((e: unknown) => {
console.error("Error during language loading!", e);
})
.finally(() => {
gameManager gameManager
.init(this.scene) .init(this.scene)
.then((nextSceneName) => { .then((nextSceneName) => {
@ -47,20 +46,20 @@ export class EntryScene extends Scene {
if (err.response && err.response.status == 404) { if (err.response && err.response.status == 404) {
ErrorScene.showError( ErrorScene.showError(
new WAError( new WAError(
_("error.access-link.title"), $LL.error.accessLink.title(),
_("error.access-link.sub-title"), $LL.error.accessLink.subTitle(),
_("error.access-link.details") $LL.error.accessLink.details()
), ),
this.scene this.scene
); );
} else if (err.response && err.response.status == 403) { } else if (err.response && err.response.status == 403) {
ErrorScene.showError( ErrorScene.showError(
new WAError( new WAError(
_("error.connection-rejected.title"), $LL.error.connectionRejected.title(),
_("error.connection-rejected.sub-title", { $LL.error.connectionRejected.subTitle({
error: err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "", error: err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "",
}), }),
_("error.connection-rejected.details") $LL.error.connectionRejected.details()
), ),
this.scene this.scene
); );
@ -68,6 +67,9 @@ export class EntryScene extends Scene {
ErrorScene.showError(err, this.scene); ErrorScene.showError(err, this.scene);
} }
}); });
})
.catch(() => {
throw new Error("Cannot load locale!");
}); });
} }
} }

View file

@ -1,7 +1,8 @@
import { _ } from "../../Translator/Translator";
import { TextField } from "../Components/TextField"; import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image; import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store";
export const ReconnectingSceneName = "ReconnectingScene"; export const ReconnectingSceneName = "ReconnectingScene";
export enum ReconnectingTextures { export enum ReconnectingTextures {
@ -39,7 +40,7 @@ export class ReconnectingScene extends Phaser.Scene {
this, this,
this.game.renderer.width / 2, this.game.renderer.width / 2,
this.game.renderer.height / 2, this.game.renderer.height / 2,
_("connection-lost") get(LL).warning.connectionLost()
); );
const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat"); const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");

View file

@ -3,6 +3,7 @@ import Timeout = NodeJS.Timeout;
import { userIsAdminStore } from "./GameStore"; import { userIsAdminStore } from "./GameStore";
import { CONTACT_URL } from "../Enum/EnvironmentVariable"; import { CONTACT_URL } from "../Enum/EnvironmentVariable";
import { analyticsClient } from "../Administration/AnalyticsClient"; import { analyticsClient } from "../Administration/AnalyticsClient";
import type { Translation } from "../i18n/i18n-types";
export const menuIconVisiblilityStore = writable(false); export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false); export const menuVisiblilityStore = writable(false);
@ -32,37 +33,95 @@ function createWarningContainerStore() {
export const warningContainerStore = createWarningContainerStore(); export const warningContainerStore = createWarningContainerStore();
export enum SubMenusInterface { export enum SubMenusInterface {
settings = "Settings", settings = "settings",
profile = "Profile", profile = "profile",
invite = "Invite", invite = "invite",
aboutRoom = "Credit", aboutRoom = "credit",
globalMessages = "Global Messages", globalMessages = "globalMessages",
contact = "Contact", contact = "contact",
} }
type MenuKeys = keyof Translation["menu"]["sub"];
interface TranslatedMenu {
type: "translated";
key: MenuKeys;
}
/**
* A menu item from the scripting API
*/
interface ScriptingMenu {
type: "scripting";
label: string;
}
export type MenuItem = TranslatedMenu | ScriptingMenu;
function createSubMenusStore() { function createSubMenusStore() {
const { subscribe, update } = writable<string[]>([ const { subscribe, update } = writable<MenuItem[]>([
SubMenusInterface.profile, {
SubMenusInterface.globalMessages, type: "translated",
SubMenusInterface.contact, key: SubMenusInterface.profile,
SubMenusInterface.settings, },
SubMenusInterface.invite, {
SubMenusInterface.aboutRoom, type: "translated",
key: SubMenusInterface.globalMessages,
},
{
type: "translated",
key: SubMenusInterface.contact,
},
{
type: "translated",
key: SubMenusInterface.settings,
},
{
type: "translated",
key: SubMenusInterface.invite,
},
{
type: "translated",
key: SubMenusInterface.aboutRoom,
},
]); ]);
return { return {
subscribe, subscribe,
addMenu(menuCommand: string) { addTranslatedMenu(menuCommand: MenuKeys) {
update((menuList: string[]) => { update((menuList) => {
if (!menuList.find((menu) => menu === menuCommand)) { if (!menuList.find((menu) => menu.type === "translated" && menu.key === menuCommand)) {
menuList.push(menuCommand); menuList.push({
type: "translated",
key: menuCommand,
});
} }
return menuList; return menuList;
}); });
}, },
removeMenu(menuCommand: string) { removeTranslatedMenu(menuCommand: MenuKeys) {
update((menuList: string[]) => { update((menuList) => {
const index = menuList.findIndex((menu) => menu === menuCommand); const index = menuList.findIndex((menu) => menu.type === "translated" && menu.key === menuCommand);
if (index !== -1) {
menuList.splice(index, 1);
}
return menuList;
});
},
addScriptingMenu(menuCommand: string) {
update((menuList) => {
if (!menuList.find((menu) => menu.type === "scripting" && menu.label === menuCommand)) {
menuList.push({
type: "scripting",
label: menuCommand,
});
}
return menuList;
});
},
removeScriptingMenu(menuCommand: string) {
update((menuList) => {
const index = menuList.findIndex((menu) => menu.type === "scripting" && menu.label === menuCommand);
if (index !== -1) { if (index !== -1) {
menuList.splice(index, 1); menuList.splice(index, 1);
} }
@ -77,15 +136,15 @@ export const subMenusStore = createSubMenusStore();
export const contactPageStore = writable<string | undefined>(CONTACT_URL); export const contactPageStore = writable<string | undefined>(CONTACT_URL);
export function checkSubMenuToShow() { export function checkSubMenuToShow() {
subMenusStore.removeMenu(SubMenusInterface.globalMessages); subMenusStore.removeTranslatedMenu(SubMenusInterface.globalMessages);
subMenusStore.removeMenu(SubMenusInterface.contact); subMenusStore.removeTranslatedMenu(SubMenusInterface.contact);
if (get(userIsAdminStore)) { if (get(userIsAdminStore)) {
subMenusStore.addMenu(SubMenusInterface.globalMessages); subMenusStore.addTranslatedMenu(SubMenusInterface.globalMessages);
} }
if (get(contactPageStore) !== undefined) { if (get(contactPageStore) !== undefined) {
subMenusStore.addMenu(SubMenusInterface.contact); subMenusStore.addTranslatedMenu(SubMenusInterface.contact);
} }
} }
@ -97,12 +156,12 @@ export function handleMenuRegistrationEvent(
source: string | undefined = undefined, source: string | undefined = undefined,
options: { allowApi: boolean } options: { allowApi: boolean }
) { ) {
if (get(subMenusStore).includes(menuName)) { if (get(subMenusStore).find((item) => item.type === "scripting" && item.label === menuName)) {
console.warn("The menu " + menuName + " already exist."); console.warn("The menu " + menuName + " already exist.");
return; return;
} }
subMenusStore.addMenu(menuName); subMenusStore.addScriptingMenu(menuName);
if (iframeUrl !== undefined) { if (iframeUrl !== undefined) {
const url = new URL(iframeUrl, source); const url = new URL(iframeUrl, source);
@ -111,12 +170,6 @@ export function handleMenuRegistrationEvent(
} }
export function handleMenuUnregisterEvent(menuName: string) { export function handleMenuUnregisterEvent(menuName: string) {
const subMenuGeneral: string[] = Object.values(SubMenusInterface); subMenusStore.removeScriptingMenu(menuName);
if (subMenuGeneral.includes(menuName)) {
console.warn("The menu " + menuName + " is a mandatory menu. It can't be remove");
return;
}
subMenusStore.removeMenu(menuName);
customMenuIframe.delete(menuName); customMenuIframe.delete(menuName);
} }

View file

@ -1,93 +0,0 @@
import fs from "fs";
import { z } from "zod";
export type LanguageFound = {
id: string;
language: string;
country: string;
default: boolean;
};
type LanguageObject = {
[key: string]: string | boolean | LanguageObject;
};
const translationsBasePath = "./translations";
const fallbackLanguage = process.env.FALLBACK_LANGUAGE || "en-US";
const getAllLanguagesByFiles = (dirPath: string, languages: Array<LanguageFound> | undefined) => {
const files = fs.readdirSync(dirPath);
languages = languages || new Array<LanguageFound>();
files.forEach(function (file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
languages = getAllLanguagesByFiles(dirPath + "/" + file, languages);
} else {
const parts = file.split(".");
if (parts.length !== 3 || parts[0] !== "index" || parts[2] !== "json") {
return;
}
const rawData = fs.readFileSync(dirPath + "/" + file, "utf-8");
const languageObject = JSON.parse(rawData);
const indexLanguageObject = z.object({
language: z.string(),
country: z.string(),
default: z.boolean(),
});
try {
const indexLanguage = indexLanguageObject.parse(languageObject);
languages?.push({
id: parts[1],
language: indexLanguage.language,
country: indexLanguage.country,
default: indexLanguage.default,
});
} catch (e) {
console.error(e);
}
}
});
return languages;
};
const getFallbackLanguageObject = (dirPath: string, languageObject: LanguageObject | undefined) => {
const files = fs.readdirSync(dirPath);
languageObject = languageObject || {};
files.forEach(function (file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
languageObject = getFallbackLanguageObject(dirPath + "/" + file, languageObject);
} else {
const parts = file.split(".");
if (parts.length !== 3 || parts[1] !== fallbackLanguage || parts[2] !== "json") {
return;
}
const data = JSON.parse(fs.readFileSync(dirPath + "/" + file, "utf-8"));
try {
const languageObjectFormat: z.ZodType<LanguageObject> = z.lazy(() => {
return z.object({}).catchall(z.union([z.string(), z.boolean(), languageObjectFormat]));
});
const languageObjectData = languageObjectFormat.parse(data);
languageObject = { ...languageObject, ...languageObjectData };
} catch (e) {
console.error(e);
}
}
});
return languageObject;
};
export const languages = getAllLanguagesByFiles(translationsBasePath, undefined);
export const fallbackLanguageObject = getFallbackLanguageObject(translationsBasePath, undefined);

View file

@ -1,254 +0,0 @@
import { FALLBACK_LANGUAGE } from "../Enum/EnvironmentVariable";
export type Language = {
language: string;
country: string;
};
type TranslationParams = {
[key: string]: string | number;
};
class Translator {
public readonly fallbackLanguage: Language = this.getLanguageByString(FALLBACK_LANGUAGE) || {
language: "en",
country: "US",
};
private readonly fallbackLanguageObject: LanguageObject = FALLBACK_LANGUAGE_OBJECT;
/**
* Current language
*/
private currentLanguage: Language;
/**
* Contain all translation keys of current language
*/
private currentLanguageObject: LanguageObject;
public constructor() {
this.currentLanguage = this.fallbackLanguage;
this.currentLanguageObject = this.fallbackLanguageObject;
this.defineCurrentLanguage();
}
/**
* Get language object from string who respect the RFC 5646
* @param {string} languageString RFC 5646 formatted string
* @returns {Language|undefined} Language object who represent the languageString
*/
public getLanguageByString(languageString: string): Language | undefined {
const parts = languageString.split("-");
if (parts.length !== 2 || parts[0].length !== 2 || parts[1].length !== 2) {
console.error(`Language string "${languageString}" do not respect RFC 5646 with language and country code`);
return undefined;
}
return {
language: parts[0].toLowerCase(),
country: parts[1].toUpperCase(),
};
}
/**
* Get a string who respect the RFC 5646 by a language object
* @param {Language} language A language object
* @returns {string|undefined} String who represent the language object
*/
public getStringByLanguage(language: Language): string | undefined {
return `${language.language}-${language.country}`;
}
/**
* Add the current language file loading into Phaser loader queue
* @param {Phaser.Loader.LoaderPlugin} pluginLoader Phaser LoaderPLugin
*/
public loadCurrentLanguageFile(pluginLoader: Phaser.Loader.LoaderPlugin) {
const languageString = this.getStringByLanguage(this.currentLanguage);
pluginLoader.json({
key: `language-${languageString}`,
url: `resources/translations/${languageString}.json`,
});
}
/**
* TypeGuard to check if is a LanguageObject
* @param {unknown} object Presume LanguageObject
* @returns {boolean} Is a LanguageObject or not
*/
private isLanguageObject(object: unknown): object is LanguageObject {
return typeof object === "object";
}
/**
* Get from the Phase cache the current language object and promise to load it
* @param {Phaser.Cache.CacheManager} cacheManager Phaser CacheManager
* @returns {Promise<void>} Load current language promise
*/
public loadCurrentLanguageObject(cacheManager: Phaser.Cache.CacheManager): Promise<void> {
return new Promise((resolve, reject) => {
const languageObject: Object = cacheManager.json.get(
`language-${this.getStringByLanguage(this.currentLanguage)}`
);
if (!languageObject) {
return reject(new Error("Language not found in cache"));
}
if (!this.isLanguageObject(languageObject)) {
throw new Error("Cannot load a bad language object");
}
this.currentLanguageObject = languageObject;
return resolve();
});
}
/**
* Get the language for RFC 5646 2 char string from availables languages
* @param {string} languageString Language RFC 5646 string
* @returns {Language|undefined} Language object who represent the languageString
*/
public getLanguageWithoutCountry(languageString: string): Language | undefined {
if (languageString.length !== 2) {
return undefined;
}
let languageFound = undefined;
for (const language of LANGUAGES) {
if (language.id.startsWith(languageString) && language.default) {
languageFound = this.getLanguageByString(language.id);
break;
}
}
return languageFound;
}
/**
* Get the current language
* @returns {Language} Current language
*/
public getCurrentLanguage(): Language {
return this.currentLanguage;
}
public switchLanguage(languageString: string) {
if (this.getLanguageByString(languageString)) {
localStorage.setItem("language", languageString);
}
}
/**
* Define the current language by the navigator or a cookie
*/
private defineCurrentLanguage() {
const navigatorLanguage: string | undefined = navigator.language;
const localStorageLanguage = localStorage.getItem("language");
let currentLanguage = undefined;
if (localStorageLanguage && typeof localStorageLanguage === "string") {
const localStorageLanguageObject = this.getLanguageByString(localStorageLanguage);
if (localStorageLanguageObject) {
currentLanguage = localStorageLanguageObject;
}
}
if (!currentLanguage && navigatorLanguage) {
const navigatorLanguageObject =
navigator.language.length === 2
? this.getLanguageWithoutCountry(navigatorLanguage)
: this.getLanguageByString(navigatorLanguage);
if (navigatorLanguageObject) {
currentLanguage = navigatorLanguageObject;
}
}
if (!currentLanguage || currentLanguage === this.fallbackLanguage) {
return;
}
this.currentLanguage = currentLanguage;
}
/**
* Get value on object by property path
* @param {string} key Translation key
* @param {LanguageObject} object Language object
* @returns {string|undefined} Found translation by path
*/
private getObjectValueByPath(key: string, object: LanguageObject): string | undefined {
const paths = key.split(".");
let currentValue: string | boolean | LanguageObject = object;
for (const path of paths) {
if (
typeof currentValue === "string" ||
typeof currentValue === "boolean" ||
currentValue[path] === undefined
) {
return undefined;
}
currentValue = currentValue[path];
}
if (typeof currentValue !== "string") {
return undefined;
}
return currentValue;
}
/**
* Replace {{ }} tags on a string by the params values
* @param {string} string Translation string
* @param {{ [key: string]: string | number }} params Tags to replace by value
* @returns {string} Formatted string
*/
private formatStringWithParams(string: string, params: TranslationParams): string {
let formattedString = string;
for (const param in params) {
const regex = `/{{\\s*\\${param}\\s*}}/g`;
formattedString = formattedString.replace(new RegExp(regex), params[param].toString());
}
return formattedString;
}
/**
* Get translation by a key and formatted with params by {{ }} tag
* @param {string} key Translation key
* @param {TranslationParams} params Tags to replace by value
* @returns {string} Translation formatted
*/
public translate(key: string, params?: TranslationParams): string {
const currentLanguageValue = this.getObjectValueByPath(key, this.currentLanguageObject);
if (currentLanguageValue) {
return params ? this.formatStringWithParams(currentLanguageValue, params) : currentLanguageValue;
}
console.warn(`"${key}" key cannot be found in ${this.getStringByLanguage(this.currentLanguage)} language`);
const fallbackLanguageValue = this.getObjectValueByPath(key, this.fallbackLanguageObject);
if (fallbackLanguageValue) {
return params ? this.formatStringWithParams(fallbackLanguageValue, params) : fallbackLanguageValue;
}
console.warn(
`"${key}" key cannot be found in ${this.getStringByLanguage(this.fallbackLanguage)} fallback language`
);
return key;
}
}
export const languages: LanguageFound[] = LANGUAGES;
export const translator = new Translator();
export const _ = translator.translate.bind(translator);

View file

@ -9,11 +9,12 @@ export type StopScreenSharingCallback = (media: MediaStream) => void;
import { cowebsiteCloseButtonId } from "./CoWebsiteManager"; import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../Stores/LayoutManagerStore";
import { get } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError"; import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
import { _ } from "../Translator/Translator"; import { localUserStore } from "../Connexion/LocalUserStore";
import LL from "../i18n/i18n-svelte";
import { get } from "svelte/store";
import { localeDetector } from "../i18n/locales";
export class MediaManager { export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
@ -24,46 +25,52 @@ export class MediaManager {
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
constructor() { constructor() {
localStreamStore.subscribe((result) => { localeDetector()
if (result.type === "error") { .catch(() => {
if (result.error.name !== MediaStreamConstraintsError.NAME) { throw new Error("Cannot load locale on media manager");
layoutManagerActionStore.addAction({ })
uuid: "cameraAccessDenied", .finally(() => {
type: "warning", localStreamStore.subscribe((result) => {
message: _("warning.access-denied.camera"), if (result.type === "error") {
callback: () => { if (result.error.name !== MediaStreamConstraintsError.NAME) {
helpCameraSettingsVisibleStore.set(true); layoutManagerActionStore.addAction({
}, uuid: "cameraAccessDenied",
userInputManager: this.userInputManager, type: "warning",
}); message: get(LL).warning.accessDenied.camera(),
} callback: () => {
//remove it after 10 sec helpCameraSettingsVisibleStore.set(true);
setTimeout(() => { },
layoutManagerActionStore.removeAction("cameraAccessDenied"); userInputManager: this.userInputManager,
}, 10000); });
return; }
} //remove it after 10 sec
}); setTimeout(() => {
layoutManagerActionStore.removeAction("cameraAccessDenied");
screenSharingLocalStreamStore.subscribe((result) => { }, 10000);
if (result.type === "error") { return;
console.error(result.error); }
layoutManagerActionStore.addAction({
uuid: "screenSharingAccessDenied",
type: "warning",
message: _("warning.access-denied.screen-sharing"),
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
userInputManager: this.userInputManager,
}); });
//remove it after 10 sec
setTimeout(() => { screenSharingLocalStreamStore.subscribe((result) => {
layoutManagerActionStore.removeAction("screenSharingAccessDenied"); if (result.type === "error") {
}, 10000); console.error(result.error);
return; layoutManagerActionStore.addAction({
} uuid: "screenSharingAccessDenied",
}); type: "warning",
message: get(LL).warning.accessDenied.screenSharing(),
callback: () => {
helpCameraSettingsVisibleStore.set(true);
},
userInputManager: this.userInputManager,
});
//remove it after 10 sec
setTimeout(() => {
layoutManagerActionStore.removeAction("screenSharingAccessDenied");
}, 10000);
return;
}
});
});
} }
public showGameOverlay(): void { public showGameOverlay(): void {

View file

@ -1,13 +0,0 @@
type LanguageObject = {
[key: string]: string | boolean | LanguageObject;
};
type LanguageFound = {
id: string;
language: string;
country: string;
default: boolean;
};
declare const FALLBACK_LANGUAGE_OBJECT: LanguageObject;
declare const LANGUAGES: LanguageFound[];

3
front/src/i18n/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
i18n-svelte.ts
i18n-types.ts
i18n-util.ts

View file

@ -0,0 +1,10 @@
import type { BaseTranslation } from "../i18n-types";
const audio: BaseTranslation = {
manager: {
reduce: "reduce in conversations",
},
message: "Audio message",
};
export default audio;

View file

@ -0,0 +1,22 @@
import type { BaseTranslation } from "../i18n-types";
const camera: BaseTranslation = {
enable: {
title: "Turn on your camera and microphone",
start: "Let's go!",
},
help: {
title: "Camera / Microphone access needed",
permissionDenied: "Permission denied",
content: "You must allow camera and microphone access in your browser.",
firefoxContent:
'Please click the "Remember this decision" checkbox, if you don\'t want Firefox to keep asking you the authorization.',
refresh: "Refresh",
continue: "Continue without webcam",
},
my: {
silentZone: "Silent zone",
},
};
export default camera;

View file

@ -0,0 +1,12 @@
import type { BaseTranslation } from "../i18n-types";
const chat: BaseTranslation = {
intro: "Here is your chat history:",
enter: "Enter your message...",
menu: {
visitCard: "Visit card",
addFriend: "Add friend",
},
};
export default chat;

View file

@ -0,0 +1,11 @@
import type { BaseTranslation } from "../i18n-types";
const companion: BaseTranslation = {
select: {
title: "Select your companion",
any: "No companion",
continue: "Continue",
},
};
export default companion;

View file

@ -0,0 +1,20 @@
import type { BaseTranslation } from "../i18n-types";
const error: BaseTranslation = {
accessLink: {
title: "Access link incorrect",
subTitle: "Could not find map. Please check your access link.",
details: "If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
},
connectionRejected: {
title: "Connection rejected",
subTitle: "You cannot join the World. Try again later {error}.",
details: "If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
},
connectionRetry: {
unableConnect: "Unable to connect to WorkAdventure. Are you connected to internet?",
},
error: "Error",
};
export default error;

View file

@ -0,0 +1,27 @@
import type { BaseTranslation } from "../i18n-types";
const follow: BaseTranslation = {
interactStatus: {
following: "Following {leader}",
waitingFollowers: "Waiting for followers confirmation",
followed: {
one: "{follower} is following you",
two: "{firstFollower} and {secondFollower} are following you",
many: "{followers} and {lastFollower} are following you",
},
},
interactMenu: {
title: {
interact: "Interaction",
follow: "Do you want to follow {leader}?",
},
stop: {
leader: "Do you want to stop leading the way?",
follower: "Do you want to stop following {leader}?",
},
yes: "Yes",
no: "No",
},
};
export default follow;

View file

@ -0,0 +1,30 @@
import type { BaseTranslation } from "../i18n-types";
import audio from "./audio";
import camera from "./camera";
import chat from "./chat";
import companion from "./companion";
import woka from "./woka";
import error from "./error";
import follow from "./follow";
import login from "./login";
import menu from "./menu";
import report from "./report";
import warning from "./warning";
const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio,
camera,
chat,
companion,
woka,
error,
follow,
login,
menu,
report,
warning,
};
export default en_US;

View file

@ -0,0 +1,14 @@
import type { BaseTranslation } from "../i18n-types";
const login: BaseTranslation = {
input: {
name: {
placeholder: "Enter your name",
empty: "The name is empty",
},
},
terms: 'By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.',
continue: "Continue",
};
export default login;

View file

@ -0,0 +1,124 @@
import type { BaseTranslation } from "../i18n-types";
const menu: BaseTranslation = {
title: "Menu",
icon: {
open: {
menu: "Open menu",
invite: "Show invite",
register: "Register",
chat: "Open chat",
},
},
visitCard: {
close: "Close",
},
profile: {
edit: {
name: "Edit your name",
woka: "Edit your WOKA",
companion: "Edit your companion",
camera: "Edit your camera",
},
login: "Sign in",
logout: "Log out",
},
settings: {
gameQuality: {
title: "Game quality",
short: {
high: "High (120 fps)",
medium: "Medium (60 fps)",
minimum: "Minimum (40 fps)",
small: "Small (20 fps)",
},
long: {
high: "High video quality (120 fps)",
medium: "Medium video quality (60 fps, recommended)",
minimum: "Minimum video quality (40 fps)",
small: "Small video quality (20 fps)",
},
},
videoQuality: {
title: "Video quality",
short: {
high: "High (30 fps)",
medium: "Medium (20 fps)",
minimum: "Minimum (10 fps)",
small: "Small (5 fps)",
},
long: {
high: "High video quality (30 fps)",
medium: "Medium video quality (20 fps, recommended)",
minimum: "Minimum video quality (10 fps)",
small: "Small video quality (5 fps)",
},
},
language: {
title: "Language",
},
save: {
warning: "(Saving these settings will restart the game)",
button: "Save",
},
fullscreen: "Fullscreen",
notifications: "Notifications",
cowebsiteTrigger: "Always ask before opening websites and Jitsi Meet rooms",
ignoreFollowRequest: "Ignore requests to follow other users",
},
invite: {
description: "Share the link of the room!",
copy: "Copy",
share: "Share",
},
globalMessage: {
text: "Text",
audio: "Audio",
warning: "Broadcast to all rooms of the world",
enter: "Enter your message here...",
send: "Send",
},
globalAudio: {
uploadInfo: "Upload a file",
error: "No file selected. You need to upload a file before sending it.",
},
contact: {
gettingStarted: {
title: "Getting started",
description:
"WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.",
},
createMap: {
title: "Create your map",
description: "You can also create your own custom map by following the step of the documentation.",
},
},
about: {
mapInfo: "Information on the map",
mapLink: "link to this map",
copyrights: {
map: {
title: "Copyrights of the map",
empty: "The map creator did not declare a copyright for the map.",
},
tileset: {
title: "Copyrights of the tilesets",
empty: "The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets have no license.",
},
audio: {
title: "Copyrights of audio files",
empty: "The map creator did not declare a copyright for audio files. This doesn't mean that those audio files have no license.",
},
},
},
sub: {
profile: "Profile",
settings: "Settings",
invite: "Invite",
credit: "Credit",
globalMessages: "Global Messages",
contact: "Contact",
},
};
export default menu;

View file

@ -0,0 +1,25 @@
import type { BaseTranslation } from "../i18n-types";
const report: BaseTranslation = {
block: {
title: "Block",
content: "Block any communication from and to {userName}. This can be reverted.",
unblock: "Unblock this user",
block: "Block this user",
},
title: "Report",
content: "Send a report message to the administrators of this room. They may later ban this user.",
message: {
title: "Your message: ",
empty: "Report message cannot to be empty.",
},
submit: "Report this user",
moderate: {
title: "Moderate {userName}",
block: "Block",
report: "Report",
noSelect: "ERROR : There is no action selected.",
},
};
export default report;

View file

@ -0,0 +1,16 @@
import type { BaseTranslation } from "../i18n-types";
const warning: BaseTranslation = {
title: "Warning!",
content:
'This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank">here</a>',
limit: "This world is close to its limit!",
accessDenied: {
camera: "Camera access denied. Click here and check your browser permissions.",
screenSharing: "Screen sharing denied. Click here and check your browser permissions.",
},
importantMessage: "Important message",
connectionLost: "Connection lost. Reconnecting...",
};
export default warning;

View file

@ -0,0 +1,20 @@
import type { BaseTranslation } from "../i18n-types";
const woka: BaseTranslation = {
customWoka: {
title: "Customize your WOKA",
navigation: {
return: "Return",
back: "Back",
finish: "Finish",
next: "Next",
},
},
selectWoka: {
title: "Select your WOKA",
continue: "Continue",
customize: "Customize your WOKA",
},
};
export default woka;

View file

@ -0,0 +1,11 @@
import type { AsyncFormattersInitializer } from "typesafe-i18n";
import type { Locales, Formatters } from "./i18n-types";
// eslint-disable-next-line @typescript-eslint/require-await
export const initFormatters: AsyncFormattersInitializer<Locales, Formatters> = async (locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
};
return formatters;
};

View file

@ -0,0 +1,10 @@
import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "réduit dans les conversations",
},
message: "Message audio",
};
export default audio;

View file

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const camera: NonNullable<Translation["camera"]> = {
enable: {
title: "Allumez votre caméra et votre microphone",
start: "C'est partie!",
},
help: {
title: "Accès à la caméra / au microphone nécessaire",
permissionDenied: "Permission refusée",
content: "Vous devez autoriser l'accès à la caméra et au microphone dans votre navigateur.",
firefoxContent:
'Veuillez cocher la case "Se souvenir de cette décision" si vous ne voulez pas que Firefox vous demande sans cesse l\'autorisation.',
refresh: "Rafraîchir",
continue: "Continuer sans webcam",
},
my: {
silentZone: "Zone silencieuse",
},
};
export default camera;

View file

@ -0,0 +1,12 @@
import type { Translation } from "../i18n-types";
const chat: NonNullable<Translation["chat"]> = {
intro: "Voici l'historique de votre chat:",
enter: "Entrez votre message...",
menu: {
visitCard: "Carte de visite",
addFriend: "Ajouter un ami",
},
};
export default chat;

View file

@ -0,0 +1,11 @@
import type { Translation } from "../i18n-types";
const companion: NonNullable<Translation["companion"]> = {
select: {
title: "Sélectionnez votre compagnon",
any: "Pas de compagnon",
continue: "Continuer",
},
};
export default companion;

View file

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const error: NonNullable<Translation["error"]> = {
accessLink: {
title: "Lien d'accès incorrect",
subTitle: "Impossible de trouver la carte. Veuillez vérifier votre lien d'accès.",
details:
"Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re",
},
connectionRejected: {
title: "Connexion rejetée",
subTitle: "Vous ne pouvez pas rejoindre le monde. Réessayer plus tard {error}.",
details:
"Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re",
},
connectionRetry: {
unableConnect: "Impossible de se connecter à WorkAdventure. Etes vous connecté à Internet?",
},
error: "Erreur",
};
export default error;

View file

@ -0,0 +1,27 @@
import type { Translation } from "../i18n-types";
const follow: NonNullable<Translation["follow"]> = {
interactStatus: {
following: "Vous suivez {leader}",
waitingFollowers: "En attente de la confirmation des suiveurs",
followed: {
one: "{follower} vous suit",
two: "{firstFollower} et {secondFollower} vous suivent",
many: "{followers} et {lastFollower} vous suivent",
},
},
interactMenu: {
title: {
interact: "Interaction",
follow: "Voulez-vous suivre {leader}?",
},
stop: {
leader: "Voulez-vous qu'on arrête de vous suivre?",
follower: "Voulez-vous arrêter de suivre {leader}?",
},
yes: "Oui",
no: "Non",
},
};
export default follow;

View file

@ -0,0 +1,32 @@
import en_US from "../en-US";
import type { Translation } from "../i18n-types";
import audio from "./audio";
import camera from "./camera";
import chat from "./chat";
import companion from "./companion";
import error from "./error";
import follow from "./follow";
import login from "./login";
import menu from "./menu";
import report from "./report";
import warning from "./warning";
import woka from "./woka";
const fr_FR: Translation = {
...en_US,
language: "Français",
country: "France",
audio,
camera,
chat,
companion,
woka,
error,
follow,
login,
menu,
report,
warning,
};
export default fr_FR;

View file

@ -0,0 +1,14 @@
import type { Translation } from "../i18n-types";
const login: NonNullable<Translation["login"]> = {
input: {
name: {
placeholder: "Entrez votre nom",
empty: "Le nom est vide",
},
},
terms: 'En continuant, vous acceptez nos <a href="https://workadventu.re/terms-of-use" target="_blank">conditions d\'utilisation</a>, notre <a href="https://workadventu.re/privacy-policy" target="_blank">politique de confidentialité</a> et notre <a href="https://workadventu.re/cookie-policy" target="_blank">politique relative aux cookies</a>.',
continue: "Continuer",
};
export default login;

View file

@ -0,0 +1,124 @@
import type { Translation } from "../i18n-types";
const menu: NonNullable<Translation["menu"]> = {
title: "Menu",
icon: {
open: {
menu: "Ouvrir le menu",
invite: "Afficher l'invitation",
register: "Enregistrez vous",
chat: "Ouvrir le chat",
},
},
visitCard: {
close: "Fermer",
},
profile: {
edit: {
name: "Modifier votre nom",
woka: "Modifier votre WOKA",
companion: "Modifier votre compagnon",
camera: "Modifier votre caméra",
},
login: "S'identifier",
logout: "Déconnexion",
},
settings: {
gameQuality: {
title: "Qualité du jeu",
short: {
high: "Haute (120 fps)",
medium: "Moyenne (60 fps)",
minimum: "Minimale (40 fps)",
small: "Reduite (20 fps)",
},
long: {
high: "Haute (120 fps)",
medium: "Moyenne (60 fps, recommandée)",
minimum: "Minimale (40 fps)",
small: "Reduite (20 fps)",
},
},
videoQuality: {
title: "Qualité de la vidéo",
short: {
high: "Haute (30 fps)",
medium: "Moyenne (20 fps)",
minimum: "Minimale (10 fps)",
small: "Reduite (5 fps)",
},
long: {
high: "Haute (30 fps)",
medium: "Moyenne (20 fps, recommandée)",
minimum: "Minimale (10 fps)",
small: "Reduite (5 fps)",
},
},
language: {
title: "Langage",
},
save: {
warning: "(La sauvegarde de ces paramètres redémarre le jeu)",
button: "Sauvegarder",
},
fullscreen: "Plein écran",
notifications: "Notifications",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi",
ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs",
},
invite: {
description: "Partager le lien de la salle!",
copy: "Copier",
share: "Partager",
},
globalMessage: {
text: "Texte",
audio: "Audio",
warning: "Diffusion dans toutes les salles du monde",
enter: "Entrez votre message ici...",
send: "Envoyer",
},
globalAudio: {
uploadInfo: "Télécharger un fichier",
error: "Aucun fichier sélectionné. Vous devez télécharger un fichier avant de l'envoyer.",
},
contact: {
gettingStarted: {
title: "Pour commencer",
description:
"WorkAdventure vous permet de créer un espace en ligne pour communiquer spontanément avec d'autres personnes. Et tout commence par la création de votre propre espace. Choisissez parmi une large sélection de cartes préfabriquées par notre équipe.",
},
createMap: {
title: "Créer votre carte",
description: "Vous pouvez également créer votre propre carte personnalisée en suivant la documentation.",
},
},
about: {
mapInfo: "Informations sur la carte",
mapLink: "lien vers cette carte",
copyrights: {
map: {
title: "Droits d'auteur de la carte",
empty: "Le créateur de la carte n'a pas déclaré de droit d'auteur pour la carte.",
},
tileset: {
title: "Droits d'auteur des tilesets",
empty: "Le créateur de la carte n'a pas déclaré de droit d'auteur pour les tilesets. Cela ne signifie pas que les tilesets n'ont pas de licence.",
},
audio: {
title: "Droits d'auteur des fichiers audio",
empty: "aré de droit d'auteur pour les fichiers audio. Cela ne signifie pas que les fichiers audio n'ont pas de licence.",
},
},
},
sub: {
profile: "Profile",
settings: "Paramètres",
invite: "Inviter",
credit: "Crédits",
globalMessages: "Messages globaux",
contact: "Contact",
},
};
export default menu;

View file

@ -0,0 +1,25 @@
import type { Translation } from "../i18n-types";
const report: NonNullable<Translation["report"]> = {
block: {
title: "Bloquer",
content: "Bloquer toute communication en provenance et à destination de {userName}. Cela peut être annulé.",
unblock: "Débloquer cet utilisateur",
block: "Bloquer cet utilisateur",
},
title: "Signaler",
content: "Signaler aux administrateurs de cette salle. Ils pourront par la suite bannir cet utilisateur.",
message: {
title: "Votre message: ",
empty: "Le message du signalement ne peut pas être vide.",
},
submit: "Signaler cet utilisateur",
moderate: {
title: "Modérer {userName}",
block: "Bloquer",
report: "Signaler",
noSelect: "ERREUR : Il n'y a pas d'action sélectionnée.",
},
};
export default report;

View file

@ -0,0 +1,16 @@
import type { Translation } from "../i18n-types";
const warning: NonNullable<Translation["warning"]> = {
title: "Attention!",
content:
'Ce monde est proche de sa limite ! Vous pouvez améliorer sa capacité <a href={upgradeLink} target="_blank">içi</a>',
limit: "Ce monde est proche de ses limites!",
accessDenied: {
camera: "Accès à la caméra refusé. Cliquez ici et vérifiez les autorisations de votre navigateur.",
screenSharing: "Partage d'écran refusé. Cliquez ici et vérifiez les autorisations de votre navigateur.",
},
importantMessage: "Message important",
connectionLost: "Connexion perdue. Reconnexion...",
};
export default warning;

View file

@ -0,0 +1,20 @@
import type { Translation } from "../i18n-types";
const woka: NonNullable<Translation["woka"]> = {
customWoka: {
title: "Personnalisez votre WOKA",
navigation: {
return: "Retour",
back: "Précédent",
finish: "Terminer",
next: "Suivant",
},
},
selectWoka: {
title: "Sélectionnez votre WOKA",
continue: "Continuer",
customize: "Personnalisez votre WOKA",
},
};
export default woka;

52
front/src/i18n/locales.ts Normal file
View file

@ -0,0 +1,52 @@
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable";
import { initI18n, setLocale } from "./i18n-svelte";
import type { Locales, Translation } from "./i18n-types";
import { baseLocale, getTranslationForLocale, locales } from "./i18n-util";
const fallbackLocale = FALLBACK_LOCALE || baseLocale;
const localStorageProperty = "language";
export const localeDetector = async () => {
const exist = localStorage.getItem(localStorageProperty);
let detectedLocale: Locales = fallbackLocale as Locales;
if (exist) {
const localStorageDetector = initLocalStorageDetector(localStorageProperty);
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales;
} else {
detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector) as Locales;
}
await initI18n(detectedLocale);
};
export const setCurrentLocale = (locale: Locales) => {
localStorage.setItem(localStorageProperty, locale);
setLocale(locale).catch(() => {
console.log("Cannot reload the locale!");
});
};
export type DisplayableLocale = { id: Locales; language: string; country: string };
function getDisplayableLocales() {
const localesObject: DisplayableLocale[] = [];
locales.forEach((locale) => {
getTranslationForLocale(locale)
.then((translations) => {
localesObject.push({
id: locale,
language: translations.language,
country: translations.country,
});
})
.catch((error) => {
console.log(error);
});
});
return localesObject;
}
export const displayableLocales = getDisplayableLocales();

View file

@ -1,8 +0,0 @@
{
"audio": {
"manager": {
"reduce": "reduce in conversations"
},
"message": "Audio message"
}
}

View file

@ -1,19 +0,0 @@
{
"camera": {
"enable": {
"title": "Turn on your camera and microphone",
"start": "Let's go!"
},
"help": {
"title": "Camera / Microphone access needed",
"permission-denied": "Permission denied",
"content": "You must allow camera and microphone access in your browser.",
"firefox-content": "Please click the \"Remember this decision\" checkbox, if you don't want Firefox to keep asking you the authorization.",
"refresh": "Refresh",
"continue": "Continue without webcam"
},
"my": {
"silent-zone": "Silent zone"
}
}
}

View file

@ -1,10 +0,0 @@
{
"chat": {
"intro": "Here is your chat history:",
"enter": "Enter your message...",
"menu": {
"visit-card": "Visit card",
"add-friend": "Add friend"
}
}
}

View file

@ -1,9 +0,0 @@
{
"companion": {
"select": {
"title": "Select your companion",
"any": "No companion",
"continue": "Continue"
}
}
}

View file

@ -1,16 +0,0 @@
{
"custom-character": {
"title": "Customize your WOKA",
"navigation": {
"return": "Return",
"back": "Back",
"finish": "Finish",
"next": "Next"
}
},
"select-woka": {
"title": "Select your WOKA",
"continue": "Continue",
"customize": "Customize your WOKA"
}
}

View file

@ -1,18 +0,0 @@
{
"error": {
"access-link": {
"title": "Access link incorrect",
"sub-title": "Could not find map. Please check your access link.",
"details": "If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
},
"connection-rejected": {
"title": "Connection rejected",
"sub-title": "You cannot join the World. Try again later {{error}}.",
"details": "If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
},
"connection-retry": {
"unable-to-connect-to-workAdventure-are-you-connected-to-internet": "Unable to connect to WorkAdventure. Are you connected to internet?"
},
"error": "Error"
}
}

View file

@ -1,25 +0,0 @@
{
"follow": {
"interact-status": {
"following": "Following {{leader}}",
"waiting-followers": "Waiting for followers confirmation",
"followed": {
"one": "{{follower}} is following you",
"two": "{{firstFollower}} and {{secondFollower}} are following you",
"many": "{{followers}} and {{lastFollower}} are following you"
}
},
"interact-menu": {
"title": {
"interact": "Interaction",
"follow": "Do you want to follow {{leader}}?"
},
"stop": {
"leader": "Do you want to stop leading the way?",
"follower": "Do you want to stop following {{leader}}?"
},
"yes": "Yes",
"no": "No"
}
}
}

View file

@ -1,5 +0,0 @@
{
"language": "English",
"country": "United States",
"default": true
}

View file

@ -1,12 +0,0 @@
{
"login": {
"input": {
"name": {
"placeholder": "Enter your name",
"empty": "The name is empty"
}
},
"terms": "By continuing, you are agreeing our <a href=\"https://workadventu.re/terms-of-use\" target=\"_blank\">terms of use</a>, <a href=\"https://workadventu.re/privacy-policy\" target=\"_blank\">privacy policy</a> and <a href=\"https://workadventu.re/cookie-policy\" target=\"_blank\">cookie policy</a>.",
"continue": "Continue"
}
}

View file

@ -1,121 +0,0 @@
{
"menu": {
"title": "Menu",
"icon": {
"open": {
"menu": "Open menu",
"invite": "Show invite",
"register": "Register",
"chat": "Open chat"
}
},
"visit-card": {
"close": "Close"
},
"profile": {
"edit": {
"name": "Edit your name",
"woka": "Edit your WOKA",
"companion": "Edit your companion",
"camera": "Edit your camera"
},
"login": "Sign in",
"logout": "Log out"
},
"settings": {
"game-quality": {
"title": "Game quality",
"short": {
"high": "High (120 fps)",
"medium": "Medium (60 fps)",
"minimum": "Minimum (40 fps)",
"small": "Small (20 fps)"
},
"long": {
"high": "High video quality (120 fps)",
"medium": "Medium video quality (60 fps, recommended)",
"minimum": "Minimum video quality (40 fps)",
"small": "Small video quality (20 fps)"
}
},
"video-quality": {
"title": "Video quality",
"short": {
"high": "High (30 fps)",
"medium": "Medium (20 fps)",
"minimum": "Minimum (10 fps)",
"small": "Small (5 fps)"
},
"long": {
"high": "High video quality (30 fps)",
"medium": "Medium video quality (20 fps, recommended)",
"minimum": "Minimum video quality (10 fps)",
"small": "Small video quality (5 fps)"
}
},
"language": {
"title": "Language"
},
"save": {
"warning": "(Saving these settings will restart the game)",
"button": "Save"
},
"fullscreen": "Fullscreen",
"notifications": "Notifications",
"cowebsite-trigger": "Always ask before opening websites and Jitsi Meet rooms",
"ignore-follow-request": "Ignore requests to follow other users"
},
"invite": {
"description": "Share the link of the room!",
"copy": "Copy",
"share": "Share"
},
"global-message": {
"text": "Text",
"audio": "Audio",
"warning": "Broadcast to all rooms of the world",
"enter": "Enter your message here...",
"send": "Send"
},
"global-audio": {
"upload-info": "Upload a file",
"error": "No file selected. You need to upload a file before sending it."
},
"contact": {
"getting-started": {
"title": "Getting started",
"description": "WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team."
},
"create-map": {
"title": "Create your map",
"description": "You can also create your own custom map by following the step of the documentation."
}
},
"about": {
"map-info": "Information on the map",
"map-link": "link to this map",
"copyrights": {
"map": {
"title": "Copyrights of the map",
"empty": "The map creator did not declare a copyright for the map."
},
"tileset": {
"title": "Copyrights of the tilesets",
"empty": "The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets have no license."
},
"audio": {
"title": "Copyrights of audio files",
"empty": "The map creator did not declare a copyright for audio files. This doesn't mean that those audio files have no license."
}
}
},
"sub": {
"profile": "Profile",
"settings": "Settings",
"invite": "Invite",
"credit": "Credit",
"global-message": "Global Messages",
"contact": "Contact"
}
}
}

View file

@ -1,23 +0,0 @@
{
"report": {
"block": {
"title": "Block",
"content": "Block any communication from and to {{userName}}. This can be reverted.",
"unblock": "Unblock this user",
"block": "Block this user"
},
"title": "Report",
"content": "Send a report message to the administrators of this room. They may later ban this user.",
"message": {
"title": "Your message: ",
"empty": "Report message cannot to be empty."
},
"submit": "Report this user"
},
"moderate": {
"title": "Moderate {{userName}}",
"block": "Block",
"report": "Report",
"no-select": "ERROR : There is no action selected."
}
}

View file

@ -1,13 +0,0 @@
{
"warning": {
"title": "Warning!",
"content": "This world is close to its limit!. You can upgrade its capacity <a href={{upgradeLink}} target=\"_blank\">here</a>",
"limit": "This world is close to its limit!",
"access-denied": {
"camera": "Camera access denied. Click here and check your browser permissions.",
"screen-sharing": "Screen sharing denied. Click here and check your browser permissions."
}
},
"important-message": "Important message",
"connection-lost": "Connection lost. Reconnecting..."
}

View file

@ -1,8 +0,0 @@
{
"audio": {
"manager": {
"reduce": "réduit dans les conversations"
},
"message": "Message audio"
}
}

View file

@ -1,19 +0,0 @@
{
"camera": {
"enable": {
"title": "Allumez votre caméra et votre microphone",
"start": "C'est partie!"
},
"help": {
"title": "Accès à la caméra / au microphone nécessaire",
"permission-denied": "Permission refusée",
"content": "Vous devez autoriser l'accès à la caméra et au microphone dans votre navigateur.",
"firefox-content": "Veuillez cocher la case \"Se souvenir de cette décision\" si vous ne voulez pas que Firefox vous demande sans cesse l'autorisation.",
"refresh": "Rafraîchir",
"continue": "Continuer sans webcam"
},
"my": {
"silent-zone": "Zone silencieuse"
}
}
}

View file

@ -1,10 +0,0 @@
{
"chat": {
"intro": "Voici l'historique de votre chat:",
"enter": "Entrez votre message...",
"menu": {
"visit-card": "Carte de visite",
"add-friend": "Ajouter un ami"
}
}
}

View file

@ -1,9 +0,0 @@
{
"companion": {
"select": {
"title": "Sélectionnez votre compagnon",
"any": "Pas de compagnon",
"continue": "Continuer"
}
}
}

View file

@ -1,16 +0,0 @@
{
"custom-character": {
"title": "Personnalisez votre WOKA",
"navigation": {
"return": "Retour",
"back": "Précédent",
"finish": "Terminer",
"next": "Suivant"
}
},
"select-woka": {
"title": "Sélectionnez votre WOKA",
"continue": "Continuer",
"customize": "Personnalisez votre WOKA"
}
}

View file

@ -1,18 +0,0 @@
{
"error": {
"access-link": {
"title": "Lien d'accès incorrect",
"sub-title": "Impossible de trouver la carte. Veuillez vérifier votre lien d'accès.",
"details": "Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re"
},
"connection-rejected": {
"title": "Connexion rejetée",
"sub-title": "Vous ne pouvez pas rejoindre le monde. Réessayer plus tard {{error}}.",
"details": "Si vous souhaitez obtenir de plus amples informations, vous pouvez contacter l'administrateur ou nous contacter à l'adresse suivante: hello@workadventu.re"
},
"connection-retry": {
"unable-to-connect-to-workAdventure-are-you-connected-to-internet": "Impossible de se connecter à WorkAdventure. Etes vous connecté à Internet?"
},
"error": "Erreur"
}
}

View file

@ -1,25 +0,0 @@
{
"follow": {
"interact-status": {
"following": "Vous suivez {{leader}}",
"waiting-followers": "En attente de la confirmation des suiveurs",
"followed": {
"one": "{{follower}} vous suit",
"two": "{{firstFollower}} et {{secondFollower}} vous suivent",
"many": "{{followers}} et {{lastFollower}} vous suivent"
}
},
"interact-menu": {
"title": {
"interact": "Interaction",
"follow": "Voulez-vous suivre {{leader}}?"
},
"stop": {
"leader": "Voulez-vous qu'on arrête de vous suivre?",
"follower": "Voulez-vous arrêter de suivre {{leader}}?"
},
"yes": "Oui",
"no": "Non"
}
}
}

View file

@ -1,5 +0,0 @@
{
"language": "Français",
"country": "France",
"default": true
}

View file

@ -1,12 +0,0 @@
{
"login": {
"input": {
"name": {
"placeholder": "Entrez votre nom",
"empty": "Le nom est vide"
}
},
"terms": "En continuant, vous acceptez nos <a href=\"https://workadventu.re/terms-of-use\" target=\"_blank\">conditions d'utilisation</a>, notre <a href=\"https://workadventu.re/privacy-policy\" target=\"_blank\">politique de confidentialité</a> et notre <a href=\"https://workadventu.re/cookie-policy\" target=\"_blank\">politique relative aux cookies</a>.",
"continue": "Continuer"
}
}

View file

@ -1,121 +0,0 @@
{
"menu": {
"title": "Menu",
"icon": {
"open": {
"menu": "Ouvrir le menu",
"invite": "Afficher l'invitation",
"register": "Enregistrez vous",
"chat": "Ouvrir le chat"
}
},
"visit-card": {
"close": "Fermer"
},
"profile": {
"edit": {
"name": "Modifier votre nom",
"woka": "Modifier votre WOKA",
"companion": "Modifier votre compagnon",
"camera": "Modifier votre caméra"
},
"login": "S'identifier",
"logout": "Déconnexion"
},
"settings": {
"game-quality": {
"title": "Qualité du jeu",
"short": {
"high": "Haute (120 fps)",
"medium": "Moyenne (60 fps)",
"minimum": "Minimale (40 fps)",
"small": "Reduite (20 fps)"
},
"long": {
"high": "Haute (120 fps)",
"medium": "Moyenne (60 fps, recommandée)",
"minimum": "Minimale (40 fps)",
"small": "Reduite (20 fps)"
}
},
"video-quality": {
"title": "Qualité de la vidéo",
"short": {
"high": "Haute (30 fps)",
"medium": "Moyenne (20 fps)",
"minimum": "Minimale (10 fps)",
"small": "Small (5 fps)"
},
"long": {
"high": "Haute (30 fps)",
"medium": "Moyenne (20 fps, recommandée)",
"minimum": "Minimale (10 fps)",
"small": "Reduite (5 fps)"
}
},
"language": {
"title": "Langage"
},
"save": {
"warning": "(La sauvegarde de ces paramètres redémarre le jeu)",
"button": "Sauvegarder"
},
"fullscreen": "Plein écran",
"notifications": "Notifications",
"cowebsite-trigger": "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi",
"ignore-follow-request": "Ignorer les demandes de suivi des autres utilisateurs"
},
"invite": {
"description": "Partager le lien de la salle!",
"copy": "Copier",
"share": "Partager"
},
"global-message": {
"text": "Texte",
"audio": "Audio",
"warning": "Diffusion dans toutes les salles du monde",
"enter": "Entrez votre message ici...",
"send": "Envoyer"
},
"global-audio": {
"upload-info": "Télécharger un fichier",
"error": "Aucun fichier sélectionné. Vous devez télécharger un fichier avant de l'envoyer."
},
"contact": {
"getting-started": {
"title": "Pour commencer",
"description": "WorkAdventure vous permet de créer un espace en ligne pour communiquer spontanément avec d'autres personnes. Et tout commence par la création de votre propre espace. Choisissez parmi une large sélection de cartes préfabriquées par notre équipe."
},
"create-map": {
"title": "Créer votre carte",
"description": "Vous pouvez également créer votre propre carte personnalisée en suivant la documentation."
}
},
"about": {
"map-info": "Informations sur la carte",
"map-link": "lien vers cette carte",
"copyrights": {
"map": {
"title": "Droits d'auteur de la carte",
"empty": "Le créateur de la carte n'a pas déclaré de droit d'auteur pour la carte."
},
"tileset": {
"title": "Droits d'auteur des tilesets",
"empty": "Le créateur de la carte n'a pas déclaré de droit d'auteur pour les tilesets. Cela ne signifie pas que les tilesets n'ont pas de licence."
},
"audio": {
"title": "Droits d'auteur des fichiers audio",
"empty": "Le créateur de la carte n'a pas déclaré de droit d'auteur pour les fichiers audio. Cela ne signifie pas que les fichiers audio n'ont pas de licence."
}
}
},
"sub": {
"profile": "Profile",
"settings": "Paramètres",
"invite": "Inviter",
"credit": "Crédits",
"global-message": "Messages globaux",
"contact": "Contact"
}
}
}

View file

@ -1,23 +0,0 @@
{
"report": {
"block": {
"title": "Bloquer",
"content": "Bloquer toute communication en provenance et à destination de {{userName}}. Cela peut être annulé.",
"unblock": "Débloquer cet utilisateur",
"block": "Bloquer cet utilisateur"
},
"title": "Signaler",
"content": "Signaler aux administrateurs de cette salle. Ils pourront par la suite bannir cet utilisateur.",
"message": {
"title": "Votre message: ",
"empty": "Le message du signalement ne peut pas être vide."
},
"submit": "Signaler cet utilisateur"
},
"moderate": {
"title": "Modérer {{userName}}",
"block": "Bloquer",
"report": "Signaler",
"no-select": "ERREUR : Il n'y a pas d'action sélectionnée."
}
}

View file

@ -1,13 +0,0 @@
{
"warning": {
"title": "Attention!",
"content": "Ce monde est proche de sa limite ! Vous pouvez améliorer sa capacité <a href={{upgradeLink}} target=\"_blank\">içi</a>",
"limit": "Ce monde est proche de ses limites!",
"access-denied": {
"camera": "Accès à la caméra refusé. Cliquez ici et vérifiez les autorisations de votre navigateur.",
"screen-sharing": "Partage d'écran refusé. Cliquez ici et vérifiez les autorisations de votre navigateur."
}
},
"important-message": "Message important",
"connection-lost": "Connexion perdue. Reconnexion..."
}

View file

@ -7,10 +7,6 @@ import sveltePreprocess from "svelte-preprocess";
import type { Configuration } from "webpack"; import type { Configuration } from "webpack";
import webpack from "webpack"; import webpack from "webpack";
import type WebpackDevServer from "webpack-dev-server"; import type WebpackDevServer from "webpack-dev-server";
import { fallbackLanguageObject, languages } from "./src/Translator/TranslationCompiler";
import type { LanguageFound } from "./src/Translator/TranslationCompiler";
const MergeJsonWebpackPlugin = require("merge-jsons-webpack-plugin");
const mode = process.env.NODE_ENV ?? "development"; const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -221,22 +217,5 @@ module.exports = {
OPID_LOGIN_SCREEN_PROVIDER: null, OPID_LOGIN_SCREEN_PROVIDER: null,
FALLBACK_LANGUAGE: null, FALLBACK_LANGUAGE: null,
}), }),
new webpack.DefinePlugin({
FALLBACK_LANGUAGE_OBJECT: JSON.stringify(fallbackLanguageObject),
LANGUAGES: JSON.stringify(languages),
}),
new MergeJsonWebpackPlugin({
output: {
groupBy: languages.map((language: LanguageFound) => {
return {
pattern: `./translations/**/*.${language.id}.json`,
fileName: `./resources/translations/${language.id}.json`
};
})
},
globOptions: {
nosort: true,
},
}),
], ],
} as Configuration & WebpackDevServer.Configuration; } as Configuration & WebpackDevServer.Configuration;

View file

@ -2776,18 +2776,6 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg=
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.2"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: glob@^7.0.3, glob@^7.1.3, glob@^7.1.6:
version "7.1.7" version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@ -3872,13 +3860,6 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-jsons-webpack-plugin@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/merge-jsons-webpack-plugin/-/merge-jsons-webpack-plugin-2.0.1.tgz#f2975ce0f734171331d42eee62d63329031800b4"
integrity sha512-8GP8rpOX3HSFsm7Gx+b3OAQR7yhgeAQvMqcZOJ+/cQIrqdak1c42a2T2vyeee8pzGPBf7pMLumthPh4CHgv2BA==
dependencies:
glob "7.1.1"
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -3980,7 +3961,7 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
minimatch@^3.0.2, minimatch@^3.0.4: minimatch@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@ -6063,6 +6044,11 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0" media-typer "0.3.0"
mime-types "~2.1.24" mime-types "~2.1.24"
typesafe-i18n@^2.59.0:
version "2.59.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg==
typescript@*: typescript@*:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"

View file

@ -1,29 +1,34 @@
import { Selector } from 'testcafe'; import { Selector } from "testcafe";
import {login} from "./utils/roles"; import { login } from "./utils/roles";
fixture `Translation` fixture`Translation`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`; .page`http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`;
test("Test that I can switch to French", async (t: TestController) => { test("Test that I can switch to French", async (t: TestController) => {
const languageSelect = Selector(".languages-switcher");
const languageOption = languageSelect.find("option");
const languageSelect = Selector('.languages-switcher'); await login(
const languageOption = languageSelect.find('option'); t,
"http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json"
);
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json'); await t
.click(".menuIcon img:first-child")
.click(Selector("button").withText("Settings"))
.click(".languages-switcher")
.click(languageOption.withText("Français (France)"))
.click(Selector("button").withText("Save"))
.wait(5000)
await t.click('.menuIcon img:first-child') .click(".menuIcon img:first-child")
.click(Selector('button').withText('Settings')) .expect(Selector("button").withText("Paramètres").innerText)
.click('.languages-switcher') .contains("Paramètres");
.click(languageOption.withText('Français (France)'))
.click(Selector('button').withText('Save'))
.click('.menuIcon img:first-child') t.ctx.passed = true;
.expect(Selector('button').withText('Paramètres').innerText).contains('Paramètres'); }).after(async (t) => {
if (!t.ctx.passed) {
t.ctx.passed = true; console.log("Test failed. Browser logs:");
}).after(async t => { console.log(await t.getBrowserConsoleMessages());
if (!t.ctx.passed) { }
console.log("Test failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
}); });