Skip to content

Commit

Permalink
Handles locales as Vite assets
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Nov 14, 2024
1 parent 137a53d commit 99d5886
Show file tree
Hide file tree
Showing 32 changed files with 99 additions and 29 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/translations-download.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ 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
with:
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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,15 @@ 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
{
...
"some_new_key": "",
...
}
```
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
Expand Down
2 changes: 1 addition & 1 deletion i18next-parser.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
8 changes: 4 additions & 4 deletions localazy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
]
Expand All @@ -22,7 +22,7 @@
"download": {
"files": [
{
"output": "public/locales/${langLsrDash}/${file}"
"output": "locales/${langLsrDash}/${file}"
}
],
"includeSourceLang": "${includeSourceLang|false}",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/@types/i18next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
70 changes: 68 additions & 2 deletions src/initializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -74,6 +139,7 @@ export class Initializer {
nsSeparator: false,
pluralSeparator: "_",
contextSeparator: "|",
supportedLngs,
interpolation: {
escapeValue: false, // React has built-in XSS protections
},
Expand Down
2 changes: 1 addition & 1 deletion src/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
16 changes: 1 addition & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down

0 comments on commit 99d5886

Please sign in to comment.