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",
|
"EditorConfig.EditorConfig",
|
||||||
"GregorBiswanger.json2ts",
|
"GregorBiswanger.json2ts",
|
||||||
"stylelint.vscode-stylelint",
|
"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",
|
"domain": "codeberg.org",
|
||||||
"type": "Gitea"
|
"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"
|
"testTsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fluent/langneg": "^0.7.0",
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.5",
|
"@vap/shiki": "0.10.5",
|
||||||
|
|
|
@ -16,6 +16,9 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fluent/langneg':
|
||||||
|
specifier: ^0.7.0
|
||||||
|
version: 0.7.0
|
||||||
'@sapphi-red/web-noise-suppressor':
|
'@sapphi-red/web-noise-suppressor':
|
||||||
specifier: 0.3.3
|
specifier: 0.3.3
|
||||||
version: 0.3.3
|
version: 0.3.3
|
||||||
|
@ -385,6 +388,10 @@ packages:
|
||||||
resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==}
|
resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
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':
|
'@humanwhocodes/config-array@0.11.10':
|
||||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
|
@ -2661,6 +2668,8 @@ snapshots:
|
||||||
|
|
||||||
'@eslint/js@8.46.0': {}
|
'@eslint/js@8.46.0': {}
|
||||||
|
|
||||||
|
'@fluent/langneg@0.7.0': {}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.10':
|
'@humanwhocodes/config-array@0.11.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 1.2.1
|
'@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}
|
* @type {import("esbuild").BuildOptions}
|
||||||
*/
|
*/
|
||||||
|
@ -260,8 +293,8 @@ export const commonOpts = {
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, translationPlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
external: ["~plugins", "~git-hash", "~git-remote", "~translations", "/assets/*"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
jsxFragment: "VencordFragment",
|
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;
|
export default remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "~translations" {
|
||||||
|
const translations: Record<string, Record<string, any>>;
|
||||||
|
export default translations;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "file://*" {
|
declare module "file://*" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
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