lazy load locales (#1940)

* lazy load locales

* fix translation getter

* prettier ignore all generated i18n files

* fix menu translation reactivity

* put language and country translations into namespace

* use Intl.DisplayNames to provide language and region translations

* update typesafe-i18n

* fix newly added translations

* remove unused translations

* add fallback to locale code when Intl.DisplayNames is unavailable
This commit is contained in:
Lukas 2022-04-25 16:45:02 +02:00 committed by GitHub
parent 1c9caa690a
commit 9e0f43d542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 55 additions and 82 deletions

View File

@ -1,5 +1,3 @@
src/Messages/generated src/Messages/generated
src/Messages/JsonMessages src/Messages/JsonMessages
src/i18n/i18n-svelte.ts src/i18n/i18n-*.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts

View File

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

View File

@ -60,7 +60,7 @@
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-deferred": "^1.0.4", "ts-deferred": "^1.0.4",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0", "typesafe-i18n": "^5.3.5",
"uuidv4": "^6.2.10", "uuidv4": "^6.2.10",
"zod": "^3.14.3" "zod": "^3.14.3"
}, },

View File

@ -88,16 +88,11 @@
} }
} }
function translateMenuName(menu: MenuItem) { $: subMenuTranslations = $subMenusStore.map((subMenu) =>
if (menu.type === "scripting") { subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
return menu.label; );
} $: activeSubMenuTranslation =
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
// 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>
<svelte:window on:keydown={onKeyDown} /> <svelte:window on:keydown={onKeyDown} />
@ -106,20 +101,20 @@
<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>{$LL.menu.title()}</h2> <h2>{$LL.menu.title()}</h2>
<nav> <nav>
{#each $subMenusStore as submenu} {#each $subMenusStore as submenu, i}
<button <button
type="button" type="button"
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => void switchMenu(submenu)} on:click|preventDefault={() => void switchMenu(submenu)}
> >
{translateMenuName(submenu)} {subMenuTranslations[i]}
</button> </button>
{/each} {/each}
</nav> </nav>
</div> </div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}> <div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button> <button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{translateMenuName(activeSubMenu)}</h2> <h2>{activeSubMenuTranslation}</h2>
<svelte:component this={activeComponent} {...props} /> <svelte:component this={activeComponent} {...props} />
</div> </div>
</div> </div>

View File

@ -28,12 +28,12 @@
let previewCameraPrivacySettings = valueCameraPrivacySettings; let previewCameraPrivacySettings = valueCameraPrivacySettings;
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
function saveSetting() { async function saveSetting() {
let change = false; let change = false;
if (valueLocale !== previewValueLocale) { if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale; previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales); await setCurrentLocale(valueLocale as Locales);
} }
if (valueVideo !== previewValueVideo) { if (valueVideo !== previewValueVideo) {
@ -174,7 +174,7 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}> <select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)} {#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option> <option value={locale.id}>{`${locale.language} (${locale.region})`}</option>
{/each} {/each}
</select> </select>
</div> </div>

View File

@ -363,15 +363,13 @@ class ConnectionManager {
if (locale) { if (locale) {
try { try {
if (locales.indexOf(locale) == -1) { if (locales.indexOf(locale) !== -1) {
locales.forEach((l) => { await setCurrentLocale(locale as Locales);
if (l.startsWith(locale.split("-")[0])) {
setCurrentLocale(l);
return;
}
});
} else { } else {
setCurrentLocale(locale as Locales); const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0]));
if (nonRegionSpecificLocale) {
await setCurrentLocale(nonRegionSpecificLocale);
}
} }
} catch (err) { } catch (err) {
console.warn("Could not set locale", err); console.warn("Could not set locale", err);

View File

@ -4,7 +4,7 @@ import { Character } from "../Entity/Character";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type { PointInterface } from "../../Connexion/ConnexionModels"; import type { PointInterface } from "../../Connexion/ConnexionModels";
import type { PlayerAnimationDirections } from "../Player/Animation"; import type { PlayerAnimationDirections } from "../Player/Animation";
import type { Unsubscriber } from "svelte/store"; import { get, Unsubscriber } from "svelte/store";
import type { ActivatableInterface } from "../Game/ActivatableInterface"; import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
const actions: ActionsMenuAction[] = []; const actions: ActionsMenuAction[] = [];
if (this.visitCardUrl) { if (this.visitCardUrl) {
actions.push({ actions.push({
actionName: LL.woka.menu.businessCard(), actionName: get(LL).woka.menu.businessCard(),
protected: true, protected: true,
priority: 1, priority: 1,
callback: () => { callback: () => {
@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface {
actions.push({ actions.push({
actionName: blackListManager.isBlackListed(this.userUuid) actionName: blackListManager.isBlackListed(this.userUuid)
? LL.report.block.unblock() ? get(LL).report.block.unblock()
: LL.report.block.block(), : get(LL).report.block.block(),
protected: true, protected: true,
priority: -1, priority: -1,
style: "is-error", style: "is-error",

View File

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

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const de_DE: Translation = { const de_DE: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "Deutsch",
country: "Deutschland",
audio, audio,
camera, camera,
chat, chat,

View File

@ -14,8 +14,6 @@ import emoji from "./emoji";
import trigger from "./trigger"; import trigger from "./trigger";
const en_US: BaseTranslation = { const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio, audio,
camera, camera,
chat, chat,

View File

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

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const fr_FR: Translation = { const fr_FR: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "Français",
country: "France",
audio, audio,
camera, camera,
chat, chat,

View File

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

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const zh_CN: Translation = { const zh_CN: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "中文",
country: "中国",
audio, audio,
camera, camera,
chat, chat,

View File

@ -8,7 +8,7 @@
"moduleResolution": "node", "moduleResolution": "node",
//"module": "CommonJS", //"module": "CommonJS",
"module": "ESNext", "module": "ESNext",
"target": "ES2017", "target": "ES2020",
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"jsx": "react", "jsx": "react",

View File

@ -2991,10 +2991,10 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typesafe-i18n@^2.59.0: typesafe-i18n@^5.3.5:
version "2.59.0" version "5.3.5"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4" resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.3.5.tgz#8561648a2be0df660404aa087993f3eee584cb87"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg== integrity sha512-ZjCCQ2lCyyvUThtxJblXoxwpr62paOjMRi/Kia1PSEh3gRfwPvEorABS0zTdF6lZ75MQXoz0WqtobChVjkO5mQ==
typescript@*: typescript@*:
version "4.3.2" version "4.3.2"