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",
|
||||
"GregorBiswanger.json2ts",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/langneg": "^0.7.0",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
|
|
|
@ -16,6 +16,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@fluent/langneg':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
'@sapphi-red/web-noise-suppressor':
|
||||
specifier: 0.3.3
|
||||
version: 0.3.3
|
||||
|
@ -385,6 +388,10 @@ packages:
|
|||
resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@fluent/langneg@0.7.0':
|
||||
resolution: {integrity: sha512-StAM0vgsD1QK+nFikaKs9Rxe3JGNipiXrpmemNGwM4gWERBXPe9gjzsBoKjgBgq1Vyiy+xy/C652QIWY+MPyYw==}
|
||||
engines: {node: '>=14.0.0', npm: '>=7.0.0'}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.10':
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
@ -2665,6 +2672,8 @@ snapshots:
|
|||
|
||||
'@eslint/js@8.46.0': {}
|
||||
|
||||
'@fluent/langneg@0.7.0': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.10':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 1.2.1
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
@ -303,8 +336,8 @@ export const commonOpts = {
|
|||
sourcemap: watch ? "inline" : "",
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, translationPlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "~translations", "/assets/*"],
|
||||
inject: ["./scripts/build/inject/react.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
jsxFragment: "VencordFragment",
|
||||
|
|
|
@ -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, 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";
|
||||
|
||||
|
@ -90,6 +90,38 @@ function parseDevs() {
|
|||
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) {
|
||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
||||
|
||||
|
@ -120,10 +152,16 @@ 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;
|
||||
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":
|
||||
data.hasPatches = true;
|
||||
break;
|
||||
|
|
|
@ -38,6 +38,7 @@ import { patches, PMLogger, startAllPlugins } from "./plugins";
|
|||
import { localStorage } from "./utils/localStorage";
|
||||
import { relaunch } from "./utils/native";
|
||||
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
||||
import { t } from "./utils/translation";
|
||||
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
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
|
||||
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.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.reauthenticate"),
|
||||
color: "var(--yellow-360)",
|
||||
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
|
||||
// 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.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.updated"),
|
||||
color: "var(--green-360)",
|
||||
onClick: relaunch
|
||||
});
|
||||
|
@ -100,8 +100,8 @@ async function init() {
|
|||
await update();
|
||||
if (Settings.autoUpdateNotification)
|
||||
setTimeout(() => showNotification({
|
||||
title: "Vencord has been updated!",
|
||||
body: "Click here to restart",
|
||||
title: t("vencord.update.updated"),
|
||||
body: t("vencord.update.clickToRestart"),
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: relaunch
|
||||
|
@ -110,8 +110,8 @@ async function init() {
|
|||
}
|
||||
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
title: t("vencord.update.available"),
|
||||
body: t("vencord.update.clickToView"),
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: openUpdaterModal!
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { t } from "@utils/translation";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
@ -85,11 +86,11 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
{...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 (
|
||||
<ErrorCard style={{ overflow: "hidden" }}>
|
||||
<h1>Oh no!</h1>
|
||||
<h1>{t("vencord.ohNo")}</h1>
|
||||
<p>{msg}</p>
|
||||
<code>
|
||||
{this.state.message}
|
||||
|
|
|
@ -12,8 +12,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Link } from "@components/Link";
|
||||
import { DevsById } 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 { t, Translate } from "@utils/translation";
|
||||
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
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));
|
||||
}, [user.id, user.username]);
|
||||
|
||||
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl("header")}>
|
||||
|
@ -88,15 +87,11 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{plugins.length ? (
|
||||
<Forms.FormText>
|
||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||
</Forms.FormText>
|
||||
) : (
|
||||
<Forms.FormText>
|
||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||
</Forms.FormText>
|
||||
)}
|
||||
<Forms.FormText>
|
||||
<Translate i18nKey="vencord.pluginContributed" variables={{ count: plugins.length }}>
|
||||
<Link href="https://vencord.dev/source" />
|
||||
</Translate>
|
||||
</Forms.FormText>
|
||||
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
|
@ -105,7 +100,7 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
onRestartNeeded={() => showToast(t("vencord.pluginRestart"))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,7 @@ import { proxyLazy } from "@utils/lazy";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { t } from "@utils/translation";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
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() {
|
||||
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 {
|
||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||
if (setting.hidden) return null;
|
||||
|
@ -274,7 +275,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
</div>
|
||||
)}
|
||||
<Forms.FormSection className={Margins.bottom16}>
|
||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
||||
<Forms.FormTitle tag="h3">{t("vencord.settings")}</Forms.FormTitle>
|
||||
{renderSettings()}
|
||||
</Forms.FormSection>
|
||||
</ModalContent>
|
||||
|
@ -289,7 +290,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||
<Tooltip text={t("vencord.settingsErrors")} shouldShow={!canSubmit()}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
|
@ -299,12 +300,12 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
onMouseLeave={onMouseLeave}
|
||||
disabled={!canSubmit()}
|
||||
>
|
||||
Save & Close
|
||||
{t("vencord.saveAndClose")}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</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>
|
||||
</ModalFooter>}
|
||||
</ModalRoot>
|
||||
|
|
|
@ -32,6 +32,7 @@ import { Logger } from "@utils/Logger";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { t } from "@utils/translation";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
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 })}>
|
||||
{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")}>
|
||||
Restart now to apply new plugins and their settings
|
||||
{t("vencord.pluginHeader.reloadDescription")}
|
||||
</Forms.FormText>
|
||||
<Button onClick={() => location.reload()}>
|
||||
Restart
|
||||
{t("vencord.pluginHeader.restart")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
||||
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
||||
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
||||
<Forms.FormTitle tag="h5">{t("vencord.pluginHeader.managementHeader")}</Forms.FormTitle>
|
||||
<Forms.FormText>{t("vencord.pluginHeader.iconInformation")}</Forms.FormText>
|
||||
<Forms.FormText>{t("vencord.pluginHeader.cogWheel")}</Forms.FormText>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
@ -209,10 +210,10 @@ export default function PluginSettings() {
|
|||
|
||||
React.useEffect(() => {
|
||||
return () => void (changes.hasChanges && Alerts.show({
|
||||
title: "Restart required",
|
||||
title: t("vencord.restartRequired"),
|
||||
body: (
|
||||
<>
|
||||
<p>The following plugins require a restart:</p>
|
||||
<p>$t("vencord.pluginsNeedRestart")</p>
|
||||
<div>{changes.map((s, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
|
@ -221,8 +222,8 @@ export default function PluginSettings() {
|
|||
))}</div>
|
||||
</>
|
||||
),
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
confirmText: t("vencord.restartNow"),
|
||||
cancelText: t("vencord.restartLater"),
|
||||
onConfirm: () => location.reload()
|
||||
}));
|
||||
}, []);
|
||||
|
@ -296,7 +297,7 @@ export default function PluginSettings() {
|
|||
|
||||
if (isRequired) {
|
||||
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));
|
||||
|
||||
requiredPlugins.push(
|
||||
|
@ -331,18 +332,18 @@ export default function PluginSettings() {
|
|||
<ReloadRequiredCard required={changes.hasChanges} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Filters
|
||||
{t("vencord.pluginFilters")}
|
||||
</Forms.FormTitle>
|
||||
|
||||
<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}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||
{ label: "Show New", value: SearchStatus.NEW }
|
||||
{ label: t("vencord.search.all"), value: SearchStatus.ALL, default: true },
|
||||
{ label: t("vencord.search.enabled"), value: SearchStatus.ENABLED },
|
||||
{ label: t("vencord.search.disabled"), value: SearchStatus.DISABLED },
|
||||
{ label: t("vencord.search.new"), value: SearchStatus.NEW }
|
||||
]}
|
||||
serialize={String}
|
||||
select={onStatusChange}
|
||||
|
@ -353,7 +354,7 @@ export default function PluginSettings() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20}>{t("vencord.plugins")}</Forms.FormTitle>
|
||||
|
||||
{plugins.length || requiredPlugins.length
|
||||
? (
|
||||
|
@ -371,7 +372,7 @@ export default function PluginSettings() {
|
|||
<Forms.FormDivider className={Margins.top20} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Required Plugins
|
||||
{t("vencord.requiredPlugins")}
|
||||
</Forms.FormTitle>
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins.length
|
||||
|
@ -386,7 +387,7 @@ export default function PluginSettings() {
|
|||
function makeDependencyList(deps: string[]) {
|
||||
return (
|
||||
<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>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import "./addonCard.css";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { t } from "@utils/translation";
|
||||
import { Text, useRef } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
|
@ -67,7 +68,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
|||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</div>{isNew && <Badge text={t("vencord.new")} color="#ED4245" />}
|
||||
</Text>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Flex } from "@components/Flex";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||
import { t } from "@utils/translation";
|
||||
import { Button, Card, Text } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
@ -29,21 +30,19 @@ function BackupRestoreTab() {
|
|||
<SettingsTab title="Backup & Restore">
|
||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||
<Flex flexDirection="column">
|
||||
<strong>Warning</strong>
|
||||
<span>Importing a settings file will overwrite your current settings.</span>
|
||||
<strong>{t("vencord.warning")}</strong>
|
||||
<span>{t("vencord.backupAndRestore.importWarning")}</span>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||
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.
|
||||
{t("vencord.backupAndRestore.description")}
|
||||
</Text>
|
||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||
Settings Export contains:
|
||||
{t("vencord.backupAndRestore.exportContains")}
|
||||
<ul>
|
||||
<li>— Custom QuickCSS</li>
|
||||
<li>— Theme Links</li>
|
||||
<li>— Plugin Settings</li>
|
||||
<li>— {t("vencord.backupAndRestore.customQuickcss")}</li>
|
||||
<li>— {t("vencord.backupAndRestore.themeLinks")}</li>
|
||||
<li>— {t("vencord.backupAndRestore.pluginSettings")}</li>
|
||||
</ul>
|
||||
</Text>
|
||||
<Flex>
|
||||
|
@ -51,13 +50,13 @@ function BackupRestoreTab() {
|
|||
onClick={() => uploadSettingsBackup()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Import Settings
|
||||
{t("vencord.backupAndRestore.importSettings")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={downloadSettingsBackup}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Export Settings
|
||||
{t("vencord.backupAndRestore.exportSettings")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingsTab>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { Link } from "@components/Link";
|
|||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||
import { t, Translate } from "@utils/translation";
|
||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
@ -46,8 +47,8 @@ async function eraseAllData() {
|
|||
if (!res.ok) {
|
||||
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
|
||||
showNotification({
|
||||
title: "Cloud Integrations",
|
||||
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.eraseError", { status: res.status }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
return;
|
||||
|
@ -57,8 +58,8 @@ async function eraseAllData() {
|
|||
await deauthorizeCloud();
|
||||
|
||||
showNotification({
|
||||
title: "Cloud Integrations",
|
||||
body: "Successfully erased all data.",
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.eraseSuccess"),
|
||||
color: "var(--green-360)"
|
||||
});
|
||||
}
|
||||
|
@ -70,8 +71,7 @@ function SettingsSyncSection() {
|
|||
return (
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
|
||||
minimal effort.
|
||||
{t("vencord.cloud.settings.description")}
|
||||
</Forms.FormText>
|
||||
<Switch
|
||||
key="cloud-sync"
|
||||
|
@ -79,7 +79,7 @@ function SettingsSyncSection() {
|
|||
value={cloud.settingsSync}
|
||||
onChange={v => { cloud.settingsSync = v; }}
|
||||
>
|
||||
Settings Sync
|
||||
{t("vencord.settingsSync")}
|
||||
</Switch>
|
||||
<div className="vc-cloud-settings-sync-grid">
|
||||
<Button
|
||||
|
@ -87,9 +87,9 @@ function SettingsSyncSection() {
|
|||
disabled={!sectionEnabled}
|
||||
onClick={() => putCloudSettings(true)}
|
||||
>
|
||||
Sync to Cloud
|
||||
{t("vencord.cloud.settings.syncToCloud")}
|
||||
</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 }) => (
|
||||
<Button
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
@ -99,7 +99,7 @@ function SettingsSyncSection() {
|
|||
disabled={!sectionEnabled}
|
||||
onClick={() => getCloudSettings(true, true)}
|
||||
>
|
||||
Sync from Cloud
|
||||
{t("vencord.cloud.settings.syncFromCloud")}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
@ -109,7 +109,7 @@ function SettingsSyncSection() {
|
|||
disabled={!sectionEnabled}
|
||||
onClick={() => deleteCloudSettings()}
|
||||
>
|
||||
Delete Cloud Settings
|
||||
{t("vencord.cloud.settings.deleteCloudSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
|
@ -121,12 +121,12 @@ function CloudTab() {
|
|||
|
||||
return (
|
||||
<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}>
|
||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
||||
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
||||
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
||||
can host it yourself.
|
||||
<Translate i18nKey="vencord.cloud.integrations.description">
|
||||
<Link href="https://vencord.dev/cloud/privacy" />
|
||||
<Link href="https://github.com/Vencord/Backend" />
|
||||
</Translate>
|
||||
</Forms.FormText>
|
||||
<Switch
|
||||
key="backend"
|
||||
|
@ -137,13 +137,13 @@ function CloudTab() {
|
|||
else
|
||||
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>
|
||||
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
|
||||
<Forms.FormTitle tag="h5">{t("vencord.cloud.integrations.backendUrl")}</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Which backend to use when using cloud integrations.
|
||||
{t("vencord.cloud.integrations.backendNote")}
|
||||
</Forms.FormText>
|
||||
<CheckedTextInput
|
||||
key="backendUrl"
|
||||
|
@ -166,25 +166,24 @@ function CloudTab() {
|
|||
await authorizeCloud();
|
||||
}}
|
||||
>
|
||||
Reauthorise
|
||||
{t("vencord.reauthorise")}
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.MEDIUM}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||
title: t("vencord.areYouSure"),
|
||||
body: t("vencord.cloud.integrations.eraseWarning"),
|
||||
onConfirm: eraseAllData,
|
||||
confirmText: "Erase it!",
|
||||
confirmText: t("vencord.cloud.integrations.eraseIt"),
|
||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
cancelText: t("vencord.nevermind")
|
||||
})}
|
||||
>
|
||||
Erase All Data
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Forms.FormDivider className={Margins.top16} />
|
||||
</Forms.FormSection >
|
||||
<SettingsSyncSection />
|
||||
|
|
|
@ -28,6 +28,7 @@ import { Margins } from "@utils/margins";
|
|||
import { classes } from "@utils/misc";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { t } from "@utils/translation";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
|
@ -202,60 +203,68 @@ function ThemesTab() {
|
|||
return (
|
||||
<>
|
||||
<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" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
BetterDiscord Themes
|
||||
{t("vencord.themes.betterDiscord")}
|
||||
</Link>
|
||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||
</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>
|
||||
|
||||
<Forms.FormSection title="Local Themes">
|
||||
<QuickActionCard>
|
||||
<Forms.FormSection title={t("vencord.themes.local")}>
|
||||
<Card className="vc-settings-quick-actions-card">
|
||||
<>
|
||||
{IS_WEB ?
|
||||
(
|
||||
<QuickAction
|
||||
text={
|
||||
<span style={{ position: "relative" }}>
|
||||
Upload Theme
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
Icon={PlusIcon}
|
||||
/>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={themeDirPending}
|
||||
>
|
||||
{t("vencord.themes.upload")}
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<QuickAction
|
||||
text="Open Themes Folder"
|
||||
text={t("vencord.themes.openFolder")}
|
||||
action={() => showItemInFolder(themeDir!)}
|
||||
disabled={themeDirPending}
|
||||
Icon={FolderIcon}
|
||||
/>
|
||||
>
|
||||
{t("vencord.themes.openFolder")}
|
||||
</Button>
|
||||
)}
|
||||
<QuickAction
|
||||
text="Load missing Themes"
|
||||
action={refreshLocalThemes}
|
||||
Icon={RestartIcon}
|
||||
/>
|
||||
<QuickAction
|
||||
text="Edit QuickCSS"
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
Icon={PaintbrushIcon}
|
||||
/>
|
||||
<Button
|
||||
onClick={refreshLocalThemes}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
{t("vencord.themes.loadMissing")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
{t("vencord.themes.editQuickCss")}
|
||||
</Button>
|
||||
|
||||
{Vencord.Settings.plugins.ClientTheme.enabled && (
|
||||
<QuickAction
|
||||
text="Edit ClientTheme"
|
||||
action={() => openPluginModal(Vencord.Plugins.plugins.ClientTheme)}
|
||||
Icon={PencilIcon}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openModal(modalProps => (
|
||||
<PluginModal
|
||||
{...modalProps}
|
||||
plugin={Vencord.Plugins.plugins.ClientTheme}
|
||||
onRestartNeeded={() => { }}
|
||||
/>
|
||||
))}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
{t("clientTheme.edit")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</QuickActionCard>
|
||||
|
@ -295,12 +304,12 @@ function ThemesTab() {
|
|||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card vc-text-selectable">
|
||||
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
||||
<Forms.FormTitle tag="h5">{t("vencord.themes.pasteLinks")}</Forms.FormTitle>
|
||||
<Forms.FormText>{t("vencord.themes.oneLinkPerLine")}</Forms.FormText>
|
||||
<Forms.FormText>{t("vencord.themes.useDirect")}</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
<Forms.FormSection title="Online Themes" tag="h5">
|
||||
<Forms.FormSection title={t("vencord.themes.online")} tag="h5">
|
||||
<TextArea
|
||||
value={themeText}
|
||||
onChange={setThemeText}
|
||||
|
@ -329,13 +338,13 @@ function ThemesTab() {
|
|||
className="vc-settings-tab-bar-item"
|
||||
id={ThemeTab.LOCAL}
|
||||
>
|
||||
Local Themes
|
||||
{t("vencord.themes.local")}
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className="vc-settings-tab-bar-item"
|
||||
id={ThemeTab.ONLINE}
|
||||
>
|
||||
Online Themes
|
||||
{t("vencord.themes.online")}
|
||||
</TabBar.Item>
|
||||
</TabBar>
|
||||
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { t } from "@utils/translation";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
|
||||
export function handleComponentFailed() {
|
||||
maybePromptToUpdate(
|
||||
"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?"
|
||||
t("vencord.failureUpdate")
|
||||
);
|
||||
}
|
||||
|
|
5
src/modules.d.ts
vendored
5
src/modules.d.ts
vendored
|
@ -43,6 +43,11 @@ declare module "~git-remote" {
|
|||
export default remote;
|
||||
}
|
||||
|
||||
declare module "~translations" {
|
||||
const translations: Record<string, Record<string, any>>;
|
||||
export default translations;
|
||||
}
|
||||
|
||||
declare module "file://*" {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
|
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 { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { t } from "@utils/translation";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
||||
|
@ -64,8 +65,8 @@ function ThemeSettings() {
|
|||
<div className="client-theme-settings">
|
||||
<div className="client-theme-container">
|
||||
<div className="client-theme-settings-labels">
|
||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
||||
<Forms.FormTitle tag="h3">{t("clientTheme.settingsTitle")}</Forms.FormTitle>
|
||||
<Forms.FormText>{t("clientTheme.settingsDescription")}</Forms.FormText>
|
||||
</div>
|
||||
<ColorPicker
|
||||
color={parseInt(settings.store.color, 16)}
|
||||
|
@ -78,12 +79,12 @@ function ThemeSettings() {
|
|||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||
<div className="client-theme-warning">
|
||||
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
|
||||
<Forms.FormText>{t("clientTheme.warningTitle")}</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText>{t("clientTheme.warnings.badContrast")}</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>{t("clientTheme.warnings.nitro")}</Forms.FormText>}
|
||||
</div>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</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}>{t("clientTheme.disableNitroTheme")}</Button>}
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
@ -92,18 +93,18 @@ function ThemeSettings() {
|
|||
|
||||
const settings = definePluginSettings({
|
||||
color: {
|
||||
description: "Color your Discord client theme will be based around. Light mode isn't supported",
|
||||
description: t("clientTheme.colorDescription"),
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => <ThemeSettings />
|
||||
},
|
||||
resetColor: {
|
||||
description: "Reset Theme Color",
|
||||
description: t("clientTheme.resetColorDescription"),
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: () => (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
{t("clientTheme.resetButton")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -112,7 +113,7 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "ClientTheme",
|
||||
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,
|
||||
|
||||
startAt: StartAt.DOMContentLoaded,
|
||||
|
|
|
@ -23,6 +23,7 @@ import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
|||
|
||||
import { Logger } from "./Logger";
|
||||
import { openModal } from "./modal";
|
||||
import { t } from "./translation";
|
||||
|
||||
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
||||
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||
|
@ -83,8 +84,8 @@ export async function authorizeCloud() {
|
|||
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||
} catch {
|
||||
showNotification({
|
||||
title: "Cloud Integration",
|
||||
body: "Setup failed (couldn't retrieve OAuth configuration)."
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.setupFailure.oauth")
|
||||
});
|
||||
Settings.cloud.authenticated = false;
|
||||
return;
|
||||
|
@ -113,22 +114,22 @@ export async function authorizeCloud() {
|
|||
cloudLogger.info("Authorized with secret");
|
||||
await setAuthorization(secret);
|
||||
showNotification({
|
||||
title: "Cloud Integration",
|
||||
body: "Cloud integrations enabled!"
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.enabled")
|
||||
});
|
||||
Settings.cloud.authenticated = true;
|
||||
} else {
|
||||
showNotification({
|
||||
title: "Cloud Integration",
|
||||
body: "Setup failed (no secret returned?)."
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.setupFailure.missingSecret")
|
||||
});
|
||||
Settings.cloud.authenticated = false;
|
||||
}
|
||||
} catch (e: any) {
|
||||
cloudLogger.error("Failed to authorize", e);
|
||||
showNotification({
|
||||
title: "Cloud Integration",
|
||||
body: `Setup failed (${e.toString()}).`
|
||||
title: t("vencord.cloudIntegrations"),
|
||||
body: t("vencord.cloud.integrations.setupFailure.generic", { error: e.toString() })
|
||||
});
|
||||
Settings.cloud.authenticated = false;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { Clipboard, Toasts } from "@webpack/common";
|
||||
|
||||
import { DevsById } from "./constants";
|
||||
import { t } from "./translation";
|
||||
|
||||
/**
|
||||
* Calls .join(" ") on the arguments
|
||||
|
@ -35,11 +36,13 @@ export function sleep(ms: number): Promise<void> {
|
|||
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) {
|
||||
Clipboard.copy(text);
|
||||
} else {
|
||||
toastMessage = "Your browser does not support copying to clipboard";
|
||||
toastMessage = t("vencord.clipboardNotSupported");
|
||||
}
|
||||
Toasts.show({
|
||||
message: toastMessage,
|
||||
|
|
|
@ -24,6 +24,7 @@ import { deflateSync, inflateSync } from "fflate";
|
|||
import { getCloudAuth, getCloudUrl } from "./cloud";
|
||||
import { Logger } from "./Logger";
|
||||
import { relaunch } from "./native";
|
||||
import { t } from "./translation";
|
||||
import { chooseFile, saveFile } from "./web";
|
||||
|
||||
export async function importSettings(data: string) {
|
||||
|
@ -68,10 +69,10 @@ const toast = (type: number, message: string) =>
|
|||
});
|
||||
|
||||
const toastSuccess = () =>
|
||||
toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
|
||||
toast(Toasts.Type.SUCCESS, t("vencord.importedSettings"));
|
||||
|
||||
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> {
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
|
@ -128,8 +129,8 @@ export async function putCloudSettings(manual?: boolean) {
|
|||
if (!res.ok) {
|
||||
cloudSettingsLogger.error(`Failed to sync up, API returned ${res.status}`);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.syncErrorUp.api", { status: res.status.toString() }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
return;
|
||||
|
@ -143,16 +144,16 @@ export async function putCloudSettings(manual?: boolean) {
|
|||
|
||||
if (manual) {
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Synchronized settings to the cloud!",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.syncSuccess"),
|
||||
noPersist: true,
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
cloudSettingsLogger.error("Failed to sync up", e);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not synchronize settings to the cloud (${e.toString()}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.syncErrorUp.generic", { error: e.toString() }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
}
|
||||
|
@ -173,8 +174,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
cloudSettingsLogger.info("No settings on the cloud");
|
||||
if (shouldNotify)
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "There are no settings in the cloud.",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.nothingOnline"),
|
||||
noPersist: true
|
||||
});
|
||||
return false;
|
||||
|
@ -184,8 +185,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
cloudSettingsLogger.info("Settings up to date");
|
||||
if (shouldNotify)
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Your settings are up to date.",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.upToDate"),
|
||||
noPersist: true
|
||||
});
|
||||
return false;
|
||||
|
@ -194,8 +195,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
if (!res.ok) {
|
||||
cloudSettingsLogger.error(`Failed to sync down, API returned ${res.status}`);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.syncErrorDown.api", { status: res.status.toString() }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
return false;
|
||||
|
@ -208,8 +209,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
if (!force && written < localWritten) {
|
||||
if (shouldNotify)
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Your local settings are newer than the cloud ones.",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.localNewer"),
|
||||
noPersist: true,
|
||||
});
|
||||
return;
|
||||
|
@ -227,8 +228,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
cloudSettingsLogger.info("Settings loaded from cloud successfully");
|
||||
if (shouldNotify)
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.updated"),
|
||||
color: "var(--green-360)",
|
||||
onClick: IS_WEB ? () => location.reload() : relaunch,
|
||||
noPersist: true
|
||||
|
@ -238,8 +239,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
} catch (e: any) {
|
||||
cloudSettingsLogger.error("Failed to sync down", e);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not synchronize settings from the cloud (${e.toString()}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.syncErrorDown.generic", { error: e.toString() }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
|
||||
|
@ -257,8 +258,8 @@ export async function deleteCloudSettings() {
|
|||
if (!res.ok) {
|
||||
cloudSettingsLogger.error(`Failed to delete, API returned ${res.status}`);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not delete settings (API returned ${res.status}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.deleteError.api", { error: res.status.toString() }),
|
||||
color: "var(--red-360)"
|
||||
});
|
||||
return;
|
||||
|
@ -266,15 +267,15 @@ export async function deleteCloudSettings() {
|
|||
|
||||
cloudSettingsLogger.info("Settings deleted from cloud successfully");
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: "Settings deleted from cloud!",
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.deleted"),
|
||||
color: "var(--green-360)"
|
||||
});
|
||||
} catch (e: any) {
|
||||
cloudSettingsLogger.error("Failed to delete", e);
|
||||
showNotification({
|
||||
title: "Cloud Settings",
|
||||
body: `Could not delete settings (${e.toString()}).`,
|
||||
title: t("vencord.cloudSettings"),
|
||||
body: t("vencord.cloud.settings.deleteError.generic", { error: e.toString() }),
|
||||
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 { relaunch } from "./native";
|
||||
import { t } from "./translation";
|
||||
import { IpcRes } from "./types";
|
||||
|
||||
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
|
||||
|
@ -70,7 +71,7 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
|
|||
const isOutdated = await checkForUpdates();
|
||||
if (isOutdated) {
|
||||
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) {
|
||||
await update();
|
||||
relaunch();
|
||||
|
@ -78,6 +79,6 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
|
|||
}
|
||||
} catch (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