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:
parent
1c9caa690a
commit
9e0f43d542
@ -1,5 +1,3 @@
|
||||
src/Messages/generated
|
||||
src/Messages/JsonMessages
|
||||
src/i18n/i18n-svelte.ts
|
||||
src/i18n/i18n-types.ts
|
||||
src/i18n/i18n-util.ts
|
||||
src/i18n/i18n-*.ts
|
||||
|
@ -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",
|
||||
"adapter": "svelte"
|
||||
}
|
@ -60,7 +60,7 @@
|
||||
"standardized-audio-context": "^25.2.4",
|
||||
"ts-deferred": "^1.0.4",
|
||||
"ts-proto": "^1.96.0",
|
||||
"typesafe-i18n": "^2.59.0",
|
||||
"typesafe-i18n": "^5.3.5",
|
||||
"uuidv4": "^6.2.10",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
|
@ -88,16 +88,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function translateMenuName(menu: MenuItem) {
|
||||
if (menu.type === "scripting") {
|
||||
return menu.label;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
$: subMenuTranslations = $subMenusStore.map((subMenu) =>
|
||||
subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
|
||||
);
|
||||
$: activeSubMenuTranslation =
|
||||
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@ -106,20 +101,20 @@
|
||||
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
|
||||
<h2>{$LL.menu.title()}</h2>
|
||||
<nav>
|
||||
{#each $subMenusStore as submenu}
|
||||
{#each $subMenusStore as submenu, i}
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={() => void switchMenu(submenu)}
|
||||
>
|
||||
{translateMenuName(submenu)}
|
||||
{subMenuTranslations[i]}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<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}>×</button>
|
||||
<h2>{translateMenuName(activeSubMenu)}</h2>
|
||||
<h2>{activeSubMenuTranslation}</h2>
|
||||
<svelte:component this={activeComponent} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,12 +28,12 @@
|
||||
let previewCameraPrivacySettings = valueCameraPrivacySettings;
|
||||
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
|
||||
|
||||
function saveSetting() {
|
||||
async function saveSetting() {
|
||||
let change = false;
|
||||
|
||||
if (valueLocale !== previewValueLocale) {
|
||||
previewValueLocale = valueLocale;
|
||||
setCurrentLocale(valueLocale as Locales);
|
||||
await setCurrentLocale(valueLocale as Locales);
|
||||
}
|
||||
|
||||
if (valueVideo !== previewValueVideo) {
|
||||
@ -174,7 +174,7 @@
|
||||
<div class="nes-select is-dark">
|
||||
<select class="languages-switcher" bind:value={valueLocale}>
|
||||
{#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}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -363,15 +363,13 @@ class ConnectionManager {
|
||||
|
||||
if (locale) {
|
||||
try {
|
||||
if (locales.indexOf(locale) == -1) {
|
||||
locales.forEach((l) => {
|
||||
if (l.startsWith(locale.split("-")[0])) {
|
||||
setCurrentLocale(l);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (locales.indexOf(locale) !== -1) {
|
||||
await setCurrentLocale(locale as Locales);
|
||||
} else {
|
||||
setCurrentLocale(locale as Locales);
|
||||
const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0]));
|
||||
if (nonRegionSpecificLocale) {
|
||||
await setCurrentLocale(nonRegionSpecificLocale);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Could not set locale", err);
|
||||
|
@ -4,7 +4,7 @@ import { Character } from "../Entity/Character";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import type { PointInterface } from "../../Connexion/ConnexionModels";
|
||||
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 CancelablePromise from "cancelable-promise";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
const actions: ActionsMenuAction[] = [];
|
||||
if (this.visitCardUrl) {
|
||||
actions.push({
|
||||
actionName: LL.woka.menu.businessCard(),
|
||||
actionName: get(LL).woka.menu.businessCard(),
|
||||
protected: true,
|
||||
priority: 1,
|
||||
callback: () => {
|
||||
@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
|
||||
actions.push({
|
||||
actionName: blackListManager.isBlackListed(this.userUuid)
|
||||
? LL.report.block.unblock()
|
||||
: LL.report.block.block(),
|
||||
? get(LL).report.block.unblock()
|
||||
: get(LL).report.block.block(),
|
||||
protected: true,
|
||||
priority: -1,
|
||||
style: "is-error",
|
||||
|
4
front/src/i18n/.gitignore
vendored
4
front/src/i18n/.gitignore
vendored
@ -1,3 +1 @@
|
||||
i18n-svelte.ts
|
||||
i18n-types.ts
|
||||
i18n-util.ts
|
||||
i18n-*.ts
|
||||
|
@ -16,8 +16,6 @@ import trigger from "./trigger";
|
||||
|
||||
const de_DE: Translation = {
|
||||
...(en_US as Translation),
|
||||
language: "Deutsch",
|
||||
country: "Deutschland",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -14,8 +14,6 @@ import emoji from "./emoji";
|
||||
import trigger from "./trigger";
|
||||
|
||||
const en_US: BaseTranslation = {
|
||||
language: "English",
|
||||
country: "United States",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { AsyncFormattersInitializer } from "typesafe-i18n";
|
||||
import type { FormattersInitializer } 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 () => {
|
||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = async () => {
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
};
|
||||
|
@ -16,8 +16,6 @@ import trigger from "./trigger";
|
||||
|
||||
const fr_FR: Translation = {
|
||||
...(en_US as Translation),
|
||||
language: "Français",
|
||||
country: "France",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -1,52 +1,44 @@
|
||||
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
|
||||
import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable";
|
||||
import { initI18n, setLocale } from "./i18n-svelte";
|
||||
import { setLocale } from "./i18n-svelte";
|
||||
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";
|
||||
|
||||
export const localeDetector = async () => {
|
||||
const exist = localStorage.getItem(localStorageProperty);
|
||||
let detectedLocale: Locales = fallbackLocale as Locales;
|
||||
let detectedLocale: Locales = fallbackLocale;
|
||||
|
||||
if (exist) {
|
||||
const localStorageDetector = initLocalStorageDetector(localStorageProperty);
|
||||
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales;
|
||||
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector);
|
||||
} 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);
|
||||
setLocale(locale).catch(() => {
|
||||
console.log("Cannot reload the locale!");
|
||||
});
|
||||
await loadLocaleAsync(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() {
|
||||
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);
|
||||
});
|
||||
});
|
||||
// backwards compatibility
|
||||
if (!Intl.DisplayNames) {
|
||||
return { id: locale, language, region };
|
||||
}
|
||||
|
||||
return localesObject;
|
||||
}
|
||||
|
||||
export const displayableLocales = getDisplayableLocales();
|
||||
return {
|
||||
id: locale,
|
||||
language: new Intl.DisplayNames(locale, { type: "language" }).of(language),
|
||||
region: new Intl.DisplayNames(locale, { type: "region" }).of(region),
|
||||
};
|
||||
});
|
||||
|
@ -16,8 +16,6 @@ import trigger from "./trigger";
|
||||
|
||||
const zh_CN: Translation = {
|
||||
...(en_US as Translation),
|
||||
language: "中文",
|
||||
country: "中国",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -8,7 +8,7 @@
|
||||
"moduleResolution": "node",
|
||||
//"module": "CommonJS",
|
||||
"module": "ESNext",
|
||||
"target": "ES2017",
|
||||
"target": "ES2020",
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react",
|
||||
|
@ -2991,10 +2991,10 @@ type-fest@^0.21.3:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
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==
|
||||
typesafe-i18n@^5.3.5:
|
||||
version "5.3.5"
|
||||
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.3.5.tgz#8561648a2be0df660404aa087993f3eee584cb87"
|
||||
integrity sha512-ZjCCQ2lCyyvUThtxJblXoxwpr62paOjMRi/Kia1PSEh3gRfwPvEorABS0zTdF6lZ75MQXoz0WqtobChVjkO5mQ==
|
||||
|
||||
typescript@*:
|
||||
version "4.3.2"
|
||||
|
Loading…
Reference in New Issue
Block a user