Compare commits
36 commits
main
...
feat/trans
Author | SHA1 | Date | |
---|---|---|---|
a1ef2c4010 | |||
946738e069 | |||
77d059dd8f | |||
d216bdb6bb | |||
3f3d5379cf | |||
70cc39b5c6 | |||
2cd94221cd | |||
faea1c8224 | |||
e55c6f99e2 | |||
cc7675eccb | |||
fcc86370f9 | |||
cc22c15bcb | |||
bbd0729ed6 | |||
51770e96f2 | |||
b92a21ac7d | |||
8c4aed699d | |||
26c21c2de8 | |||
15394e106a | |||
9cc9c57f11 | |||
38624a8661 | |||
ec5f9f78d3 | |||
ef028edc0d | |||
a4d4d981e0 | |||
22e5396684 | |||
d1242633e5 | |||
becf4a4c4f | |||
c5c0732ffe | |||
f1e1f9cd44 | |||
d349689c6a | |||
16549695d1 | |||
caf1779be3 | |||
7ed73b49e5 | |||
9c9a02f9bf | |||
c0111169b8 | |||
2dc0c20462 | |||
42307ccc0e |
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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
16
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
16
.vscode/i18n-ally-custom-framework.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
languageIds:
|
||||||
|
- javascript
|
||||||
|
- typescript
|
||||||
|
- javascriptreact
|
||||||
|
- typescriptreact
|
||||||
|
|
||||||
|
usageMatchRegex:
|
||||||
|
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||||
|
- "<Translate ?.* i18nKey=\\{?['\"`]({key})['\"`]"
|
||||||
|
|
||||||
|
refactorTemplates:
|
||||||
|
- "t(\"$1\")"
|
||||||
|
- "{t(\"$1\")}"
|
||||||
|
- "<Translate i18nKey=\"$1\" />"
|
||||||
|
|
||||||
|
monopoly: true
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -21,5 +21,11 @@
|
||||||
"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,
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,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'}
|
||||||
|
@ -2665,6 +2672,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
|
||||||
|
|
|
@ -292,6 +292,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}
|
||||||
*/
|
*/
|
||||||
|
@ -303,8 +336,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",
|
||||||
|
|
|
@ -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, 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, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
||||||
|
|
||||||
import { getPluginTarget } from "./utils.mjs";
|
import { getPluginTarget } from "./utils.mjs";
|
||||||
|
|
||||||
|
@ -90,6 +90,38 @@ function parseDevs() {
|
||||||
throw new Error("Could not find Devs constant");
|
throw new Error("Could not find Devs 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(`./translations/en/${namespace}.json`, "utf-8")
|
||||||
|
);
|
||||||
|
|
||||||
|
const dotProp = (key: string, object: any) =>
|
||||||
|
key.split(".").reduce((obj, key) => obj?.[key], object);
|
||||||
|
|
||||||
|
return dotProp(path, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
async function parseFile(fileName: string) {
|
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);
|
||||||
|
|
||||||
|
@ -120,10 +152,16 @@ async function parseFile(fileName: string) {
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
case "description":
|
|
||||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
||||||
data[key] = value.text;
|
data[key] = value.text;
|
||||||
break;
|
break;
|
||||||
|
case "description":
|
||||||
|
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":
|
case "patches":
|
||||||
data.hasPatches = true;
|
data.hasPatches = true;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
import { localStorage } from "./utils/localStorage";
|
import { localStorage } from "./utils/localStorage";
|
||||||
import { relaunch } from "./utils/native";
|
import { relaunch } from "./utils/native";
|
||||||
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
||||||
|
import { t } from "./utils/translation";
|
||||||
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
@ -54,9 +55,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.cloudIntegrations"),
|
||||||
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " +
|
body: t("vencord.cloud.integrations.reauthenticate"),
|
||||||
"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("VencordCloud")
|
onClick: () => SettingsRouter.open("VencordCloud")
|
||||||
});
|
});
|
||||||
|
@ -76,8 +76,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.cloudSettings"),
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
body: t("vencord.cloud.settings.updated"),
|
||||||
color: "var(--green-360)",
|
color: "var(--green-360)",
|
||||||
onClick: relaunch
|
onClick: relaunch
|
||||||
});
|
});
|
||||||
|
@ -100,8 +100,8 @@ async function init() {
|
||||||
await update();
|
await update();
|
||||||
if (Settings.autoUpdateNotification)
|
if (Settings.autoUpdateNotification)
|
||||||
setTimeout(() => showNotification({
|
setTimeout(() => showNotification({
|
||||||
title: "Vencord has been updated!",
|
title: t("vencord.update.updated"),
|
||||||
body: "Click here to restart",
|
body: t("vencord.update.clickToRestart"),
|
||||||
permanent: true,
|
permanent: true,
|
||||||
noPersist: true,
|
noPersist: true,
|
||||||
onClick: relaunch
|
onClick: relaunch
|
||||||
|
@ -110,8 +110,8 @@ async function init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => showNotification({
|
setTimeout(() => showNotification({
|
||||||
title: "A Vencord update is available!",
|
title: t("vencord.update.available"),
|
||||||
body: "Click here to view the update",
|
body: t("vencord.update.clickToView"),
|
||||||
permanent: true,
|
permanent: true,
|
||||||
noPersist: true,
|
noPersist: true,
|
||||||
onClick: openUpdaterModal!
|
onClick: openUpdaterModal!
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { LazyComponent } from "@utils/react";
|
import { LazyComponent } from "@utils/react";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
@ -85,11 +86,11 @@ const ErrorBoundary = LazyComponent(() => {
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || t("vencord.errorBoundaryDescription");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{ overflow: "hidden" }}>
|
<ErrorCard style={{ overflow: "hidden" }}>
|
||||||
<h1>Oh no!</h1>
|
<h1>{t("vencord.ohNo")}</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
|
|
|
@ -12,8 +12,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { DevsById } from "@utils/constants";
|
import { DevsById } 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 { t, Translate } from "@utils/translation";
|
||||||
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
@ -60,8 +61,6 @@ 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://vencord.dev/source">contributed</Link>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
|
@ -88,15 +87,11 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{plugins.length ? (
|
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
<Translate i18nKey="vencord.pluginContributed" variables={{ count: plugins.length }}>
|
||||||
|
<Link href="https://vencord.dev/source" />
|
||||||
|
</Translate>
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
) : (
|
|
||||||
<Forms.FormText>
|
|
||||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
|
||||||
</Forms.FormText>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!plugins.length && (
|
{!!plugins.length && (
|
||||||
<div className={cl("plugins")}>
|
<div className={cl("plugins")}>
|
||||||
|
@ -105,7 +100,7 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
key={p.name}
|
key={p.name}
|
||||||
plugin={p}
|
plugin={p}
|
||||||
disabled={p.required ?? false}
|
disabled={p.required ?? false}
|
||||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
onRestartNeeded={() => showToast(t("vencord.pluginRestart"))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { proxyLazy } from "@utils/lazy";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
|
@ -138,7 +139,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
if (!hasSettings || !plugin.options) {
|
if (!hasSettings || !plugin.options) {
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>{t("vencord.noSettings")}</Forms.FormText>;
|
||||||
} else {
|
} else {
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||||
if (setting.hidden) return null;
|
if (setting.hidden) return null;
|
||||||
|
@ -274,7 +275,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Forms.FormSection className={Margins.bottom16}>
|
<Forms.FormSection className={Margins.bottom16}>
|
||||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">{t("vencord.settings")}</Forms.FormTitle>
|
||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -289,7 +290,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
<Tooltip text={t("vencord.settingsErrors")} shouldShow={!canSubmit()}>
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
<Button
|
<Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
|
@ -299,12 +300,12 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
disabled={!canSubmit()}
|
disabled={!canSubmit()}
|
||||||
>
|
>
|
||||||
Save & Close
|
{t("vencord.saveAndClose")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>{t("vencord.settingsSaveError", { saveError })}</Text>}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>}
|
</ModalFooter>}
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
|
||||||
|
@ -64,19 +65,19 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
<Card className={cl("info-card", { "restart-card": required })}>
|
<Card className={cl("info-card", { "restart-card": required })}>
|
||||||
{required ? (
|
{required ? (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{t("vencord.pluginHeader.reloadHeader")}</Forms.FormTitle>
|
||||||
<Forms.FormText className={cl("dep-text")}>
|
<Forms.FormText className={cl("dep-text")}>
|
||||||
Restart now to apply new plugins and their settings
|
{t("vencord.pluginHeader.reloadDescription")}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Button onClick={() => location.reload()}>
|
<Button onClick={() => location.reload()}>
|
||||||
Restart
|
{t("vencord.pluginHeader.restart")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{t("vencord.pluginHeader.managementHeader")}</Forms.FormTitle>
|
||||||
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
<Forms.FormText>{t("vencord.pluginHeader.iconInformation")}</Forms.FormText>
|
||||||
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
<Forms.FormText>{t("vencord.pluginHeader.cogWheel")}</Forms.FormText>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -209,10 +210,10 @@ export default function PluginSettings() {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => void (changes.hasChanges && Alerts.show({
|
return () => void (changes.hasChanges && Alerts.show({
|
||||||
title: "Restart required",
|
title: t("vencord.restartRequired"),
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
<p>The following plugins require a restart:</p>
|
<p>$t("vencord.pluginsNeedRestart")</p>
|
||||||
<div>{changes.map((s, i) => (
|
<div>{changes.map((s, i) => (
|
||||||
<>
|
<>
|
||||||
{i > 0 && ", "}
|
{i > 0 && ", "}
|
||||||
|
@ -221,8 +222,8 @@ export default function PluginSettings() {
|
||||||
))}</div>
|
))}</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
confirmText: "Restart now",
|
confirmText: t("vencord.restartNow"),
|
||||||
cancelText: "Later!",
|
cancelText: t("vencord.restartLater"),
|
||||||
onConfirm: () => location.reload()
|
onConfirm: () => location.reload()
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -296,7 +297,7 @@ export default function PluginSettings() {
|
||||||
|
|
||||||
if (isRequired) {
|
if (isRequired) {
|
||||||
const tooltipText = p.required
|
const tooltipText = p.required
|
||||||
? "This plugin is required for Vencord to function."
|
? t("vencord.requiredPlugin")
|
||||||
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
||||||
|
|
||||||
requiredPlugins.push(
|
requiredPlugins.push(
|
||||||
|
@ -331,18 +332,18 @@ export default function PluginSettings() {
|
||||||
<ReloadRequiredCard required={changes.hasChanges} />
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Filters
|
{t("vencord.pluginFilters")}
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
|
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
|
<TextInput autoFocus value={searchValue.value} placeholder={t("vencord.search.placeholder")} onChange={onSearch} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
{ label: t("vencord.search.all"), value: SearchStatus.ALL, default: true },
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
{ label: t("vencord.search.enabled"), value: SearchStatus.ENABLED },
|
||||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
{ label: t("vencord.search.disabled"), value: SearchStatus.DISABLED },
|
||||||
{ label: "Show New", value: SearchStatus.NEW }
|
{ label: t("vencord.search.new"), value: SearchStatus.NEW }
|
||||||
]}
|
]}
|
||||||
serialize={String}
|
serialize={String}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
|
@ -353,7 +354,7 @@ export default function PluginSettings() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>{t("vencord.plugins")}</Forms.FormTitle>
|
||||||
|
|
||||||
{plugins.length || requiredPlugins.length
|
{plugins.length || requiredPlugins.length
|
||||||
? (
|
? (
|
||||||
|
@ -371,7 +372,7 @@ export default function PluginSettings() {
|
||||||
<Forms.FormDivider className={Margins.top20} />
|
<Forms.FormDivider className={Margins.top20} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Required Plugins
|
{t("vencord.requiredPlugins")}
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{requiredPlugins.length
|
{requiredPlugins.length
|
||||||
|
@ -386,7 +387,7 @@ export default function PluginSettings() {
|
||||||
function makeDependencyList(deps: string[]) {
|
function makeDependencyList(deps: string[]) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
<Forms.FormText>{t("vencord.pluginRequiredBy")}</Forms.FormText>
|
||||||
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import "./addonCard.css";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Badge } from "@components/Badge";
|
import { Badge } from "@components/Badge";
|
||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Text, useRef } from "@webpack/common";
|
import { Text, useRef } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
</div>{isNew && <Badge text={t("vencord.new")} color="#ED4245" />}
|
||||||
</Text>
|
</Text>
|
||||||
{!!author && (
|
{!!author && (
|
||||||
<Text variant="text-md/normal" className={cl("author")}>
|
<Text variant="text-md/normal" className={cl("author")}>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { Button, Card, Text } from "@webpack/common";
|
import { Button, Card, Text } from "@webpack/common";
|
||||||
|
|
||||||
import { SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
@ -29,21 +30,19 @@ function BackupRestoreTab() {
|
||||||
<SettingsTab title="Backup & Restore">
|
<SettingsTab title="Backup & Restore">
|
||||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<strong>Warning</strong>
|
<strong>{t("vencord.warning")}</strong>
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
<span>{t("vencord.backupAndRestore.importWarning")}</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
You can import and export your Vencord settings as a JSON file.
|
{t("vencord.backupAndRestore.description")}
|
||||||
This allows you to easily transfer your settings to another device,
|
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
Settings Export contains:
|
{t("vencord.backupAndRestore.exportContains")}
|
||||||
<ul>
|
<ul>
|
||||||
<li>— Custom QuickCSS</li>
|
<li>— {t("vencord.backupAndRestore.customQuickcss")}</li>
|
||||||
<li>— Theme Links</li>
|
<li>— {t("vencord.backupAndRestore.themeLinks")}</li>
|
||||||
<li>— Plugin Settings</li>
|
<li>— {t("vencord.backupAndRestore.pluginSettings")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Text>
|
</Text>
|
||||||
<Flex>
|
<Flex>
|
||||||
|
@ -51,13 +50,13 @@ function BackupRestoreTab() {
|
||||||
onClick={() => uploadSettingsBackup()}
|
onClick={() => uploadSettingsBackup()}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
>
|
>
|
||||||
Import Settings
|
{t("vencord.backupAndRestore.importSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={downloadSettingsBackup}
|
onClick={downloadSettingsBackup}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
>
|
>
|
||||||
Export Settings
|
{t("vencord.backupAndRestore.exportSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { Link } from "@components/Link";
|
||||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||||
|
import { t, Translate } from "@utils/translation";
|
||||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import { SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
@ -46,8 +47,8 @@ async function eraseAllData() {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
|
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integrations",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
body: t("vencord.cloud.integrations.eraseError", { status: res.status }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -57,8 +58,8 @@ async function eraseAllData() {
|
||||||
await deauthorizeCloud();
|
await deauthorizeCloud();
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integrations",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: "Successfully erased all data.",
|
body: t("vencord.cloud.integrations.eraseSuccess"),
|
||||||
color: "var(--green-360)"
|
color: "var(--green-360)"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -70,8 +71,7 @@ function SettingsSyncSection() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||||
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
|
{t("vencord.cloud.settings.description")}
|
||||||
minimal effort.
|
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Switch
|
<Switch
|
||||||
key="cloud-sync"
|
key="cloud-sync"
|
||||||
|
@ -79,7 +79,7 @@ function SettingsSyncSection() {
|
||||||
value={cloud.settingsSync}
|
value={cloud.settingsSync}
|
||||||
onChange={v => { cloud.settingsSync = v; }}
|
onChange={v => { cloud.settingsSync = v; }}
|
||||||
>
|
>
|
||||||
Settings Sync
|
{t("vencord.settingsSync")}
|
||||||
</Switch>
|
</Switch>
|
||||||
<div className="vc-cloud-settings-sync-grid">
|
<div className="vc-cloud-settings-sync-grid">
|
||||||
<Button
|
<Button
|
||||||
|
@ -87,9 +87,9 @@ function SettingsSyncSection() {
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => putCloudSettings(true)}
|
onClick={() => putCloudSettings(true)}
|
||||||
>
|
>
|
||||||
Sync to Cloud
|
{t("vencord.cloud.settings.syncToCloud")}
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
<Tooltip text={t("vencord.cloud.settings.overwriteWarning")}>
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
<Button
|
<Button
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
@ -99,7 +99,7 @@ function SettingsSyncSection() {
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => getCloudSettings(true, true)}
|
onClick={() => getCloudSettings(true, true)}
|
||||||
>
|
>
|
||||||
Sync from Cloud
|
{t("vencord.cloud.settings.syncFromCloud")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -109,7 +109,7 @@ function SettingsSyncSection() {
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => deleteCloudSettings()}
|
onClick={() => deleteCloudSettings()}
|
||||||
>
|
>
|
||||||
Delete Cloud Settings
|
{t("vencord.cloud.settings.deleteCloudSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
@ -121,12 +121,12 @@ function CloudTab() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Vencord Cloud">
|
<SettingsTab title="Vencord Cloud">
|
||||||
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
|
<Forms.FormSection title={t("vencord.cloudSettings")} className={Margins.top16}>
|
||||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
<Translate i18nKey="vencord.cloud.integrations.description">
|
||||||
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
<Link href="https://vencord.dev/cloud/privacy" />
|
||||||
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
<Link href="https://github.com/Vencord/Backend" />
|
||||||
can host it yourself.
|
</Translate>
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Switch
|
<Switch
|
||||||
key="backend"
|
key="backend"
|
||||||
|
@ -137,13 +137,13 @@ function CloudTab() {
|
||||||
else
|
else
|
||||||
settings.cloud.authenticated = v;
|
settings.cloud.authenticated = v;
|
||||||
}}
|
}}
|
||||||
note="This will request authorization if you have not yet set up cloud integrations."
|
note={t("vencord.cloud.integrations.authorizationNote")}
|
||||||
>
|
>
|
||||||
Enable Cloud Integrations
|
{t("vencord.cloud.integrations.enable")}
|
||||||
</Switch>
|
</Switch>
|
||||||
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{t("vencord.cloud.integrations.backendUrl")}</Forms.FormTitle>
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
Which backend to use when using cloud integrations.
|
{t("vencord.cloud.integrations.backendNote")}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
key="backendUrl"
|
key="backendUrl"
|
||||||
|
@ -166,25 +166,24 @@ function CloudTab() {
|
||||||
await authorizeCloud();
|
await authorizeCloud();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reauthorise
|
{t("vencord.reauthorise")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
disabled={!settings.cloud.authenticated}
|
disabled={!settings.cloud.authenticated}
|
||||||
onClick={() => Alerts.show({
|
onClick={() => Alerts.show({
|
||||||
title: "Are you sure?",
|
title: t("vencord.areYouSure"),
|
||||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
body: t("vencord.cloud.integrations.eraseWarning"),
|
||||||
onConfirm: eraseAllData,
|
onConfirm: eraseAllData,
|
||||||
confirmText: "Erase it!",
|
confirmText: t("vencord.cloud.integrations.eraseIt"),
|
||||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||||
cancelText: "Nevermind"
|
cancelText: t("vencord.nevermind")
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Erase All Data
|
Erase All Data
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top16} />
|
<Forms.FormDivider className={Margins.top16} />
|
||||||
</Forms.FormSection >
|
</Forms.FormSection >
|
||||||
<SettingsSyncSection />
|
<SettingsSyncSection />
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { showItemInFolder } from "@utils/native";
|
import { showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { findByPropsLazy, findLazy } from "@webpack";
|
import { findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
@ -202,60 +203,68 @@ function ThemesTab() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="vc-settings-card">
|
<Card className="vc-settings-card">
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{t("vencord.themes.findThemes")}</Forms.FormTitle>
|
||||||
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
BetterDiscord Themes
|
{t("vencord.themes.betterDiscord")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||||
</div>
|
</div>
|
||||||
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
<Forms.FormText>{t("vencord.themes.betterDiscordNote")}</Forms.FormText>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Forms.FormSection title="Local Themes">
|
<Forms.FormSection title={t("vencord.themes.local")}>
|
||||||
<QuickActionCard>
|
<Card className="vc-settings-quick-actions-card">
|
||||||
<>
|
<>
|
||||||
{IS_WEB ?
|
{IS_WEB ?
|
||||||
(
|
(
|
||||||
<QuickAction
|
<Button
|
||||||
text={
|
size={Button.Sizes.SMALL}
|
||||||
<span style={{ position: "relative" }}>
|
disabled={themeDirPending}
|
||||||
Upload Theme
|
>
|
||||||
|
{t("vencord.themes.upload")}
|
||||||
<FileInput
|
<FileInput
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={onFileUpload}
|
onChange={onFileUpload}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
filters={[{ extensions: ["css"] }]}
|
filters={[{ extensions: ["css"] }]}
|
||||||
/>
|
/>
|
||||||
</span>
|
</Button>
|
||||||
}
|
|
||||||
Icon={PlusIcon}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<QuickAction
|
<QuickAction
|
||||||
text="Open Themes Folder"
|
text={t("vencord.themes.openFolder")}
|
||||||
action={() => showItemInFolder(themeDir!)}
|
action={() => showItemInFolder(themeDir!)}
|
||||||
disabled={themeDirPending}
|
disabled={themeDirPending}
|
||||||
Icon={FolderIcon}
|
>
|
||||||
/>
|
{t("vencord.themes.openFolder")}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
<QuickAction
|
<Button
|
||||||
text="Load missing Themes"
|
onClick={refreshLocalThemes}
|
||||||
action={refreshLocalThemes}
|
size={Button.Sizes.SMALL}
|
||||||
Icon={RestartIcon}
|
>
|
||||||
/>
|
{t("vencord.themes.loadMissing")}
|
||||||
<QuickAction
|
</Button>
|
||||||
text="Edit QuickCSS"
|
<Button
|
||||||
action={() => VencordNative.quickCss.openEditor()}
|
onClick={() => VencordNative.quickCss.openEditor()}
|
||||||
Icon={PaintbrushIcon}
|
size={Button.Sizes.SMALL}
|
||||||
/>
|
>
|
||||||
|
{t("vencord.themes.editQuickCss")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{Vencord.Settings.plugins.ClientTheme.enabled && (
|
{Vencord.Settings.plugins.ClientTheme.enabled && (
|
||||||
<QuickAction
|
<Button
|
||||||
text="Edit ClientTheme"
|
onClick={() => openModal(modalProps => (
|
||||||
action={() => openPluginModal(Vencord.Plugins.plugins.ClientTheme)}
|
<PluginModal
|
||||||
Icon={PencilIcon}
|
{...modalProps}
|
||||||
|
plugin={Vencord.Plugins.plugins.ClientTheme}
|
||||||
|
onRestartNeeded={() => { }}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
>
|
||||||
|
{t("clientTheme.edit")}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</QuickActionCard>
|
</QuickActionCard>
|
||||||
|
@ -295,12 +304,12 @@ function ThemesTab() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="vc-settings-card vc-text-selectable">
|
<Card className="vc-settings-card vc-text-selectable">
|
||||||
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{t("vencord.themes.pasteLinks")}</Forms.FormTitle>
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
<Forms.FormText>{t("vencord.themes.oneLinkPerLine")}</Forms.FormText>
|
||||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
<Forms.FormText>{t("vencord.themes.useDirect")}</Forms.FormText>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Forms.FormSection title="Online Themes" tag="h5">
|
<Forms.FormSection title={t("vencord.themes.online")} tag="h5">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={themeText}
|
value={themeText}
|
||||||
onChange={setThemeText}
|
onChange={setThemeText}
|
||||||
|
@ -329,13 +338,13 @@ function ThemesTab() {
|
||||||
className="vc-settings-tab-bar-item"
|
className="vc-settings-tab-bar-item"
|
||||||
id={ThemeTab.LOCAL}
|
id={ThemeTab.LOCAL}
|
||||||
>
|
>
|
||||||
Local Themes
|
{t("vencord.themes.local")}
|
||||||
</TabBar.Item>
|
</TabBar.Item>
|
||||||
<TabBar.Item
|
<TabBar.Item
|
||||||
className="vc-settings-tab-bar-item"
|
className="vc-settings-tab-bar-item"
|
||||||
id={ThemeTab.ONLINE}
|
id={ThemeTab.ONLINE}
|
||||||
>
|
>
|
||||||
Online Themes
|
{t("vencord.themes.online")}
|
||||||
</TabBar.Item>
|
</TabBar.Item>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
|
||||||
export function handleComponentFailed() {
|
export function handleComponentFailed() {
|
||||||
maybePromptToUpdate(
|
maybePromptToUpdate(
|
||||||
"Uh Oh! Failed to render this Page." +
|
t("vencord.failureUpdate")
|
||||||
" However, there is an update available that might fix it." +
|
|
||||||
" Would you like to update and restart now?"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
5
src/modules.d.ts
vendored
5
src/modules.d.ts
vendored
|
@ -43,6 +43,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;
|
||||||
|
|
27
src/plugins/_core/translation.ts
Normal file
27
src/plugins/_core/translation.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { setLocale } from "@utils/translation";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { i18n } from "@webpack/common";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Translation",
|
||||||
|
required: true,
|
||||||
|
description: "Assists with translating Vencord",
|
||||||
|
authors: [Devs.lewisakura],
|
||||||
|
|
||||||
|
flux: {
|
||||||
|
USER_SETTINGS_PROTO_UPDATE({ settings }) {
|
||||||
|
setLocale(settings.proto.localization.locale.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
setLocale(i18n.getLocale());
|
||||||
|
}
|
||||||
|
});
|
|
@ -10,6 +10,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
|
import { t } from "@utils/translation";
|
||||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
||||||
|
@ -64,8 +65,8 @@ function ThemeSettings() {
|
||||||
<div className="client-theme-settings">
|
<div className="client-theme-settings">
|
||||||
<div className="client-theme-container">
|
<div className="client-theme-container">
|
||||||
<div className="client-theme-settings-labels">
|
<div className="client-theme-settings-labels">
|
||||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">{t("clientTheme.settingsTitle")}</Forms.FormTitle>
|
||||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
<Forms.FormText>{t("clientTheme.settingsDescription")}</Forms.FormText>
|
||||||
</div>
|
</div>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={parseInt(settings.store.color, 16)}
|
color={parseInt(settings.store.color, 16)}
|
||||||
|
@ -78,12 +79,12 @@ function ThemeSettings() {
|
||||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||||
<div className="client-theme-warning">
|
<div className="client-theme-warning">
|
||||||
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
|
<Forms.FormText>{t("clientTheme.warningTitle")}</Forms.FormText>
|
||||||
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
|
{contrastWarning && <Forms.FormText>{t("clientTheme.warnings.badContrast")}</Forms.FormText>}
|
||||||
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
|
{nitroThemeEnabled && <Forms.FormText>{t("clientTheme.warnings.nitro")}</Forms.FormText>}
|
||||||
</div>
|
</div>
|
||||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>{t(`clientTheme.switchToOpposite.${oppositeTheme}`)}</Button>}
|
||||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>{t("clientTheme.disableNitroTheme")}</Button>}
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,18 +93,18 @@ function ThemeSettings() {
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
color: {
|
color: {
|
||||||
description: "Color your Discord client theme will be based around. Light mode isn't supported",
|
description: t("clientTheme.colorDescription"),
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
default: "313338",
|
default: "313338",
|
||||||
component: () => <ThemeSettings />
|
component: () => <ThemeSettings />
|
||||||
},
|
},
|
||||||
resetColor: {
|
resetColor: {
|
||||||
description: "Reset Theme Color",
|
description: t("clientTheme.resetColorDescription"),
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
default: "313338",
|
default: "313338",
|
||||||
component: () => (
|
component: () => (
|
||||||
<Button onClick={() => onPickColor(0x313338)}>
|
<Button onClick={() => onPickColor(0x313338)}>
|
||||||
Reset Theme Color
|
{t("clientTheme.resetButton")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -112,7 +113,7 @@ const settings = definePluginSettings({
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ClientTheme",
|
name: "ClientTheme",
|
||||||
authors: [Devs.F53, Devs.Nuckyz],
|
authors: [Devs.F53, Devs.Nuckyz],
|
||||||
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
|
description: t("clientTheme.description"),
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
startAt: StartAt.DOMContentLoaded,
|
startAt: StartAt.DOMContentLoaded,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { openModal } from "./modal";
|
import { openModal } from "./modal";
|
||||||
|
import { t } from "./translation";
|
||||||
|
|
||||||
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
||||||
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||||
|
@ -83,8 +84,8 @@ export async function authorizeCloud() {
|
||||||
var { clientId, redirectUri } = await oauthConfiguration.json();
|
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||||
} catch {
|
} catch {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: "Setup failed (couldn't retrieve OAuth configuration)."
|
body: t("vencord.cloud.integrations.setupFailure.oauth")
|
||||||
});
|
});
|
||||||
Settings.cloud.authenticated = false;
|
Settings.cloud.authenticated = false;
|
||||||
return;
|
return;
|
||||||
|
@ -113,22 +114,22 @@ export async function authorizeCloud() {
|
||||||
cloudLogger.info("Authorized with secret");
|
cloudLogger.info("Authorized with secret");
|
||||||
await setAuthorization(secret);
|
await setAuthorization(secret);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: "Cloud integrations enabled!"
|
body: t("vencord.cloud.integrations.enabled")
|
||||||
});
|
});
|
||||||
Settings.cloud.authenticated = true;
|
Settings.cloud.authenticated = true;
|
||||||
} else {
|
} else {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: "Setup failed (no secret returned?)."
|
body: t("vencord.cloud.integrations.setupFailure.missingSecret")
|
||||||
});
|
});
|
||||||
Settings.cloud.authenticated = false;
|
Settings.cloud.authenticated = false;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
cloudLogger.error("Failed to authorize", e);
|
cloudLogger.error("Failed to authorize", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Integration",
|
title: t("vencord.cloudIntegrations"),
|
||||||
body: `Setup failed (${e.toString()}).`
|
body: t("vencord.cloud.integrations.setupFailure.generic", { error: e.toString() })
|
||||||
});
|
});
|
||||||
Settings.cloud.authenticated = false;
|
Settings.cloud.authenticated = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import { Clipboard, Toasts } from "@webpack/common";
|
import { Clipboard, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import { DevsById } from "./constants";
|
import { DevsById } from "./constants";
|
||||||
|
import { t } from "./translation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls .join(" ") on the arguments
|
* Calls .join(" ") on the arguments
|
||||||
|
@ -35,11 +36,13 @@ export function sleep(ms: number): Promise<void> {
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise(r => setTimeout(r, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
|
export function copyWithToast(text: string, toastMessage?: string) {
|
||||||
|
toastMessage ??= t("vencord.copiedToClipboard");
|
||||||
|
|
||||||
if (Clipboard.SUPPORTS_COPY) {
|
if (Clipboard.SUPPORTS_COPY) {
|
||||||
Clipboard.copy(text);
|
Clipboard.copy(text);
|
||||||
} else {
|
} else {
|
||||||
toastMessage = "Your browser does not support copying to clipboard";
|
toastMessage = t("vencord.clipboardNotSupported");
|
||||||
}
|
}
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
message: toastMessage,
|
message: toastMessage,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { deflateSync, inflateSync } from "fflate";
|
||||||
import { getCloudAuth, getCloudUrl } from "./cloud";
|
import { getCloudAuth, getCloudUrl } from "./cloud";
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { relaunch } from "./native";
|
import { relaunch } from "./native";
|
||||||
|
import { t } from "./translation";
|
||||||
import { chooseFile, saveFile } from "./web";
|
import { chooseFile, saveFile } from "./web";
|
||||||
|
|
||||||
export async function importSettings(data: string) {
|
export async function importSettings(data: string) {
|
||||||
|
@ -68,10 +69,10 @@ const toast = (type: number, message: string) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const toastSuccess = () =>
|
const toastSuccess = () =>
|
||||||
toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
|
toast(Toasts.Type.SUCCESS, t("vencord.importedSettings"));
|
||||||
|
|
||||||
const toastFailure = (err: any) =>
|
const toastFailure = (err: any) =>
|
||||||
toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);
|
toast(Toasts.Type.FAILURE, t("vencord.failedToImport", { error: String(err) }));
|
||||||
|
|
||||||
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
@ -128,8 +129,8 @@ export async function putCloudSettings(manual?: boolean) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
cloudSettingsLogger.error(`Failed to sync up, API returned ${res.status}`);
|
cloudSettingsLogger.error(`Failed to sync up, API returned ${res.status}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
|
body: t("vencord.cloud.settings.syncErrorUp.api", { status: res.status.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -143,16 +144,16 @@ export async function putCloudSettings(manual?: boolean) {
|
||||||
|
|
||||||
if (manual) {
|
if (manual) {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "Synchronized settings to the cloud!",
|
body: t("vencord.cloud.settings.syncSuccess"),
|
||||||
noPersist: true,
|
noPersist: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
cloudSettingsLogger.error("Failed to sync up", e);
|
cloudSettingsLogger.error("Failed to sync up", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not synchronize settings to the cloud (${e.toString()}).`,
|
body: t("vencord.cloud.settings.syncErrorUp.generic", { error: e.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -173,8 +174,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
cloudSettingsLogger.info("No settings on the cloud");
|
cloudSettingsLogger.info("No settings on the cloud");
|
||||||
if (shouldNotify)
|
if (shouldNotify)
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "There are no settings in the cloud.",
|
body: t("vencord.cloud.settings.nothingOnline"),
|
||||||
noPersist: true
|
noPersist: true
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
@ -184,8 +185,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
cloudSettingsLogger.info("Settings up to date");
|
cloudSettingsLogger.info("Settings up to date");
|
||||||
if (shouldNotify)
|
if (shouldNotify)
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "Your settings are up to date.",
|
body: t("vencord.cloud.settings.upToDate"),
|
||||||
noPersist: true
|
noPersist: true
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
@ -194,8 +195,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
cloudSettingsLogger.error(`Failed to sync down, API returned ${res.status}`);
|
cloudSettingsLogger.error(`Failed to sync down, API returned ${res.status}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
|
body: t("vencord.cloud.settings.syncErrorDown.api", { status: res.status.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
@ -208,8 +209,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
if (!force && written < localWritten) {
|
if (!force && written < localWritten) {
|
||||||
if (shouldNotify)
|
if (shouldNotify)
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "Your local settings are newer than the cloud ones.",
|
body: t("vencord.cloud.settings.localNewer"),
|
||||||
noPersist: true,
|
noPersist: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -227,8 +228,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
cloudSettingsLogger.info("Settings loaded from cloud successfully");
|
cloudSettingsLogger.info("Settings loaded from cloud successfully");
|
||||||
if (shouldNotify)
|
if (shouldNotify)
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
body: t("vencord.cloud.settings.updated"),
|
||||||
color: "var(--green-360)",
|
color: "var(--green-360)",
|
||||||
onClick: IS_WEB ? () => location.reload() : relaunch,
|
onClick: IS_WEB ? () => location.reload() : relaunch,
|
||||||
noPersist: true
|
noPersist: true
|
||||||
|
@ -238,8 +239,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
cloudSettingsLogger.error("Failed to sync down", e);
|
cloudSettingsLogger.error("Failed to sync down", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not synchronize settings from the cloud (${e.toString()}).`,
|
body: t("vencord.cloud.settings.syncErrorDown.generic", { error: e.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -257,8 +258,8 @@ export async function deleteCloudSettings() {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
cloudSettingsLogger.error(`Failed to delete, API returned ${res.status}`);
|
cloudSettingsLogger.error(`Failed to delete, API returned ${res.status}`);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not delete settings (API returned ${res.status}).`,
|
body: t("vencord.cloud.settings.deleteError.api", { error: res.status.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -266,15 +267,15 @@ export async function deleteCloudSettings() {
|
||||||
|
|
||||||
cloudSettingsLogger.info("Settings deleted from cloud successfully");
|
cloudSettingsLogger.info("Settings deleted from cloud successfully");
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: "Settings deleted from cloud!",
|
body: t("vencord.cloud.settings.deleted"),
|
||||||
color: "var(--green-360)"
|
color: "var(--green-360)"
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
cloudSettingsLogger.error("Failed to delete", e);
|
cloudSettingsLogger.error("Failed to delete", e);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Cloud Settings",
|
title: t("vencord.cloudSettings"),
|
||||||
body: `Could not delete settings (${e.toString()}).`,
|
body: t("vencord.cloud.settings.deleteError.generic", { error: e.toString() }),
|
||||||
color: "var(--red-360)"
|
color: "var(--red-360)"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
179
src/utils/translation.tsx
Normal file
179
src/utils/translation.tsx
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
/*
|
||||||
|
* 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 { React } from "@webpack/common";
|
||||||
|
|
||||||
|
import translations from "~translations";
|
||||||
|
|
||||||
|
import { localStorage } from "./localStorage";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
|
const logger = new Logger("Translations", "#7bc876");
|
||||||
|
|
||||||
|
let loadedLocale: Record<string, any>;
|
||||||
|
|
||||||
|
let lastDiscordLocale: string = localStorage.getItem("vcLocale")!;
|
||||||
|
let bestLocale: string;
|
||||||
|
|
||||||
|
export function setLocale(locale: string) {
|
||||||
|
if (locale === lastDiscordLocale) return;
|
||||||
|
|
||||||
|
localStorage.setItem("vcLocale", locale);
|
||||||
|
|
||||||
|
lastDiscordLocale = locale;
|
||||||
|
|
||||||
|
reloadLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastDiscordLocale) 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
|
||||||
|
const dotProp = (key: string, object: any) =>
|
||||||
|
key.split(".").reduce((obj, key) => obj?.[key], object);
|
||||||
|
|
||||||
|
type Translation = string | ({ [rule in Intl.LDMLPluralRule]?: string } & { other: string; });
|
||||||
|
|
||||||
|
// translation retrieval function
|
||||||
|
function _t(key: string, bundle: any): Translation {
|
||||||
|
const translation = dotProp(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. If dealing with plurals, `count` must be set.
|
||||||
|
* @returns A translated string.
|
||||||
|
*/
|
||||||
|
export function t(key: string, variables?: Record<string, any>): string {
|
||||||
|
const getter = (): string => {
|
||||||
|
const translation = _t(key, loadedLocale);
|
||||||
|
|
||||||
|
if (typeof translation !== "string") {
|
||||||
|
if (!variables?.count)
|
||||||
|
throw new Error(`translation key ${key} is an object (is it a plural?)`);
|
||||||
|
|
||||||
|
const pluralTag: Intl.LDMLPluralRule = variables.count === 0 ? "zero" :
|
||||||
|
new Intl.PluralRules(bestLocale).select(variables.count);
|
||||||
|
|
||||||
|
if (translation[pluralTag]) {
|
||||||
|
return format(translation[pluralTag]!, variables);
|
||||||
|
} else if (translation.other) {
|
||||||
|
return format(translation.other, variables);
|
||||||
|
} else {
|
||||||
|
throw new Error(`translation key ${key} is an object and doesn't seem to be plural`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("clientTheme.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: JSX.Element | JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A translation component. Follows the same rules as {@link t}, but lets you add components to strings.
|
||||||
|
* @param param0 Component props.
|
||||||
|
*/
|
||||||
|
export function Translate({ i18nKey, variables, children: trueChildren }: TranslateProps): JSX.Element {
|
||||||
|
const children = [trueChildren].flat();
|
||||||
|
|
||||||
|
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}</>;
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import gitHash from "~git-hash";
|
||||||
|
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { relaunch } from "./native";
|
import { relaunch } from "./native";
|
||||||
|
import { t } from "./translation";
|
||||||
import { IpcRes } from "./types";
|
import { IpcRes } from "./types";
|
||||||
|
|
||||||
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
|
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
|
||||||
|
@ -70,7 +71,7 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (isOutdated) {
|
if (isOutdated) {
|
||||||
const wantsUpdate = confirm(confirmMessage);
|
const wantsUpdate = confirm(confirmMessage);
|
||||||
if (wantsUpdate && isNewer) return alert("Your local copy has more recent commits. Please stash or reset them.");
|
if (wantsUpdate && isNewer) return alert(t("vencord.gitCopyNewer"));
|
||||||
if (wantsUpdate) {
|
if (wantsUpdate) {
|
||||||
await update();
|
await update();
|
||||||
relaunch();
|
relaunch();
|
||||||
|
@ -78,6 +79,6 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error(err);
|
UpdateLogger.error(err);
|
||||||
alert("That also failed :( Try updating or re-installing with the installer!");
|
alert(t("vencord.updaterRepeatFailed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
translations/de/clientTheme.json
Normal file
4
translations/de/clientTheme.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"description": "Plugin description but in German. Ja!",
|
||||||
|
"edit": "Edit ClientTheme"
|
||||||
|
}
|
7
translations/de/vencord.json
Normal file
7
translations/de/vencord.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"pluginContributed": {
|
||||||
|
"zero": "Diese Person hat keine Plugins erstellt. Sie haben wahrscheinlich auf andere Weise <0>zu Vencord beigetragen</0>!",
|
||||||
|
"one": "Diese Person hat zu einem Plugin <0>beigetragen</0>!",
|
||||||
|
"other": "Diese Person hat zu {count} Plugins <0>beigetragen</0>!"
|
||||||
|
}
|
||||||
|
}
|
19
translations/en/clientTheme.json
Normal file
19
translations/en/clientTheme.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"colorDescription": "Color your Discord client theme will be based around. Light mode isn't supported",
|
||||||
|
"description": "Recreation of the old client theme experiment. Add a color to your Discord client theme",
|
||||||
|
"disableNitroTheme": "Disable Nitro Theme",
|
||||||
|
"edit": "Edit ClientTheme",
|
||||||
|
"resetButton": "Reset Theme Color",
|
||||||
|
"resetColorDescription": "Reset Theme Color",
|
||||||
|
"settingsDescription": "Add a color to your Discord client theme",
|
||||||
|
"settingsTitle": "Theme Color",
|
||||||
|
"switchToOpposite": {
|
||||||
|
"dark": "Switch to dark mode",
|
||||||
|
"light": "Switch to light mode"
|
||||||
|
},
|
||||||
|
"warningTitle": "Warning, your theme won't look good:",
|
||||||
|
"warnings": {
|
||||||
|
"badContrast": "Selected color won't contrast well with text",
|
||||||
|
"nitro": "Nitro themes aren't supported"
|
||||||
|
}
|
||||||
|
}
|
3
translations/en/translation.json
Normal file
3
translations/en/translation.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"description": "Assists with translating Vencord"
|
||||||
|
}
|
131
translations/en/vencord.json
Normal file
131
translations/en/vencord.json
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
{
|
||||||
|
"areYouSure": "Are you sure?",
|
||||||
|
"backupAndRestore": {
|
||||||
|
"customQuickcss": "Custom QuickCSS",
|
||||||
|
"description": "You can import and export your Vencord settings as a JSON file. This allows you to easily transfer your settings to another device, or recover your settings after reinstalling Vencord or Discord.",
|
||||||
|
"exportContains": "Settings Export contains:",
|
||||||
|
"exportSettings": "Export Settings",
|
||||||
|
"importSettings": "Import Settings",
|
||||||
|
"importWarning": "Importing a settings file will overwrite your current settings.",
|
||||||
|
"pluginSettings": "Plugin Settings",
|
||||||
|
"themeLinks": "Theme Links"
|
||||||
|
},
|
||||||
|
"clipboardNotSupported": "Your browser does not support copying to clipboard",
|
||||||
|
"cloud": {
|
||||||
|
"integrations": {
|
||||||
|
"authorizationNote": "This will request authorization if you have not yet set up cloud integrations.",
|
||||||
|
"backendNote": "Which backend to use when using cloud integrations.",
|
||||||
|
"backendUrl": "Backend URL",
|
||||||
|
"description": "Vencord comes with a cloud integration that adds goodies like settings sync across devices. It <0>respects your privacy</0>, and the <1>source code</1> is AGPL 3.0 licensed so you can host it yourself.",
|
||||||
|
"enable": "Enable Cloud Integrations",
|
||||||
|
"enabled": "Cloud integrations enabled!",
|
||||||
|
"eraseAllData": "Erase All Data",
|
||||||
|
"eraseError": "Could not erase all data (API returned {status}), please contact support.",
|
||||||
|
"eraseIt": "Erase it!",
|
||||||
|
"eraseSuccess": "Successfully erased all data.",
|
||||||
|
"eraseWarning": "Once your data is erased, we cannot recover it. There's no going back!",
|
||||||
|
"reauthenticate": "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!",
|
||||||
|
"setupFailure": {
|
||||||
|
"generic": "Setup failed ({error}).",
|
||||||
|
"missingSecret": "Setup failed (no secret returned?).",
|
||||||
|
"oauth": "Setup failed (couldn't retrieve OAuth configuration)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"deleteCloudSettings": "Delete Cloud Settings",
|
||||||
|
"deleteError": {
|
||||||
|
"api": "Could not delete settings (API returned {status}).",
|
||||||
|
"generic": "Could not delete settings ({error})."
|
||||||
|
},
|
||||||
|
"deleted": "Settings deleted from cloud!",
|
||||||
|
"description": "Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with minimal effort.",
|
||||||
|
"localNewer": "Your local settings are newer than the cloud ones.",
|
||||||
|
"nothingOnline": "There are no settings in the cloud.",
|
||||||
|
"overwriteWarning": "This will overwrite your local settings with the ones on the cloud. Use wisely!",
|
||||||
|
"syncErrorDown": {
|
||||||
|
"api": "Could not synchronize settings from the cloud (API returned {status}).",
|
||||||
|
"generic": "Could not synchronize settings from the cloud ({error})."
|
||||||
|
},
|
||||||
|
"syncErrorUp": {
|
||||||
|
"api": "Could not synchronize settings to the cloud (API returned {status}).",
|
||||||
|
"generic": "Could not synchronize settings to the cloud ({error})."
|
||||||
|
},
|
||||||
|
"syncFromCloud": "Sync from Cloud",
|
||||||
|
"syncSuccess": "Synchronized settings to the cloud!",
|
||||||
|
"syncToCloud": "Sync to Cloud",
|
||||||
|
"upToDate": "Your settings are up to date.",
|
||||||
|
"updated": "Your settings have been updated! Click here to restart to fully apply changes!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cloudIntegrations": "Cloud Integrations",
|
||||||
|
"cloudSettings": "Cloud Settings",
|
||||||
|
"copiedToClipboard": "Copied to clipboard!",
|
||||||
|
"errorBoundaryDescription": "An error occurred while rendering this Component. More info can be found below and in your console.",
|
||||||
|
"failedToImport": "Failed to import settings: {error}",
|
||||||
|
"failureUpdate": "Uh Oh! Failed to render this Page. However, there is an update available that might fix it. Would you like to update and restart now?",
|
||||||
|
"gitCopyNewer": "Your local copy has more recent commits. Please stash or reset them.",
|
||||||
|
"importedSettings": "Settings successfully imported. Restart to apply changes!",
|
||||||
|
"nevermind": "Nevermind",
|
||||||
|
"new": "NEW",
|
||||||
|
"noSearchResults": "No plugins meet search criteria.",
|
||||||
|
"noSettings": "There are no settings for this plugin.",
|
||||||
|
"ohNo": "Oh no!",
|
||||||
|
"pluginContributed": {
|
||||||
|
"one": "This person has <0>contributed</0> to one plugin!",
|
||||||
|
"other": "This person has <0>contributed</0> to {count} plugins!",
|
||||||
|
"zero": "This person has not made any plugins. They likely <0>contributed</0> to Vencord in other ways!"
|
||||||
|
},
|
||||||
|
"pluginFilters": "Filters",
|
||||||
|
"pluginHeader": {
|
||||||
|
"cogWheel": "Plugins with a cog wheel have settings you can modify!",
|
||||||
|
"iconInformation": "Press the cog wheel or info icon to get more info on a plugin.",
|
||||||
|
"managementHeader": "Plugin Management",
|
||||||
|
"reloadDescription": "Restart now to apply new plugins and their settings",
|
||||||
|
"reloadHeader": "Restart required!",
|
||||||
|
"restart": "Restart"
|
||||||
|
},
|
||||||
|
"pluginRequiredBy": "This plugin is required by:",
|
||||||
|
"pluginRestart": "Restart to apply changes!",
|
||||||
|
"plugins": "Plugins",
|
||||||
|
"pluginsNeedRestart": "The following plugins require a restart:",
|
||||||
|
"reauthorise": "Reauthorise",
|
||||||
|
"requiredPlugin": "This plugin is required for Vencord to function.",
|
||||||
|
"requiredPlugins": "Required Plugins",
|
||||||
|
"restartLater": "Later!",
|
||||||
|
"restartNow": "Restart now",
|
||||||
|
"restartRequired": "Restart required",
|
||||||
|
"saveAndClose": "Save & Close",
|
||||||
|
"search": {
|
||||||
|
"all": "Show All",
|
||||||
|
"disabled": "Show Disabled",
|
||||||
|
"enabled": "Show Enabled",
|
||||||
|
"new": "Show New",
|
||||||
|
"placeholder": "Search for a plugin..."
|
||||||
|
},
|
||||||
|
"settings": "Settings",
|
||||||
|
"settingsErrors": "You must fix all errors before saving",
|
||||||
|
"settingsSaveError": "Error while saving: {saveError}",
|
||||||
|
"settingsSync": "Settings Sync",
|
||||||
|
"themes": {
|
||||||
|
"betterDiscord": "BetterDiscord Themes",
|
||||||
|
"betterDiscordNote": "If using the BD site, click on \"Download\" and place the downloaded .theme.css file into your themes folder.",
|
||||||
|
"editQuickCss": "Edit QuickCSS",
|
||||||
|
"findThemes": "Find Themes:",
|
||||||
|
"loadMissing": "Load missing Themes",
|
||||||
|
"local": "Local Themes",
|
||||||
|
"oneLinkPerLine": "One link per line.",
|
||||||
|
"online": "Online Themes",
|
||||||
|
"openFolder": "Open Themes Folder",
|
||||||
|
"pasteLinks": "Paste links to css files here",
|
||||||
|
"upload": "Upload Theme",
|
||||||
|
"useDirect": "Make sure to use direct links to files (raw or github.io)!"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"available": "A Vencord update is available!",
|
||||||
|
"clickToRestart": "Click here to restart",
|
||||||
|
"clickToView": "Click here to view the update",
|
||||||
|
"updated": "Vencord has been updated!"
|
||||||
|
},
|
||||||
|
"updaterRepeatFailed": "That also failed :( Try updating or re-installing with the installer!",
|
||||||
|
"warning": "Warning"
|
||||||
|
}
|
Loading…
Reference in a new issue