feat: translation v2
This commit is contained in:
parent
2dc0c20462
commit
c0111169b8
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -4,6 +4,7 @@
|
|||
"EditorConfig.EditorConfig",
|
||||
"GregorBiswanger.json2ts",
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vendicated.vencord-companion"
|
||||
"Vendicated.vencord-companion",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
|
|
10
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
10
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
languageIds:
|
||||
- javascript
|
||||
- typescript
|
||||
- javascriptreact
|
||||
- typescriptreact
|
||||
|
||||
usageMatchRegex:
|
||||
- "[^\\w\\d]\\$t\\(['\"`]({key})['\"`]"
|
||||
|
||||
monopoly: true
|
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -19,5 +19,10 @@
|
|||
"domain": "codeberg.org",
|
||||
"type": "Gitea"
|
||||
}
|
||||
]
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.localesPaths": ["./translations"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.extract.keygenStyle": "camelCase",
|
||||
"i18n-ally.sortKeys": true
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"testTsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/langneg": "^0.7.0",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
|
|
|
@ -16,6 +16,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@fluent/langneg':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
'@sapphi-red/web-noise-suppressor':
|
||||
specifier: 0.3.3
|
||||
version: 0.3.3
|
||||
|
@ -385,6 +388,10 @@ packages:
|
|||
resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@fluent/langneg@0.7.0':
|
||||
resolution: {integrity: sha512-StAM0vgsD1QK+nFikaKs9Rxe3JGNipiXrpmemNGwM4gWERBXPe9gjzsBoKjgBgq1Vyiy+xy/C652QIWY+MPyYw==}
|
||||
engines: {node: '>=14.0.0', npm: '>=7.0.0'}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.10':
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
@ -2661,6 +2668,8 @@ snapshots:
|
|||
|
||||
'@eslint/js@8.46.0': {}
|
||||
|
||||
'@fluent/langneg@0.7.0': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.10':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 1.2.1
|
||||
|
|
|
@ -249,6 +249,39 @@ export const stylePlugin = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const translationPlugin = {
|
||||
name: "translation-plugin",
|
||||
setup: ({ onResolve, onLoad }) => {
|
||||
const filter = /^~translations$/;
|
||||
|
||||
onResolve({ filter }, ({ path }) => ({
|
||||
namespace: "translations", path
|
||||
}));
|
||||
onLoad({ filter, namespace: "translations" }, async () => {
|
||||
const translations = {};
|
||||
const locales = await readdir("./translations");
|
||||
|
||||
for (const locale of locales) {
|
||||
const translationBundles = await readdir(`./translations/${locale}`);
|
||||
|
||||
for (const bundle of translationBundles) {
|
||||
const name = bundle.replace(/\.json$/, "");
|
||||
|
||||
translations[locale] ??= {};
|
||||
translations[locale][name] = JSON.parse(await readFile(`./translations/${locale}/${bundle}`, "utf-8"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contents: `export default ${JSON.stringify(translations)}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").BuildOptions}
|
||||
*/
|
||||
|
@ -260,8 +293,8 @@ export const commonOpts = {
|
|||
sourcemap: watch ? "inline" : "",
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, translationPlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "~translations", "/assets/*"],
|
||||
inject: ["./scripts/build/inject/react.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
jsxFragment: "VencordFragment",
|
||||
|
|
5
src/modules.d.ts
vendored
5
src/modules.d.ts
vendored
|
@ -38,6 +38,11 @@ declare module "~git-remote" {
|
|||
export default remote;
|
||||
}
|
||||
|
||||
declare module "~translations" {
|
||||
const translations: Record<string, Record<string, any>>;
|
||||
export default translations;
|
||||
}
|
||||
|
||||
declare module "file://*" {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
|
111
src/utils/translation.ts
Normal file
111
src/utils/translation.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { negotiateLanguages } from "@fluent/langneg";
|
||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
||||
|
||||
import translations from "~translations";
|
||||
|
||||
import { Logger } from "./Logger";
|
||||
|
||||
const logger = new Logger("Translations", "#7bc876");
|
||||
|
||||
let loadedLocale: Record<string, any>;
|
||||
|
||||
let lastDiscordLocale = i18n.getLocale();
|
||||
let bestLocale: string;
|
||||
|
||||
FluxDispatcher.subscribe("USER_SETTINGS_PROTO_UPDATE", ({ settings }) => {
|
||||
if (settings.proto.localization.locale.value !== lastDiscordLocale) {
|
||||
lastDiscordLocale = settings.proto.localization.locale.value;
|
||||
|
||||
reloadLocale();
|
||||
}
|
||||
});
|
||||
|
||||
reloadLocale();
|
||||
|
||||
function reloadLocale() {
|
||||
// finds the best locale based on the available ones
|
||||
bestLocale = negotiateLanguages(
|
||||
[lastDiscordLocale],
|
||||
Object.keys(translations),
|
||||
{
|
||||
defaultLocale: "en",
|
||||
strategy: "lookup",
|
||||
}
|
||||
)[0];
|
||||
|
||||
loadedLocale = translations[bestLocale];
|
||||
|
||||
logger.info("Changed locale to", bestLocale);
|
||||
}
|
||||
|
||||
// derived from stackoverflow's string formatting function
|
||||
function format(source: string, variables: Record<string, any>) {
|
||||
for (const key in variables) {
|
||||
let formatted: string;
|
||||
|
||||
switch (typeof variables[key]) {
|
||||
case "number": {
|
||||
formatted = new Intl.NumberFormat(bestLocale).format(variables[key]);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
formatted = variables[key].toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
source = source.replace(
|
||||
new RegExp(`\\{${key}\\}`, "gi"),
|
||||
formatted
|
||||
);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
// converts a dot-notation path to an object value
|
||||
function getByPath(key: string, object: any) {
|
||||
try {
|
||||
return key.split(".").reduce((obj, key) => obj[key], object);
|
||||
} catch {
|
||||
// errors if the object doesn't contain the key
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// translation retrieval function
|
||||
function _t(key: string, bundle: any): string {
|
||||
const translation = getByPath(key, bundle);
|
||||
|
||||
if (!translation) {
|
||||
if (bundle !== translations.en) {
|
||||
return _t(key, translations.en);
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a key. Soft-fails and returns the key if it is not valid.
|
||||
* @param key The key to translate.
|
||||
* @param variables The variables to interpolate into the resultant string.
|
||||
* @returns A translated string.
|
||||
*/
|
||||
export function $t(key: string, variables?: Record<string, any>): string {
|
||||
const translation = _t(key, loadedLocale);
|
||||
|
||||
if (!variables) return translation;
|
||||
return format(translation, variables);
|
||||
}
|
||||
|
||||
$t("vencord.hello");
|
3
translations/de/vencord.json
Normal file
3
translations/de/vencord.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"hello": "Hallo {name}!"
|
||||
}
|
3
translations/en/vencord.json
Normal file
3
translations/en/vencord.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"hello": "Hello {name}!"
|
||||
}
|
Loading…
Reference in a new issue