Handles locales as Vite assets

This commit is contained in:
Quentin Gliech 2024-11-14 11:53:43 +01:00
parent 137a53dbee
commit 99d5886e32
No known key found for this signature in database
GPG Key ID: 22D62B84552719FC
32 changed files with 99 additions and 29 deletions

View File

@ -24,7 +24,7 @@ jobs:
run: "yarn install --frozen-lockfile" run: "yarn install --frozen-lockfile"
- name: Prune i18n - name: Prune i18n
run: "rm -R public/locales" run: "rm -R locales"
- name: Download translation files - name: Download translation files
uses: localazy/download@0a79880fb66150601e3b43606fab69c88123c087 # v1.1.0 uses: localazy/download@0a79880fb66150601e3b43606fab69c88123c087 # v1.1.0
@ -32,7 +32,7 @@ jobs:
groups: "-p includeSourceLang:true" groups: "-p includeSourceLang:true"
- name: Fix the owner of the downloaded files - name: Fix the owner of the downloaded files
run: "sudo chown runner:docker -R public/locales" run: "sudo chown runner:docker -R locales"
- name: Prettier - name: Prettier
run: yarn prettier:format run: yarn prettier:format

View File

@ -213,7 +213,7 @@ To add a new translation key you can do these steps:
1. Add the new key entry to the code where the new key is used: `t("some_new_key")` 1. Add the new key entry to the code where the new key is used: `t("some_new_key")`
1. Run `yarn i18n` to extract the new key and update the translation files. This 1. Run `yarn i18n` to extract the new key and update the translation files. This
will add a skeleton entry to the `public/locales/en-GB/app.json` file: will add a skeleton entry to the `locales/en-GB/app.json` file:
```jsonc ```jsonc
{ {
... ...
@ -221,7 +221,7 @@ To add a new translation key you can do these steps:
... ...
} }
``` ```
1. Update the skeleton entry in the `public/locales/en-GB/app.json` file with 1. Update the skeleton entry in the `locales/en-GB/app.json` file with
the English translation: the English translation:
```jsonc ```jsonc

View File

@ -22,7 +22,7 @@ export default {
], ],
}, },
locales: ["en-GB"], locales: ["en-GB"],
output: "public/locales/$LOCALE/$NAMESPACE.json", output: "locales/$LOCALE/$NAMESPACE.json",
input: ["src/**/*.{ts,tsx}"], input: ["src/**/*.{ts,tsx}"],
sort: true, sort: true,
}; };

View File

