mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-24 00:38:31 +08:00
Merge pull request #2776 from element-hq/quenting/locales-as-assets
Handle locales as Vite assets
This commit is contained in:
commit
4c1c818ba9
4
.github/workflows/translations-download.yaml
vendored
4
.github/workflows/translations-download.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Prune i18n
|
||||
run: "rm -R public/locales"
|
||||
run: "rm -R locales"
|
||||
|
||||
- name: Download translation files
|
||||
uses: localazy/download@0a79880fb66150601e3b43606fab69c88123c087 # v1.1.0
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
groups: "-p includeSourceLang:true"
|
||||
|
||||
- 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
|
||||
run: yarn prettier:format
|
||||
|
@ -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. 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
|
||||
{
|
||||
...
|
||||
@ -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:
|
||||
|
||||
```jsonc
|
||||
|
@ -22,7 +22,7 @@ export default {
|
||||
],
|
||||
},
|
||||
locales: ["en-GB"],
|
||||
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
||||
output: "locales/$LOCALE/$NAMESPACE.json",
|
||||
input: ["src/**/*.{ts,tsx}"],
|
||||
sort: true,
|
||||
};
|
||||
|
@ -7,13 +7,13 @@
|
||||
"features": ["plural_postfix_us", "filter_untranslated"],
|
||||
"files": [
|
||||
{
|
||||
"pattern": "public/locales/en-GB/*.json",
|
||||
"pattern": "locales/en-GB/*.json",
|
||||
"lang": "inherited"
|
||||
},
|
||||
{
|
||||
"group": "existing",
|
||||
"pattern": "public/locales/*/*.json",
|
||||
"excludes": ["public/locales/en-GB/*.json"],
|
||||
"pattern": "locales/*/*.json",
|
||||
"excludes": ["locales/en-GB/*.json"],
|
||||
"lang": "${autodetectLang}"
|
||||
}
|
||||
]
|
||||
@ -22,7 +22,7 @@
|
||||
"download": {
|
||||
"files": [
|
||||
{
|
||||
"output": "public/locales/${langLsrDash}/${file}"
|
||||
"output": "locales/${langLsrDash}/${file}"
|
||||
}
|
||||
],
|
||||
"includeSourceLang": "${includeSourceLang|false}",
|
||||
|
@ -84,7 +84,6 @@
|
||||
"history": "^4.0.0",
|
||||
"i18next": "^23.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.0.0",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"knip": "^5.27.2",
|
||||
|
2
src/@types/i18next.d.ts
vendored
2
src/@types/i18next.d.ts
vendored
@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import "i18next";
|
||||
// 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" {
|
||||
interface CustomTypeOptions {
|
||||
|
@ -5,10 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
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 LanguageDetector from "i18next-browser-languagedetector";
|
||||
import Backend from "i18next-http-backend";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
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 { 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 {
|
||||
None,
|
||||
Loading,
|
||||
@ -74,6 +139,7 @@ export class Initializer {
|
||||
nsSeparator: false,
|
||||
pluralSeparator: "_",
|
||||
contextSeparator: "|",
|
||||
supportedLngs,
|
||||
interpolation: {
|
||||
escapeValue: false, // React has built-in XSS protections
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ import { cleanup } from "@testing-library/react";
|
||||
import "vitest-axe/extend-expect";
|
||||
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";
|
||||
|
||||
// Bare-minimum i18n config
|
||||
|
@ -60,6 +60,25 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
build: {
|
||||
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,
|
||||
resolve: {
|
||||
|
16
yarn.lock
16
yarn.lock
@ -4138,13 +4138,6 @@ cosmiconfig@^8.1.3:
|
||||
parse-json "^5.2.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:
|
||||
version "7.0.3"
|
||||
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:
|
||||
"@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:
|
||||
version "9.0.2"
|
||||
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"
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
node-fetch@^2.6.12, node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
|
Loading…
Reference in New Issue
Block a user