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": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@fluent/langneg": "^0.7.0",
|
||||
"@intrnl/xxhash64": "^0.1.2",
|
||||
"@types/less": "^3.0.8",
|
||||
"@types/stylus": "^0.48.42",
|
||||
|
|
|
|||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -22,6 +22,9 @@ importers:
|
|||
'@ffmpeg/util':
|
||||
specifier: ^0.12.1
|
||||
version: 0.12.1
|
||||
'@fluent/langneg':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
'@intrnl/xxhash64':
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2
|
||||
|
|
@ -630,6 +633,10 @@ packages:
|
|||
resolution: {integrity: sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
|
@ -2889,6 +2896,8 @@ snapshots:
|
|||
|
||||
'@ffmpeg/util@0.12.1': {}
|
||||
|
||||
'@fluent/langneg@0.7.0': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@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}
|
||||
*/
|
||||
|
|
@ -353,8 +388,20 @@ export const commonOpts = {
|
|||
sourcemap: watch ? "inline" : "external",
|
||||
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: [join(dirname(fileURLToPath(import.meta.url)), "inject/react.mjs")],
|
||||
jsx: "transform",
|
||||
jsxFactory: "VencordCreateElement",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
|||
import { access, readFile } from "fs/promises";
|
||||
import { join, sep } from "path";
|
||||
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";
|
||||
|
||||
|
|
@ -130,6 +130,38 @@ export function parseEquicordDevs() {
|
|||
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) {
|
||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
||||
|
||||
|
|
@ -162,8 +194,11 @@ export async function parseFile(fileName: string) {
|
|||
switch (key) {
|
||||
case "name":
|
||||
case "description":
|
||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
||||
data[key] = value.text;
|
||||
if (isStringLiteral(value))
|
||||
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;
|
||||
case "patches":
|
||||
data.hasPatches = true;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { openSettingsTabModal, UpdaterTab } from "@components/settings";
|
|||
import { debounce } from "@shared/debounce";
|
||||
import { IS_WINDOWS } from "@utils/constants";
|
||||
import { createAndAppendStyle } from "@utils/css";
|
||||
import { t } from "@utils/translation";
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
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
|
||||
showNotification({
|
||||
title: "Cloud Integrations",
|
||||
body: "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!",
|
||||
title: t("vencord.cloud.integrations.header"),
|
||||
body: t("vencord.cloud.integrations.reauthenticateOtherClient"),
|
||||
color: "var(--yellow-360)",
|
||||
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
|
||||
// of the possible ones it has (such as when your settings are newer).
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
||||
title: t("vencord.cloud.settings.header"),
|
||||
body: t("vencord.cloud.settings.updatedRemote"),
|
||||
color: "var(--green-360)",
|
||||
onClick: relaunch
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import { useAwaiter, useForceUpdater } from "@utils/react";
|
|||
import type { ThemeHeader } from "@utils/themes";
|
||||
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
||||
import { usercssParse } from "@utils/themes/usercss";
|
||||
import { t } from "@utils/translation";
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Alerts, Button, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
|
|
@ -573,10 +574,10 @@ export function CspErrorCard() {
|
|||
forceUpdate();
|
||||
|
||||
Alerts.show({
|
||||
title: "Restart Required",
|
||||
body: "A restart is required to apply this change",
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
title: t("vencord.settings.restartRequired.title"),
|
||||
body: t("vencord.settings.restartRequired.body"),
|
||||
confirmText: t("vencord.settings.restartRequired.confirm"),
|
||||
cancelText: t("vencord.settings.restartRequired.cancel"),
|
||||
onConfirm: relaunch
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { classNameFactory } from "@api/Styles";
|
|||
import { BaseText } from "@components/BaseText";
|
||||
import { AddonBadge } from "@components/settings/PluginBadge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { t } from "@utils/translation";
|
||||
import { Tooltip, useRef } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ export function AddonCard({ disabled, isNew, sourceBadge, tooltip, name, infoBut
|
|||
{name}
|
||||
</div>
|
||||
</div>
|
||||
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
|
||||
{isNew && <AddonBadge text={t("vencord.addonCard.new")} color="#ED4245" />}
|
||||
</BaseText>
|
||||
|
||||
{!!author && (
|
||||
|
|
|
|||
|
|
@ -18,43 +18,26 @@
|
|||
|
||||
import { Heart } from "@components/Heart";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { t } from "@utils/translation";
|
||||
import { ButtonProps } from "@vencord/discord-types";
|
||||
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({
|
||||
look = Button.Looks.LINK,
|
||||
color = Button.Colors.TRANSPARENT,
|
||||
link,
|
||||
...props
|
||||
}: Partial<ButtonProps>) {
|
||||
}: Partial<ButtonProps> & { link: string; }) {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
look={look}
|
||||
color={color}
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/thororen1234")}
|
||||
onClick={() => VencordNative.native.openExternal(link)}
|
||||
className="vc-donate-button"
|
||||
>
|
||||
<Heart />
|
||||
Donate
|
||||
{t("vencord.donate")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -77,7 +60,7 @@ export function InviteButton({
|
|||
}}
|
||||
className="vc-donate-button"
|
||||
>
|
||||
Invite
|
||||
{t("vencord.donate")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import { t } from "@utils/translation";
|
||||
import type { ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
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) {
|
||||
const wrapped = ErrorBoundary.wrap(component, {
|
||||
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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import { classNameFactory } from "@api/Styles";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { HeadingPrimary } from "@components/Heading";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { EquicordDevsById, VencordDevsById } from "@utils/constants";
|
||||
import { fetchUserProfile } from "@utils/discord";
|
||||
import { classes, pluralise } from "@utils/misc";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||
import { Translate } from "@utils/translation";
|
||||
import { User } from "@vencord/discord-types";
|
||||
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));
|
||||
}, [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 (
|
||||
<>
|
||||
|
|
@ -94,13 +94,16 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
</div>
|
||||
|
||||
{plugins.length ? (
|
||||
<Paragraph>
|
||||
{user.username} has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||
</Paragraph>
|
||||
<Translate
|
||||
i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.plugins"
|
||||
variables={{ count: plugins.length }}>
|
||||
This person has {ContributedHyperLink} to some plugins!
|
||||
</Translate>
|
||||
) : (
|
||||
<Paragraph>
|
||||
{user.username} has not made any plugins. They likely {ContributedHyperLink} in other ways!
|
||||
</Paragraph>
|
||||
<Translate i18nKey="vencord.settings.plugins.contributorModal.contributorInfo.noPlugins">
|
||||
This person has not made any plugins. They likely {ContributedHyperLink}
|
||||
to Vencord in other ways!
|
||||
</Translate>
|
||||
)}
|
||||
|
||||
{!!plugins.length && (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Heading } from "@components/Heading";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { t } from "@utils/translation";
|
||||
import { Select } from "@webpack/common";
|
||||
|
||||
export function VibrancySettings() {
|
||||
|
|
@ -16,11 +17,11 @@ export function VibrancySettings() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Heading>Window vibrancy style (requires restart)</Heading>
|
||||
<Heading>{t("vencord.settings.macosVibrancyStyle.title")}</Heading>
|
||||
<ErrorBoundary noop>
|
||||
<Select
|
||||
className={Margins.bottom20}
|
||||
placeholder="Window vibrancy style"
|
||||
placeholder={t("vencord.settings.macosVibrancyStyle.placeholder")}
|
||||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { Paragraph } from "@components/Paragraph";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { Button, Select, Slider } from "@webpack/common";
|
||||
|
||||
export function NotificationSection() {
|
||||
|
|
@ -27,10 +28,10 @@ export function NotificationSection() {
|
|||
</Paragraph>
|
||||
<Flex>
|
||||
<Button onClick={openNotificationSettingsModal}>
|
||||
Notification Settings
|
||||
{t("vencord.settings.notification.settingsButton")}
|
||||
</Button>
|
||||
<Button onClick={openNotificationLogModal}>
|
||||
View Notification Log
|
||||
{t("vencord.settings.notification.logButton")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</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 { identity, isAnyPluginDev } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { t, Translate } from "@utils/translation";
|
||||
import { Button, GuildMemberStore, React, Select, UserStore } from "@webpack/common";
|
||||
import BadgeAPI from "plugins/_api/badges";
|
||||
|
||||
|
|
@ -64,13 +65,14 @@ function EquicordSettings() {
|
|||
> = [
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: "Enable Custom CSS",
|
||||
title: t("vencord.settings.useQuickCss.title"),
|
||||
description: t("vencord.settings.useQuickCss.description"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: "Enable React Developer Tools",
|
||||
title: t("vencord.settings.enableReactDevtools.title"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
|
|
@ -78,21 +80,20 @@ function EquicordSettings() {
|
|||
(!IS_DISCORD_DESKTOP || !isWindows
|
||||
? {
|
||||
key: "frameless",
|
||||
title: "Disable the Window Frame",
|
||||
title: t("vencord.settings.frameless.title"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
}
|
||||
: {
|
||||
key: "winNativeTitleBar",
|
||||
title:
|
||||
"Use Windows' native title bar instead of Discord's custom one",
|
||||
title: t("vencord.settings.winNativeTitleBar.title"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
}),
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable Window Transparency",
|
||||
description: "A theme that supports transparency is required or this will do nothing. Stops the window from being resizable as a side effect",
|
||||
title: t("vencord.settings.transparent.title"),
|
||||
description: t("vencord.settings.transparent.description"),
|
||||
restartRequired: true,
|
||||
warning: {
|
||||
enabled: isWindows,
|
||||
|
|
@ -101,32 +102,31 @@ function EquicordSettings() {
|
|||
},
|
||||
IS_DISCORD_DESKTOP && {
|
||||
key: "disableMinSize",
|
||||
title: "Disable Minimum Window Size",
|
||||
title: t("vencord.settings.disableMinSize.title"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
!IS_WEB &&
|
||||
isWindows && {
|
||||
key: "winCtrlQ",
|
||||
title:
|
||||
"Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||
title: t("vencord.settings.winCtrlQ.title"),
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTab title="Equicord Settings">
|
||||
<SettingsTab title={t("vencord.tabs.settings")}>
|
||||
{(isEquicordDonor(user?.id) || isVencordDonor(user?.id)) ? (
|
||||
<SpecialCard
|
||||
title="Donations"
|
||||
subtitle="Thank you for donating!"
|
||||
title={t("vencord.donorCard.donated.title")}
|
||||
subtitle={t("vencord.donorCard.donated.subtitle")}
|
||||
description={
|
||||
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)
|
||||
? "All Vencord users can see your badge! You can manage your perks by messaging @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.vencordDescription", { v: "vending.machine" })
|
||||
: t("vencord.donorCard.donated.equicordDescription")
|
||||
}
|
||||
cardImage={VENNIE_DONATOR_IMAGE}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
|
|
@ -136,8 +136,8 @@ function EquicordSettings() {
|
|||
</SpecialCard>
|
||||
) : (
|
||||
<SpecialCard
|
||||
title="Support the Project"
|
||||
description="Please consider supporting the development of Equicord by donating!"
|
||||
title={t("vencord.donorCard.notDonated.title")}
|
||||
description={t("vencord.donorCard.notDonated.description")}
|
||||
cardImage={donateImage}
|
||||
backgroundImage={DONOR_BACKGROUND_IMAGE}
|
||||
backgroundColor="#c3a3ce"
|
||||
|
|
@ -147,13 +147,13 @@ function EquicordSettings() {
|
|||
)}
|
||||
{isAnyPluginDev(user?.id) && (
|
||||
<SpecialCard
|
||||
title="Contributions"
|
||||
subtitle="Thank you for contributing!"
|
||||
description="Since you've contributed to Equicord you now have a cool new badge!"
|
||||
title={t("vencord.contributorCard.title")}
|
||||
subtitle={t("vencord.contributorCard.subtitle")}
|
||||
description={t("vencord.contributorCard.description")}
|
||||
cardImage={COZY_CONTRIB_IMAGE}
|
||||
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
|
||||
backgroundColor="#EDCC87"
|
||||
buttonTitle="See what you've contributed to"
|
||||
buttonTitle={t("vencord.contributorCard.contributionsButton")}
|
||||
buttonOnClick={() => openContributorModal(user)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -164,31 +164,31 @@ function EquicordSettings() {
|
|||
<QuickActionCard>
|
||||
<QuickAction
|
||||
Icon={LogIcon}
|
||||
text="Notification Log"
|
||||
text={t("vencord.quickActions.notificationLog")}
|
||||
action={openNotificationLogModal}
|
||||
/>
|
||||
<QuickAction
|
||||
Icon={PaintbrushIcon}
|
||||
text="Edit QuickCSS"
|
||||
text={t("vencord.quickActions.editQuickCSS")}
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
/>
|
||||
{!IS_WEB && (
|
||||
<QuickAction
|
||||
Icon={RestartIcon}
|
||||
text="Relaunch Discord"
|
||||
text={t("vencord.quickActions.relaunchDiscord")}
|
||||
action={relaunch}
|
||||
/>
|
||||
)}
|
||||
{!IS_WEB && (
|
||||
<QuickAction
|
||||
Icon={FolderIcon}
|
||||
text="Open Settings Folder"
|
||||
text={t("vencord.quickActions.openSettings")}
|
||||
action={() => VencordNative.settings.openFolder()}
|
||||
/>
|
||||
)}
|
||||
<QuickAction
|
||||
Icon={GithubIcon}
|
||||
text="View Source Code"
|
||||
text={t("vencord.quickActions.viewSource")}
|
||||
action={() =>
|
||||
VencordNative.native.openExternal(
|
||||
"https://github.com/" + gitRemote,
|
||||
|
|
@ -201,17 +201,19 @@ function EquicordSettings() {
|
|||
<Divider />
|
||||
|
||||
<section className={Margins.top16}>
|
||||
<Heading>Settings</Heading>
|
||||
<Heading>{t("vencord.settings.title")}</Heading>
|
||||
<Paragraph className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
||||
Hint: You can change the position of this settings section in the{" "}
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||
>
|
||||
settings of the Settings plugin
|
||||
</Button>
|
||||
!
|
||||
<Translate i18nKey="vencord.settings.hint">
|
||||
Hint: You can change the position of this settings section in the
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||
>
|
||||
settings of the Settings plugin
|
||||
</Button>
|
||||
!
|
||||
</Translate>
|
||||
</Paragraph>
|
||||
|
||||
{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 {
|
||||
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;
|
||||
}
|
||||
|
||||
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://*" {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Flex } from "@components/Flex";
|
||||
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 { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ export function VencordDonorModal() {
|
|||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Flex justifyContent="center" style={{ width: "100%" }}>
|
||||
<VCDonateButton />
|
||||
<DonateButton link="https://github.com/sponsors/Vendicated" />
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
|
|
@ -113,7 +113,7 @@ export function EquicordDonorModal() {
|
|||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Flex justifyContent="center" style={{ width: "100%" }}>
|
||||
<DonateButton />
|
||||
<DonateButton link="https://github.com/sponsors/thororen1234" />
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</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