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

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();