@ -7,13 +7,13 @@
"features": ["plural_postfix_us", "filter_untranslated"], "features": ["plural_postfix_us", "filter_untranslated"],
"files": [ "files": [
{ {
"pattern": "public/locales/en-GB/*.json", "pattern": "locales/en-GB/*.json",
"lang": "inherited" "lang": "inherited"
}, },
{ {
"group": "existing", "group": "existing",
"pattern": "public/locales/*/*.json", "pattern": "locales/*/*.json",
"excludes": ["public/locales/en-GB/*.json"], "excludes": ["locales/en-GB/*.json"],
"lang": "${autodetectLang}" "lang": "${autodetectLang}"
} }
] ]
@ -22,7 +22,7 @@
"download": { "download": {
"files": [ "files": [
{ {
"output": "public/locales/${langLsrDash}/${file}" "output": "locales/${langLsrDash}/${file}"
} }
], ],
"includeSourceLang": "${includeSourceLang|false}", "includeSourceLang": "${includeSourceLang|false}",

View File

@ -84,7 +84,6 @@
"history": "^4.0.0", "history": "^4.0.0",
"i18next": "^23.0.0", "i18next": "^23.0.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.0.0",
"i18next-parser": "^9.0.0", "i18next-parser": "^9.0.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"knip": "^5.27.2", "knip": "^5.27.2",

View File

@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import "i18next"; import "i18next";
// import all namespaces (for the default language, only) // import all namespaces (for the default language, only)
import app from "../../public/locales/en-GB/app.json"; import app from "../../locales/en-GB/app.json";
declare module "i18next" { declare module "i18next" {
interface CustomTypeOptions { interface CustomTypeOptions {

View File

@ -5,10 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import i18n from "i18next"; import i18n, {
type BackendModule,
type ReadCallback,
type ResourceKey,
} from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
@ -19,6 +22,68 @@ import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel"; import { ElementCallOpenTelemetry } from "./otel/otel";
import { platform } from "./Platform"; import { platform } from "./Platform";
// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// {
// "../locales/en-GB/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
// ...
// }
const locales = import.meta.glob<string>("../locales/*/*.json", {
query: "?url",
import: "default",
eager: true,
});
const getLocaleUrl = (
language: string,
namespace: string,
): string | undefined => locales[`../locales/${language}/${namespace}.json`];
const supportedLngs = [
...new Set(
Object.keys(locales).map((url) => {
// The URLs are of the form ../locales/en-GB/app.json
// This extracts the language code from the URL
const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1];
if (!lang) {
throw new Error(`Could not parse locale URL ${url}`);
}
return lang;
}),
),
];
// A backend that fetches the locale files from the URLs generated by the glob above
const Backend = {
type: "backend",
init(): void {},
read(language: string, namespace: string, callback: ReadCallback): void {
(async (): Promise<ResourceKey> => {
const url = getLocaleUrl(language, namespace);
if (!url) {
throw new Error(
`Namespace ${namespace} for locale ${language} not found`,
);
}
const response = await fetch(url, {
credentials: "omit",
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw Error(`Failed to fetch ${url}`);
}
return await response.json();
})().then(
(data) => callback(null, data),
(error) => callback(error, null),
);
},
} satisfies BackendModule;
enum LoadState { enum LoadState {
None, None,
Loading, Loading,
@ -74,6 +139,7 @@ export class Initializer {
nsSeparator: false, nsSeparator: false,
pluralSeparator: "_", pluralSeparator: "_",
contextSeparator: "|", contextSeparator: "|",
supportedLngs,
interpolation: { interpolation: {
escapeValue: false, // React has built-in XSS protections escapeValue: false, // React has built-in XSS protections
}, },

View File

@ -16,7 +16,7 @@ import { cleanup } from "@testing-library/react";
import "vitest-axe/extend-expect"; import "vitest-axe/extend-expect";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import EN_GB from "../public/locales/en-GB/app.json"; import EN_GB from "../locales/en-GB/app.json";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
// Bare-minimum i18n config // Bare-minimum i18n config

View File

@ -60,6 +60,25 @@ export default defineConfig(({ mode }) => {
}, },
build: { build: {
sourcemap: true, sourcemap: true,
rollupOptions: {
output: {
assetFileNames: ({ originalFileNames }) => {
if (originalFileNames) {
for (const name of originalFileNames) {
// Custom asset name for locales to include the locale code in the filename
const match = name.match(/locales\/([^/]+)\/(.+)\.json$/);
if (match) {
const [, locale, filename] = match;
return `assets/${locale}-${filename}-[hash].json`;
}
}
}
// Default naming fallback
return "assets/[name]-[hash][extname]";
},
},
},
}, },
plugins, plugins,
resolve: { resolve: {

View File

@ -4138,13 +4138,6 @@ cosmiconfig@^8.1.3:
parse-json "^5.2.0" parse-json "^5.2.0"
path-type "^4.0.0" path-type "^4.0.0"
cross-fetch@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==
dependencies:
node-fetch "^2.6.12"
cross-spawn@^7.0.0, cross-spawn@^7.0.2: cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -5469,13 +5462,6 @@ i18next-browser-languagedetector@^8.0.0:
dependencies: dependencies:
"@babel/runtime" "^7.23.2" "@babel/runtime" "^7.23.2"
i18next-http-backend@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz#b25516446ae6f251ce8231e70e6ffbca833d46a5"
integrity sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==
dependencies:
cross-fetch "4.0.0"
i18next-parser@^9.0.0: i18next-parser@^9.0.0:
version "9.0.2" version "9.0.2"
resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.2.tgz#f9d627422d33c352967556c8724975d58f1f5a95" resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.2.tgz#f9d627422d33c352967556c8724975d58f1f5a95"
@ -6331,7 +6317,7 @@ node-addon-api@^7.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-fetch@^2.6.12, node-fetch@^2.6.7: node-fetch@^2.6.7:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==