bleh
This commit is contained in:
parent
2f23da64a0
commit
e38ee7aa9c
19 changed files with 557 additions and 93 deletions
10
i18n/en-US/translation.json
Normal file
10
i18n/en-US/translation.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"description": "Assists with translating Vencord",
|
||||||
|
"settings": {
|
||||||
|
"forceLocale": {
|
||||||
|
"description": "Force a different locale for Vencord to use",
|
||||||
|
"followDiscord": "Default (follow Discord)"
|
||||||
|
},
|
||||||
|
"showMissingKeys": "If the current locale is missing translations, show the keys instead of falling back to English"
|
||||||
|
}
|
||||||
|
}
|
||||||
97
i18n/en-US/vencord.json
Normal file
97
i18n/en-US/vencord.json
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"addonCard": {
|
||||||
|
"new": "NEW"
|
||||||
|
},
|
||||||
|
"cloud": {
|
||||||
|
"integrations": {
|
||||||
|
"header": "Cloud Integrations",
|
||||||
|
"reauthenticateOtherClient": "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will need to re-authenticate to continue using them. Click here to go to the settings page to do so!"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"header": "Cloud Settings",
|
||||||
|
"updatedRemote": "Your settings have been updated! Click here to restart to fully apply changes!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contributorCard": {
|
||||||
|
"contributionsButton": "See what you've contributed to",
|
||||||
|
"description": "Since you've contributed to Equicord you now have a cool new badge!",
|
||||||
|
"subtitle": "Thank you for contributing!",
|
||||||
|
"title": "Contributions"
|
||||||
|
},
|
||||||
|
"donate": "Donate",
|
||||||
|
"donorCard": {
|
||||||
|
"donated": {
|
||||||
|
"vencordDescription": "All Vencord users can see your badge! You can manage your perks by messaging @{v}.",
|
||||||
|
"equicordDescription": "All Equicord users can see your badge! You can manage your perks by making a ticket in Equicord's server.",
|
||||||
|
"description": "All Vencord users can see your Vencord donor badge, and Equicord users can see your Equicord donor badge. To change your Vencord donor badge, contact @vending.machine. For your Equicord donor badge, make a ticket in Equicord's server..",
|
||||||
|
"subtitle": "Thank you for donating!",
|
||||||
|
"title": "Donations"
|
||||||
|
},
|
||||||
|
"notDonated": {
|
||||||
|
"description": "Please consider supporting the development of Equicord by donating!",
|
||||||
|
"title": "Support the Project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"failedToRender": "Failed to render the {tab} tab. If this issue persists, try using the installer to reinstall!",
|
||||||
|
"quickActions": {
|
||||||
|
"editQuickCSS": "Edit QuickCSS",
|
||||||
|
"notificationLog": "Notification Log",
|
||||||
|
"openSettings": "Open Settings Folder",
|
||||||
|
"relaunchDiscord": "Relaunch Discord",
|
||||||
|
"title": "Quick Actions",
|
||||||
|
"viewSource": "View Source Code"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"disableMinSize": {
|
||||||
|
"title": "Disable minimum window size"
|
||||||
|
},
|
||||||
|
"enableReactDevtools": {
|
||||||
|
"title": "Enable React Developer Tools"
|
||||||
|
},
|
||||||
|
"frameless": {
|
||||||
|
"title": "Disable the window frame"
|
||||||
|
},
|
||||||
|
"hint": "Hint: You can change the position of this settings section in the <0>settings of the Settings plugin</0>!",
|
||||||
|
"macosVibrancyStyle": {
|
||||||
|
"placeholder": "Window vibrancy style",
|
||||||
|
"title": "Window vibrancy style (requires restart)"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"logButton": "View Notification Log",
|
||||||
|
"settingsButton": "Notification Settings"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"contributorModal": {
|
||||||
|
"contributorInfo": {
|
||||||
|
"noPlugins": "This person has not made any plugins. They likely <0>contributed</0> to Equicord in other ways!",
|
||||||
|
"plugins_one": "This person has <0>contributed</0> to {count} plugin!",
|
||||||
|
"plugins_other": "This person has <0>contributed</0> to {count} plugins!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restartRequired": {
|
||||||
|
"body": "A restart is required to apply this change",
|
||||||
|
"cancel": "Later!",
|
||||||
|
"confirm": "Restart now",
|
||||||
|
"title": "Restart Required"
|
||||||
|
},
|
||||||
|
"title": "Settings",
|
||||||
|
"transparent": {
|
||||||
|
"description": "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart",
|
||||||
|
"title": "Enable window transparency"
|
||||||
|
},
|
||||||
|
"useQuickCss": {
|
||||||
|
"description": "Loads your Custom CSS",
|
||||||
|
"title": "Enable Custom CSS"
|
||||||
|
},
|
||||||
|
"winCtrlQ": {
|
||||||
|
"title": "Register Ctrl+Q as shortcut to close Discord (alternative to Alt+F4)"
|
||||||
|
},
|
||||||
|
"winNativeTitleBar": {
|
||||||
|
"title": "Use Windows' native title bar instead of Discord's custom one"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"settings": "Equicord Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||||
"@ffmpeg/util": "^0.12.1",
|
"@ffmpeg/util": "^0.12.1",
|
||||||
|
"@fluent/langneg": "^0.7.0",
|
||||||
"@intrnl/xxhash64": "^0.1.2",
|
"@intrnl/xxhash64": "^0.1.2",
|
||||||
"@types/less": "^3.0.8",
|
"@types/less": "^3.0.8",
|
||||||
"@types/stylus": "^0.48.42",
|
"@types/stylus": "^0.48.42",
|
||||||
|
|
|
||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -22,6 +22,9 @@ importers:
|
||||||
'@ffmpeg/util':
|
'@ffmpeg/util':
|
||||||
specifier: ^0.12.1
|
specifier: ^0.12.1
|
||||||
version: 0.12.1
|
version: 0.12.1
|
||||||
|
'@fluent/langneg':
|
||||||
|
specifier: ^0.7.0
|
||||||
|
version: 0.7.0
|
||||||
'@intrnl/xxhash64':
|
'@intrnl/xxhash64':
|
||||||
specifier: ^0.1.2
|
specifier: ^0.1.2
|
||||||
version: 0.1.2
|
version: 0.1.2
|
||||||
|
|
@ -630,6 +633,10 @@ packages:
|
||||||
resolution: {integrity: sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==}
|
resolution: {integrity: sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==}
|
||||||
engines: {node: '>=18.x'}
|
engines: {node: '>=18.x'}
|
||||||
|
|
||||||
|
'@fluent/langneg@0.7.0':
|
||||||
|
resolution: {integrity: sha512-StAM0vgsD1QK+nFikaKs9Rxe3JGNipiXrpmemNGwM4gWERBXPe9gjzsBoKjgBgq1Vyiy+xy/C652QIWY+MPyYw==}
|
||||||
|
engines: {node: '>=14.0.0', npm: '>=7.0.0'}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
@ -2889,6 +2896,8 @@ snapshots:
|
||||||
|
|
||||||
'@ffmpeg/util@0.12.1': {}
|
'@ffmpeg/util@0.12.1': {}
|
||||||
|
|
||||||
|
'@fluent/langneg@0.7.0': {}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.6':
|
'@humanfs/node@0.16.6':
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,41 @@ 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("./i18n");
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
const translationBundles = await readdir(`./i18n/${locale}`);
|
||||||
|
|
||||||
|
for (const bundle of translationBundles) {
|
||||||
|
const name = bundle.replace(/\.json$/, "");
|
||||||
|
|
||||||
|
translations[locale] ??= {};
|
||||||
|
translations[locale][name] = JSON.parse(
|
||||||
|
await readFile(`./i18n/${locale}/${bundle}`, "utf-8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: `export default ${JSON.stringify(translations)}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").BuildOptions}
|
* @type {import("esbuild").BuildOptions}
|
||||||
*/
|
*/
|
||||||
|
|
@ -353,8 +388,20 @@ export const commonOpts = {
|
||||||
sourcemap: watch ? "inline" : "external",
|
sourcemap: watch ? "inline" : "external",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
fileUrlPlugin,
|
||||||
|
gitHashPlugin,
|
||||||
|
gitRemotePlugin,
|
||||||
|
stylePlugin,
|
||||||
|
translationPlugin,
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
"~plugins",
|
||||||
|
"~git-hash",
|
||||||
|
"~git-remote",
|
||||||
|
"~translations",
|
||||||
|
"/assets/*",
|
||||||
|
],
|
||||||
inject: [join(dirname(fileURLToPath(import.meta.url)), "inject/react.mjs")],
|
inject: [join(dirname(fileURLToPath(import.meta.url)), "inject/react.mjs")],
|
||||||
jsx: "transform",
|
jsx: "transform",
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { access, readFile } from "fs/promises";
|
import { access, readFile } from "fs/promises";
|
||||||
import { join, sep } from "path";
|
import { join, sep } from "path";
|
||||||
import { normalize as posixNormalize, sep as posixSep } from "path/posix";
|
import { normalize as posixNormalize, sep as posixSep } from "path/posix";
|
||||||
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, PropertyAssignment, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
import { BigIntLiteral, CallExpression, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, LiteralExpression, NamedDeclaration, Node, NodeArray, ObjectLiteralExpression, PropertyAssignment, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
||||||
|
|
||||||
import { getPluginTarget } from "./utils.mjs";
|
import { getPluginTarget } from "./utils.mjs";
|
||||||
|
|
||||||
|
|
@ -130,6 +130,38 @@ export function parseEquicordDevs() {
|
||||||
throw new Error("Could not find EquicordDevs constant");
|
throw new Error("Could not find EquicordDevs constant");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTranslationExpression(node: Node): node is CallExpression {
|
||||||
|
if (!isCallExpression(node)) return false;
|
||||||
|
|
||||||
|
const literal = node.expression as LiteralExpression;
|
||||||
|
|
||||||
|
if (literal.text !== "t") return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTranslation(node: Node): Promise<string | null> {
|
||||||
|
if (!isTranslationExpression(node)) return null;
|
||||||
|
|
||||||
|
const translationString = node.arguments[0];
|
||||||
|
|
||||||
|
if (!isStringLiteral(translationString)) return null;
|
||||||
|
|
||||||
|
const splitPath = translationString.text.split(".");
|
||||||
|
const namespace = splitPath.shift();
|
||||||
|
const path = splitPath.join(".");
|
||||||
|
|
||||||
|
// load the namespace
|
||||||
|
const bundle = JSON.parse(
|
||||||
|
await readFile(`./i18n/en/${namespace}.json`, "utf-8")
|
||||||
|
);
|
||||||
|
|
||||||
|
const dotProp = (key: string, object: any) =>
|
||||||
|
key.split(".").reduce((obj, key) => obj?.[key], object);
|
||||||
|
|
||||||
|
return dotProp(path, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
export async function parseFile(fileName: string) {
|
export async function parseFile(fileName: string) {
|
||||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
||||||
|
|
||||||
|
|
@ -162,8 +194,11 @@ export async function parseFile(fileName: string) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
case "description":
|
case "description":
|
||||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
if (isStringLiteral(value))
|
||||||
data[key] = value.text;
|
data[key] = value.text;
|
||||||
|
else if (isTranslationExpression(value))
|
||||||
|
data[key] = (await getTranslation(value))!;
|
||||||
|
else throw fail(`${key} is not a string literal or a translation function call`);
|
||||||
break;
|
break;
|
||||||
case "patches":
|
case "patches":
|
||||||
data.hasPatches = true;
|
data.hasPatches = true;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { openSettingsTabModal, UpdaterTab } from "@components/settings";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { IS_WINDOWS } from "@utils/constants";
|
import { IS_WINDOWS } from "@utils/constants";
|
||||||
import { createAndAppendStyle } from "@utils/css";
|
import { createAndAppendStyle } from "@utils/css";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { StartAt } from "@utils/types";
|
import { StartAt } from "@utils/types";
|
||||||
|
|
||||||
import { get as dsGet } from "./api/DataStore";
|
import { get as dsGet } from "./api/DataStore";
|
||||||
|
|
@ -76,9 +77,8 @@ async function syncSettings() {
|
||||||
) {
|
) {
|
||||||
// show a notification letting them know and tell them how to fix it
|
// show a notification letting them know and tell them how to fix it
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integrations",
|
title: t("vencord.cloud.integrations.header"),
|
||||||
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " +
|
body: t("vencord.cloud.integrations.reauthenticateOtherClient"),
|
||||||
"need to re-authenticate to continue using them. Click here to go to the settings page to do so!",
|
|
||||||
color: "var(--yellow-360)",
|
color: "var(--yellow-360)",
|
||||||
onClick: () => SettingsRouter.open("EquicordCloud")
|
onClick: () => SettingsRouter.open("EquicordCloud")
|
||||||
});
|
});
|
||||||
|
|
@ -99,8 +99,8 @@ async function syncSettings() {
|
||||||
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
||||||
// of the possible ones it has (such as when your settings are newer).
|
// of the possible ones it has (such as when your settings are newer).
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloud.settings.header"),
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
body: t("vencord.cloud.settings.updatedRemote"),
|
||||||
color: "var(--green-360)",
|
color: "var(--green-360)",
|
||||||
onClick: relaunch
|
onClick: relaunch
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||||
import type { ThemeHeader } from "@utils/themes";
|
import type { ThemeHeader } from "@utils/themes";
|
||||||
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
||||||
import { usercssParse } from "@utils/themes/usercss";
|
import { usercssParse } from "@utils/themes/usercss";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { getStylusWebStoreUrl } from "@utils/web";
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Alerts, Button, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
import { Alerts, Button, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||||
|
|
@ -573,10 +574,10 @@ export function CspErrorCard() {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
|
|
||||||
Alerts.show({
|
Alerts.show({
|
||||||
title: "Restart Required",
|
title: t("vencord.settings.restartRequired.title"),
|
||||||
body: "A restart is required to apply this change",
|
body: t("vencord.settings.restartRequired.body"),
|
||||||
confirmText: "Restart now",
|
confirmText: t("vencord.settings.restartRequired.confirm"),
|
||||||
cancelText: "Later!",
|
cancelText: t("vencord.settings.restartRequired.cancel"),
|
||||||
onConfirm: relaunch
|
onConfirm: relaunch
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { classNameFactory } from "@api/Styles";
|
||||||
import { BaseText } from "@components/BaseText";
|
import { BaseText } from "@components/BaseText";
|
||||||
import { AddonBadge } from "@components/settings/PluginBadge";
|
import { AddonBadge } from "@components/settings/PluginBadge";
|
||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Tooltip, useRef } from "@webpack/common";
|
import { Tooltip, useRef } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ export function AddonCard({ disabled, isNew, sourceBadge, tooltip, name, infoBut
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
|
{isNew && <AddonBadge text={t("vencord.addonCard.new")} color="#ED4245" />}
|
||||||
</BaseText>
|
</BaseText>
|
||||||
|
|
||||||
{!!author && (
|
{!!author && (
|
||||||
|
|
|
||||||
|
|
@ -18,43 +18,26 @@
|
||||||
|
|
||||||
import { Heart } from "@components/Heart";
|
import { Heart } from "@components/Heart";
|
||||||
import { openInviteModal } from "@utils/discord";
|
import { openInviteModal } from "@utils/discord";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { ButtonProps } from "@vencord/discord-types";
|
import { ButtonProps } from "@vencord/discord-types";
|
||||||
import { Button, showToast } from "@webpack/common";
|
import { Button, showToast } from "@webpack/common";
|
||||||
|
|
||||||
export function VCDonateButton({
|
|
||||||
look = Button.Looks.LINK,
|
|
||||||
color = Button.Colors.TRANSPARENT,
|
|
||||||
...props
|
|
||||||
}: Partial<ButtonProps>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
look={look}
|
|
||||||
color={color}
|
|
||||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
|
|
||||||
className="vc-donate-button"
|
|
||||||
>
|
|
||||||
<Heart />
|
|
||||||
Donate
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DonateButton({
|
export function DonateButton({
|
||||||
look = Button.Looks.LINK,
|
look = Button.Looks.LINK,
|
||||||
color = Button.Colors.TRANSPARENT,
|
color = Button.Colors.TRANSPARENT,
|
||||||
|
link,
|
||||||
...props
|
...props
|
||||||
}: Partial<ButtonProps>) {
|
}: Partial<ButtonProps> & { link: string; }) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
look={look}
|
look={look}
|
||||||
color={color}
|
color={color}
|
||||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/thororen1234")}
|
onClick={() => VencordNative.native.openExternal(link)}
|
||||||
className="vc-donate-button"
|
className="vc-donate-button"
|
||||||
>
|
>
|
||||||
<Heart />
|
<Heart />
|
||||||
Donate
|
{t("vencord.donate")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +60,7 @@ export function InviteButton({
|
||||||
}}
|
}}
|
||||||
className="vc-donate-button"
|
className="vc-donate-button"
|
||||||
>
|
>
|
||||||
Invite
|
{t("vencord.donate")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { onlyOnce } from "@utils/onlyOnce";
|
import { onlyOnce } from "@utils/onlyOnce";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import type { ComponentType, PropsWithChildren } from "react";
|
import type { ComponentType, PropsWithChildren } from "react";
|
||||||
|
|
||||||
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
|
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
|
||||||
|
|
@ -38,7 +39,7 @@ export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||||
const wrapped = ErrorBoundary.wrap(component, {
|
const wrapped = ErrorBoundary.wrap(component, {
|
||||||
displayName: `${tab}SettingsTab`,
|
displayName: `${tab}SettingsTab`,
|
||||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
message: t("vencord.failedToRender", { tab }),
|
||||||
onError: handleSettingsTabError,
|
onError: handleSettingsTabError,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { HeadingPrimary } from "@components/Heading";
|
import { HeadingPrimary } from "@components/Heading";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Paragraph } from "@components/Paragraph";
|
|
||||||
import { EquicordDevsById, VencordDevsById } from "@utils/constants";
|
import { EquicordDevsById, VencordDevsById } from "@utils/constants";
|
||||||
import { fetchUserProfile } from "@utils/discord";
|
import { fetchUserProfile } from "@utils/discord";
|
||||||
import { classes, pluralise } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
|
import { Translate } from "@utils/translation";
|
||||||
import { User } from "@vencord/discord-types";
|
import { User } from "@vencord/discord-types";
|
||||||
import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
}, [user.id, user.username]);
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
const ContributedHyperLink = <Link href="https://github.com/Equicord/Equicord">contributed</Link>;
|
const ContributedHyperLink = <Link href="https://equicord.org/projects">contributed</Link>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -94,13 +94,16 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{plugins.length ? (
|
{plugins.length ? (
|
||||||
<Paragraph>
|
<Translate
|
||||||
{user.username} has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.plugins"
|
||||||
</Paragraph>
|
variables={{ count: plugins.length }}>
|
||||||
|
This person has {ContributedHyperLink} to some plugins!
|
||||||
|
</Translate>
|
||||||
) : (
|
) : (
|
||||||
<Paragraph>
|
<Translate i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.noPlugins">
|
||||||
{user.username} has not made any plugins. They likely {ContributedHyperLink} in other ways!
|
This person has not made any plugins. They likely {ContributedHyperLink}
|
||||||
</Paragraph>
|
to Vencord in other ways!
|
||||||
|
</Translate>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!plugins.length && (
|
{!!plugins.length && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Heading } from "@components/Heading";
|
import { Heading } from "@components/Heading";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity } from "@utils/misc";
|
import { identity } from "@utils/misc";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Select } from "@webpack/common";
|
import { Select } from "@webpack/common";
|
||||||
|
|
||||||
export function VibrancySettings() {
|
export function VibrancySettings() {
|
||||||
|
|
@ -16,11 +17,11 @@ export function VibrancySettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading>Window vibrancy style (requires restart)</Heading>
|
<Heading>{t("vencord.settings.macosVibrancyStyle.title")}</Heading>
|
||||||
<ErrorBoundary noop>
|
<ErrorBoundary noop>
|
||||||
<Select
|
<Select
|
||||||
className={Margins.bottom20}
|
className={Margins.bottom20}
|
||||||
placeholder="Window vibrancy style"
|
placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")}
|
||||||
options={[
|
options={[
|
||||||
// Sorted from most opaque to most transparent
|
// Sorted from most opaque to most transparent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { Paragraph } from "@components/Paragraph";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity } from "@utils/misc";
|
import { identity } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Button, Select, Slider } from "@webpack/common";
|
import { Button, Select, Slider } from "@webpack/common";
|
||||||
|
|
||||||
export function NotificationSection() {
|
export function NotificationSection() {
|
||||||
|
|
@ -27,10 +28,10 @@ export function NotificationSection() {
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Button onClick={openNotificationSettingsModal}>
|
<Button onClick={openNotificationSettingsModal}>
|
||||||
Notification Settings
|
{t("vencord.settings.notification.settingsButton")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={openNotificationLogModal}>
|
<Button onClick={openNotificationLogModal}>
|
||||||
View Notification Log
|
{t("vencord.settings.notification.logButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { DONOR_ROLE_ID, GUILD_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/c
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity, isAnyPluginDev } from "@utils/misc";
|
import { identity, isAnyPluginDev } from "@utils/misc";
|
||||||
import { relaunch } from "@utils/native";
|
import { relaunch } from "@utils/native";
|
||||||
|
import { t, Translate } from "@utils/translation";
|
||||||
import { Button, GuildMemberStore, React, Select, UserStore } from "@webpack/common";
|
import { Button, GuildMemberStore, React, Select, UserStore } from "@webpack/common";
|
||||||
import BadgeAPI from "plugins/_api/badges";
|
import BadgeAPI from "plugins/_api/badges";
|
||||||
|
|
||||||
|
|
@ -64,13 +65,14 @@ function EquicordSettings() {
|
||||||
> = [
|
> = [
|
||||||
{
|
{
|
||||||
key: "useQuickCss",
|
key: "useQuickCss",
|
||||||
title: "Enable Custom CSS",
|
title: t("vencord.settings.useQuickCss.title"),
|
||||||
|
description: t("vencord.settings.useQuickCss.description"),
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
},
|
},
|
||||||
!IS_WEB && {
|
!IS_WEB && {
|
||||||
key: "enableReactDevtools",
|
key: "enableReactDevtools",
|
||||||
title: "Enable React Developer Tools",
|
title: t("vencord.settings.enableReactDevtools.title"),
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
},
|
},
|
||||||
|
|
@ -78,21 +80,20 @@ function EquicordSettings() {
|
||||||
(!IS_DISCORD_DESKTOP || !isWindows
|
(!IS_DISCORD_DESKTOP || !isWindows
|
||||||
? {
|
? {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the Window Frame",
|
title: t("vencord.settings.frameless.title"),
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
key: "winNativeTitleBar",
|
key: "winNativeTitleBar",
|
||||||
title:
|
title: t("vencord.settings.winNativeTitleBar.title"),
|
||||||
"Use Windows' native title bar instead of Discord's custom one",
|
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
}),
|
}),
|
||||||
!IS_WEB && {
|
!IS_WEB && {
|
||||||
key: "transparent",
|
key: "transparent",
|
||||||
title: "Enable Window Transparency",
|
title: t("vencord.settings.transparent.title"),
|
||||||
description: "A theme that supports transparency is required or this will do nothing. Stops the window from being resizable as a side effect",
|
description: t("vencord.settings.transparent.description"),
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: {
|
warning: {
|
||||||
enabled: isWindows,
|
enabled: isWindows,
|
||||||
|
|
@ -101,32 +102,31 @@ function EquicordSettings() {
|
||||||
},
|
},
|
||||||
IS_DISCORD_DESKTOP && {
|
IS_DISCORD_DESKTOP && {
|
||||||
key: "disableMinSize",
|
key: "disableMinSize",
|
||||||
title: "Disable Minimum Window Size",
|
title: t("vencord.settings.disableMinSize.title"),
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
},
|
},
|
||||||
!IS_WEB &&
|
!IS_WEB &&
|
||||||
isWindows && {
|
isWindows && {
|
||||||
key: "winCtrlQ",
|
key: "winCtrlQ",
|
||||||
title:
|
title: t("vencord.settings.winCtrlQ.title"),
|
||||||
"Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
|
||||||
restartRequired: true,
|
restartRequired: true,
|
||||||
warning: { enabled: false },
|
warning: { enabled: false },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Equicord Settings">
|
<SettingsTab title={t("vencord.tabs.settings")}>
|
||||||
{(isEquicordDonor(user?.id) || isVencordDonor(user?.id)) ? (
|
{(isEquicordDonor(user?.id) || isVencordDonor(user?.id)) ? (
|
||||||
<SpecialCard
|
<SpecialCard
|
||||||
title="Donations"
|
title={t("vencord.donorCard.donated.title")}
|
||||||
subtitle="Thank you for donating!"
|
subtitle={t("vencord.donorCard.donated.subtitle")}
|
||||||
description={
|
description={
|
||||||
isEquicordDonor(user?.id) && isVencordDonor(user?.id)
|
isEquicordDonor(user?.id) && isVencordDonor(user?.id)
|
||||||
? "All Vencord users can see your Vencord donor badge, and Equicord users can see your Equicord donor badge. To change your Vencord donor badge, contact @vending.machine. For your Equicord donor badge, make a ticket in Equicord's server."
|
? t("vencord.donorCard.donated.description")
|
||||||
: isVencordDonor(user?.id)
|
: isVencordDonor(user?.id)
|
||||||
? "All Vencord users can see your badge! You can manage your perks by messaging @vending.machine."
|
? t("vencord.donorCard.donated.vencordDescription", { v: "vending.machine" })
|
||||||
: "All Equicord users can see your badge! You can manage your perks by making a ticket in Equicord's server."
|
: t("vencord.donorCard.donated.equicordDescription")
|
||||||
}
|
}
|
||||||
cardImage={VENNIE_DONATOR_IMAGE}
|
cardImage={VENNIE_DONATOR_IMAGE}
|
||||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||||
|
|
@ -136,8 +136,8 @@ function EquicordSettings() {
|
||||||
</SpecialCard>
|
</SpecialCard>
|
||||||
) : (
|
) : (
|
||||||
<SpecialCard
|
<SpecialCard
|
||||||
title="Support the Project"
|
title={t("vencord.donorCard.notDonated.title")}
|
||||||
description="Please consider supporting the development of Equicord by donating!"
|
description={t("vencord.donorCard.notDonated.description")}
|
||||||
cardImage={donateImage}
|
cardImage={donateImage}
|
||||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||||
backgroundColor="#c3a3ce"
|
backgroundColor="#c3a3ce"
|
||||||
|
|
@ -147,13 +147,13 @@ function EquicordSettings() {
|
||||||
)}
|
)}
|
||||||
{isAnyPluginDev(user?.id) && (
|
{isAnyPluginDev(user?.id) && (
|
||||||
<SpecialCard
|
<SpecialCard
|
||||||
title="Contributions"
|
title={t("vencord.contributorCard.title")}
|
||||||
subtitle="Thank you for contributing!"
|
subtitle={t("vencord.contributorCard.subtitle")}
|
||||||
description="Since you've contributed to Equicord you now have a cool new badge!"
|
description={t("vencord.contributorCard.description")}
|
||||||
cardImage={COZY_CONTRIB_IMAGE}
|
cardImage={COZY_CONTRIB_IMAGE}
|
||||||
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
||||||
backgroundColor="#EDCC87"
|
backgroundColor="#EDCC87"
|
||||||
buttonTitle="See what you've contributed to"
|
buttonTitle={t("vencord.contributorCard.contributionsButton")}
|
||||||
buttonOnClick={() => openContributorModal(user)}
|
buttonOnClick={() => openContributorModal(user)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -164,31 +164,31 @@ function EquicordSettings() {
|
||||||
<QuickActionCard>
|
<QuickActionCard>
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={LogIcon}
|
Icon={LogIcon}
|
||||||
text="Notification Log"
|
text={t("vencord.quickActions.notificationLog")}
|
||||||
action={openNotificationLogModal}
|
action={openNotificationLogModal}
|
||||||
/>
|
/>
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={PaintbrushIcon}
|
Icon={PaintbrushIcon}
|
||||||
text="Edit QuickCSS"
|
text={t("vencord.quickActions.editQuickCSS")}
|
||||||
action={() => VencordNative.quickCss.openEditor()}
|
action={() => VencordNative.quickCss.openEditor()}
|
||||||
/>
|
/>
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={RestartIcon}
|
Icon={RestartIcon}
|
||||||
text="Relaunch Discord"
|
text={t("vencord.quickActions.relaunchDiscord")}
|
||||||
action={relaunch}
|
action={relaunch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={FolderIcon}
|
Icon={FolderIcon}
|
||||||
text="Open Settings Folder"
|
text={t("vencord.quickActions.openSettings")}
|
||||||
action={() => VencordNative.settings.openFolder()}
|
action={() => VencordNative.settings.openFolder()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={GithubIcon}
|
Icon={GithubIcon}
|
||||||
text="View Source Code"
|
text={t("vencord.quickActions.viewSource")}
|
||||||
action={() =>
|
action={() =>
|
||||||
VencordNative.native.openExternal(
|
VencordNative.native.openExternal(
|
||||||
"https://github.com/" + gitRemote,
|
"https://github.com/" + gitRemote,
|
||||||
|
|
@ -201,17 +201,19 @@ function EquicordSettings() {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<section className={Margins.top16}>
|
<section className={Margins.top16}>
|
||||||
<Heading>Settings</Heading>
|
<Heading>{t("vencord.settings.title")}</Heading>
|
||||||
<Paragraph className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
<Paragraph className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
||||||
Hint: You can change the position of this settings section in the{" "}
|
<Translate i18nKey="vencord.settings.hint">
|
||||||
<Button
|
Hint: You can change the position of this settings section in the
|
||||||
look={Button.Looks.LINK}
|
<Button
|
||||||
style={{ color: "var(--text-link)", display: "inline-block" }}
|
look={Button.Looks.LINK}
|
||||||
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||||
>
|
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||||
settings of the Settings plugin
|
>
|
||||||
</Button>
|
settings of the Settings plugin
|
||||||
!
|
</Button>
|
||||||
|
!
|
||||||
|
</Translate>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
{Switches.map(
|
{Switches.map(
|
||||||
|
|
@ -341,7 +343,7 @@ function DonateButtonComponent() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wrapTab(EquicordSettings, "Equicord Settings");
|
export default wrapTab(EquicordSettings, t("vencord.tabs.settings"));
|
||||||
|
|
||||||
export function isEquicordDonor(userId: string): boolean {
|
export function isEquicordDonor(userId: string): boolean {
|
||||||
const donorBadges = BadgeAPI.getEquicordDonorBadges(userId);
|
const donorBadges = BadgeAPI.getEquicordDonorBadges(userId);
|
||||||
|
|
|
||||||
12
src/modules.d.ts
vendored
12
src/modules.d.ts
vendored
|
|
@ -35,6 +35,18 @@ declare module "~git-remote" {
|
||||||
export default remote;
|
export default remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "~translations" {
|
||||||
|
type BaseTranslation = string | { [key: string]: BaseTranslation; };
|
||||||
|
export type TranslationBundle = {
|
||||||
|
[namespace: string]: Record<string, BaseTranslation>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translations: {
|
||||||
|
[locale: string]: TranslationBundle;
|
||||||
|
};
|
||||||
|
export default translations;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "file://*" {
|
declare module "file://*" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Heart } from "@components/Heart";
|
import { Heart } from "@components/Heart";
|
||||||
import { DonateButton, ErrorBoundary, HeadingPrimary, Paragraph, VCDonateButton } from "@components/index";
|
import { DonateButton, ErrorBoundary, HeadingPrimary, Paragraph } from "@components/index";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function VencordDonorModal() {
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Flex justifyContent="center" style={{ width: "100%" }}>
|
<Flex justifyContent="center" style={{ width: "100%" }}>
|
||||||
<VCDonateButton />
|
<DonateButton link="https://github.com/sponsors/Vendicated" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
@ -113,7 +113,7 @@ export function EquicordDonorModal() {
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Flex justifyContent="center" style={{ width: "100%" }}>
|
<Flex justifyContent="center" style={{ width: "100%" }}>
|
||||||
<DonateButton />
|
<DonateButton link="https://github.com/sponsors/thororen1234" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
|
||||||
57
src/plugins/_core/translation.ts
Normal file
57
src/plugins/_core/translation.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { availableLocales, setLocale, t } from "@utils/translation";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { i18n } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
forceLocale: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: t("translation.settings.forceLocale.description"),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: t("translation.settings.forceLocale.followDiscord"),
|
||||||
|
value: "",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
...availableLocales.map(l => ({
|
||||||
|
label: l,
|
||||||
|
value: l
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
onChange: (value: string) => {
|
||||||
|
setLocale(value || i18n.intl.currentLocale);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMissingKeys: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: t("translation.settings.showMissingKeys"),
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Translation",
|
||||||
|
required: true,
|
||||||
|
description: t("translation.description"),
|
||||||
|
authors: [Devs.lewisakura],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
flux: {
|
||||||
|
USER_SETTINGS_PROTO_UPDATE({ settings }) {
|
||||||
|
if (settings.proto.localization) // sometimes this apparently doesn't exist? not sure why
|
||||||
|
setLocale(settings.proto.localization.locale.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
setLocale(i18n.intl.currentLocale);
|
||||||
|
}
|
||||||
|
});
|
||||||
203
src/utils/translation.tsx
Normal file
203
src/utils/translation.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Settings } from "@api/Settings";
|
||||||
|
import { negotiateLanguages } from "@fluent/langneg";
|
||||||
|
import { React } from "@webpack/common";
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
import translations, { type TranslationBundle } from "~translations";
|
||||||
|
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
|
const logger = new Logger("Translations", "#7bc876");
|
||||||
|
|
||||||
|
export const availableLocales = Object.keys(translations);
|
||||||
|
|
||||||
|
let loadedLocale = translations.en; // english is the default fallback anyway
|
||||||
|
|
||||||
|
let bestLocale: string;
|
||||||
|
let lastDiscordLocale: string;
|
||||||
|
|
||||||
|
export function setLocale(locale: string) {
|
||||||
|
// discord sometimes sends the locale update multiple times so this just prevents us from constantly renegotiating
|
||||||
|
if (locale === lastDiscordLocale) return;
|
||||||
|
|
||||||
|
lastDiscordLocale = locale;
|
||||||
|
|
||||||
|
// finds the best locale based on the available ones
|
||||||
|
bestLocale = negotiateLanguages(
|
||||||
|
[locale],
|
||||||
|
Object.keys(translations),
|
||||||
|
{
|
||||||
|
defaultLocale: "en",
|
||||||
|
strategy: "lookup",
|
||||||
|
}
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
loadedLocale = translations[bestLocale];
|
||||||
|
|
||||||
|
logger.info(`Locale was updated (wanted ${locale}, negotiated 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
|
||||||
|
const dotProp = (key: string, object: any) =>
|
||||||
|
key.split(".").reduce((obj, key) => obj?.[key], object);
|
||||||
|
|
||||||
|
// translation retrieval function
|
||||||
|
function _t(key: string, bundle: TranslationBundle): string | undefined {
|
||||||
|
const translation = dotProp(key, bundle);
|
||||||
|
|
||||||
|
if (translation && typeof translation !== "string")
|
||||||
|
throw new Error(`Translation for key "${key}" is not a string!`);
|
||||||
|
|
||||||
|
if (!translation) {
|
||||||
|
if (bundle !== translations.en && !Settings.plugins.Translation.showMissingKeys) {
|
||||||
|
return _t(key, translations.en);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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. If dealing with plurals, `count` must be set.
|
||||||
|
* @returns A translated string, or the translation key if it is not valid.
|
||||||
|
*/
|
||||||
|
export function t(key: string, variables?: Record<string, any>): string {
|
||||||
|
const getter = (): string => {
|
||||||
|
if (typeof variables?.count === "number") {
|
||||||
|
// this seems plural!
|
||||||
|
const pluralTag: Intl.LDMLPluralRule = variables.count === 0 ? "zero" :
|
||||||
|
new Intl.PluralRules(bestLocale).select(variables.count);
|
||||||
|
|
||||||
|
const translation = _t(`${key}_${pluralTag}`, loadedLocale);
|
||||||
|
|
||||||
|
if (typeof translation === "string") {
|
||||||
|
return format(translation, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...okay, maybe this form just isn't in this translation, try the "other" tag
|
||||||
|
const otherTranslation = _t(`${key}_other`, loadedLocale);
|
||||||
|
|
||||||
|
if (typeof otherTranslation === "string") {
|
||||||
|
return format(otherTranslation, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// okay then, nevermind...
|
||||||
|
}
|
||||||
|
|
||||||
|
const translation = _t(key, loadedLocale);
|
||||||
|
|
||||||
|
if (!translation) return key; // for easier debugging
|
||||||
|
|
||||||
|
if (!variables) return translation;
|
||||||
|
|
||||||
|
return format(translation, variables);
|
||||||
|
};
|
||||||
|
|
||||||
|
// top level support hax (thank you vee!!)
|
||||||
|
// tl;dr: this lets you use t at the top level in objects by simulating a string, a la:
|
||||||
|
// {
|
||||||
|
// description: t("somePlugin.description")
|
||||||
|
// }
|
||||||
|
// and any future accesses of the description prop will result in an up to date translation
|
||||||
|
const descriptor = {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
writable: false,
|
||||||
|
value: getter
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.create(String.prototype, {
|
||||||
|
toString: descriptor,
|
||||||
|
valueOf: descriptor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type TranslatableChild = (string | JSX.Element);
|
||||||
|
interface TranslateProps {
|
||||||
|
/** The key to translate. */
|
||||||
|
i18nKey: string;
|
||||||
|
/** The variables to interpolate into the resultant string. If dealing with plurals, `count` must be set. */
|
||||||
|
variables?: Record<string, any>;
|
||||||
|
/** The component(s) to interpolate into the resultant string. */
|
||||||
|
children: TranslatableChild | TranslatableChild[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A translation component. Follows the same rules as {@link t}, but lets you add components to strings.
|
||||||
|
*
|
||||||
|
* It's worth noting components cannot have components inside of them as their children will be replaced with the string
|
||||||
|
* contents of the translation. If you want to do this, you will need to make a wrapper component that accepts a string
|
||||||
|
* child.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* // you can either do it plainly:
|
||||||
|
* <Translate i18nKey="vencord.website">
|
||||||
|
* <Link href="https://vencord.dev" />
|
||||||
|
* </Translate>
|
||||||
|
*
|
||||||
|
* // or, if you want to add some context:
|
||||||
|
* <Translate i18nKey="vencord.website">
|
||||||
|
* Would you like to see our <Link href="https://vencord.dev">website</Link>?
|
||||||
|
* </Translate>
|
||||||
|
*
|
||||||
|
* // string children are simply ignored by the component, so you don't have to keep it up to date with the translation
|
||||||
|
* ```
|
||||||
|
* @param param0 Component props.
|
||||||
|
*/
|
||||||
|
export function Translate({ i18nKey, variables, children: trueChildren }: TranslateProps): JSX.Element {
|
||||||
|
const children = [trueChildren].flat().filter(child => typeof child !== "string");
|
||||||
|
|
||||||
|
const translation = t(i18nKey, variables);
|
||||||
|
|
||||||
|
const parts = translation.split(/(<\d+>.*?<\/\d+>)/g);
|
||||||
|
|
||||||
|
const finalChildren = parts.map((part, index) => {
|
||||||
|
const match = part.match(/<(\d+)>(.*?)<\/\d+>/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const childIndex = parseInt(match[1], 10);
|
||||||
|
return React.cloneElement(children[childIndex], { key: index }, match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{finalChildren}</>;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue