feat: localization (#196)

* Started adding locale translations :)

* Added some more translations

* Working on translating even more pages

* More translations

* Added test default locale retrieval

* replace `intl.formatMessage` with custom `t` hook

* add more translations

* improve title syntax

* add more translations

* translate admin config page

* translated error messages

* add language selecter

* minor fixes

* improve language handling

* add upcoming languages

* add `crowdin.yml`

* run formatter

---------

Co-authored-by: Steve Tautonico <stautonico@gmail.com>
This commit is contained in:
Elias Schneider
2023-07-20 15:32:07 +02:00
committed by GitHub
parent 7c5ec8d0ea
commit b9f6e3bd08
68 changed files with 4712 additions and 461 deletions

View File

@@ -1,5 +1,10 @@
{
"extends": ["eslint-config-next", "eslint:recommended", "prettier"],
"extends": [
"next/babel",
"eslint-config-next",
"eslint:recommended",
"prettier"
],
"plugins": ["react"],
"rules": {
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],

View File

@@ -1,5 +1,4 @@
/** @type {import('next').NextConfig} */
const { version } = require('./package.json');
const withPWA = require("next-pwa")({

View File

@@ -32,6 +32,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-intl": "^6.3.1",
"sharp": "^0.31.3",
"yup": "^1.0.2"
},
@@ -1860,6 +1861,92 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
"dependencies": {
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.0.0.tgz",
"integrity": "sha512-U88W/qhEs5ZuX+Inw/DZHjA6w2YCTWTNzTkprzNznyWoGl8h+XtlOCW3nM78+VX7lSbvpMdnaHmWLnDnjJjuwg==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz",
"integrity": "sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-skeleton-parser": "1.3.18",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/intl": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.6.8.tgz",
"integrity": "sha512-lWfFUBGvYfeQoIaW5/6TW7WdyiqfWg5wPFVnsb1eKs4JPmAz2nYBUPuKbyvBiRV87QCSNZ+tZTViFBU+bIHeqQ==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "2.0.0",
"@formatjs/icu-messageformat-parser": "2.3.0",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-listformat": "7.1.9",
"intl-messageformat": "10.3.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"typescript": "^4.7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-displaynames": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.2.6.tgz",
"integrity": "sha512-scf5AQTk9EjpvPhboo5sizVOvidTdMOnajv9z+0cejvl7JNl9bl/aMrNBgC72UH+bP3l45usPUKAGskV6sNIrA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/intl-listformat": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.1.9.tgz",
"integrity": "sha512-5YikxwRqRXTVWVujhswDOTCq6gs+m9IcNbNZLa6FLtyBStAjEsuE2vAU+lPsbz9ZTST57D5fodjIh2JXT6sMWQ==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -2625,6 +2712,15 @@
"@types/node": "*"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
@@ -2668,14 +2764,12 @@
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"devOptional": true
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"node_modules/@types/react": {
"version": "18.0.28",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -2702,8 +2796,7 @@
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"devOptional": true
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"node_modules/@types/trusted-types": {
"version": "2.0.2",
@@ -5349,6 +5442,17 @@
"node": ">= 0.4"
}
},
"node_modules/intl-messageformat": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.3.2.tgz",
"integrity": "sha512-kGY1KrpxPGbWX/yz6rpWQahBh5bJC6pIbq/cTzVYlmAYjRVzP+l2MulagbZf/5mABbcLT/0RJbZC46Iw6Mhmtw==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "2.0.0",
"@formatjs/icu-messageformat-parser": "2.3.0",
"tslib": "^2.4.0"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -6863,6 +6967,32 @@
"react": "*"
}
},
"node_modules/react-intl": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.1.tgz",
"integrity": "sha512-wI7YSxS2xQRYrHPMFSHXSWNuOLrgt9L0Dg8G35xJNfDRpcyebE9H1k2/DDPxgbV4ImeG/qKJMN5SrZV7fuB/Ag==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-messageformat-parser": "2.3.0",
"@formatjs/intl": "2.6.8",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-listformat": "7.1.9",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "16 || 17 || 18",
"hoist-non-react-statics": "^3.3.2",
"intl-messageformat": "10.3.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"react": "^16.6.0 || 17 || 18",
"typescript": "^4.7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -7988,7 +8118,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9950,6 +10080,84 @@
"@floating-ui/dom": "^1.2.1"
}
},
"@formatjs/ecma402-abstract": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
"requires": {
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"@formatjs/fast-memoize": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.0.0.tgz",
"integrity": "sha512-U88W/qhEs5ZuX+Inw/DZHjA6w2YCTWTNzTkprzNznyWoGl8h+XtlOCW3nM78+VX7lSbvpMdnaHmWLnDnjJjuwg==",
"requires": {
"tslib": "^2.4.0"
}
},
"@formatjs/icu-messageformat-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz",
"integrity": "sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-skeleton-parser": "1.3.18",
"tslib": "^2.4.0"
}
},
"@formatjs/icu-skeleton-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"tslib": "^2.4.0"
}
},
"@formatjs/intl": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.6.8.tgz",
"integrity": "sha512-lWfFUBGvYfeQoIaW5/6TW7WdyiqfWg5wPFVnsb1eKs4JPmAz2nYBUPuKbyvBiRV87QCSNZ+tZTViFBU+bIHeqQ==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "2.0.0",
"@formatjs/icu-messageformat-parser": "2.3.0",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-listformat": "7.1.9",
"intl-messageformat": "10.3.2",
"tslib": "^2.4.0"
}
},
"@formatjs/intl-displaynames": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.2.6.tgz",
"integrity": "sha512-scf5AQTk9EjpvPhboo5sizVOvidTdMOnajv9z+0cejvl7JNl9bl/aMrNBgC72UH+bP3l45usPUKAGskV6sNIrA==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"@formatjs/intl-listformat": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.1.9.tgz",
"integrity": "sha512-5YikxwRqRXTVWVujhswDOTCq6gs+m9IcNbNZLa6FLtyBStAjEsuE2vAU+lPsbz9ZTST57D5fodjIh2JXT6sMWQ==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/intl-localematcher": "0.2.32",
"tslib": "^2.4.0"
}
},
"@formatjs/intl-localematcher": {
"version": "0.2.32",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
"requires": {
"tslib": "^2.4.0"
}
},
"@humanwhocodes/config-array": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -10460,6 +10668,15 @@
"@types/node": "*"
}
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/http-proxy": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
@@ -10503,14 +10720,12 @@
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"devOptional": true
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"@types/react": {
"version": "18.0.28",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
"devOptional": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -10537,8 +10752,7 @@
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"devOptional": true
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"@types/trusted-types": {
"version": "2.0.2",
@@ -12506,6 +12720,17 @@
"side-channel": "^1.0.4"
}
},
"intl-messageformat": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.3.2.tgz",
"integrity": "sha512-kGY1KrpxPGbWX/yz6rpWQahBh5bJC6pIbq/cTzVYlmAYjRVzP+l2MulagbZf/5mABbcLT/0RJbZC46Iw6Mhmtw==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/fast-memoize": "2.0.0",
"@formatjs/icu-messageformat-parser": "2.3.0",
"tslib": "^2.4.0"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -13565,6 +13790,23 @@
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
"requires": {}
},
"react-intl": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.1.tgz",
"integrity": "sha512-wI7YSxS2xQRYrHPMFSHXSWNuOLrgt9L0Dg8G35xJNfDRpcyebE9H1k2/DDPxgbV4ImeG/qKJMN5SrZV7fuB/Ag==",
"requires": {
"@formatjs/ecma402-abstract": "1.14.3",
"@formatjs/icu-messageformat-parser": "2.3.0",
"@formatjs/intl": "2.6.8",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-listformat": "7.1.9",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "16 || 17 || 18",
"hoist-non-react-statics": "^3.3.2",
"intl-messageformat": "10.3.2",
"tslib": "^2.4.0"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -14362,7 +14604,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true
"devOptional": true
},
"unbox-primitive": {
"version": "1.0.2",

View File

@@ -33,6 +33,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-intl": "^6.3.1",
"sharp": "^0.31.3",
"yup": "^1.0.2"
},

View File

@@ -0,0 +1,33 @@
import { Select } from "@mantine/core";
import { getCookie, setCookie } from "cookies-next";
import { useState } from "react";
import { LOCALES } from "../../i18n/locales";
const LanguagePicker = () => {
const [selectedLanguage, setSelectedLanguage] = useState(
getCookie("language")?.toString()
);
const languages = Object.values(LOCALES).map((locale) => ({
value: locale.code,
label: locale.name,
}));
return (
<Select
value={selectedLanguage}
onChange={(value) => {
setSelectedLanguage(value ?? "en");
setCookie("language", value, {
sameSite: "lax",
expires: new Date(
new Date().setFullYear(new Date().getFullYear() + 1)
),
});
location.reload();
}}
data={languages}
/>
);
};
export default LanguagePicker;

View File

@@ -9,12 +9,12 @@ import {
import { useColorScheme } from "@mantine/hooks";
import { useState } from "react";
import { TbDeviceLaptop, TbMoon, TbSun } from "react-icons/tb";
import usePreferences from "../../hooks/usePreferences";
import { FormattedMessage } from "react-intl";
import userPreferences from "../../utils/userPreferences.util";
const ThemeSwitcher = () => {
const preferences = usePreferences();
const [colorScheme, setColorScheme] = useState(
preferences.get("colorScheme")
userPreferences.get("colorScheme")
);
const { toggleColorScheme } = useMantineColorScheme();
const systemColorScheme = useColorScheme();
@@ -23,7 +23,7 @@ const ThemeSwitcher = () => {
<SegmentedControl
value={colorScheme}
onChange={(value) => {
preferences.set("colorScheme", value);
userPreferences.set("colorScheme", value);
setColorScheme(value);
toggleColorScheme(
value == "system" ? systemColorScheme : (value as ColorScheme)
@@ -34,7 +34,9 @@ const ThemeSwitcher = () => {
label: (
<Center>
<TbMoon size={16} />
<Box ml={10}>Dark</Box>
<Box ml={10}>
<FormattedMessage id="account.theme.dark" />
</Box>
</Center>
),
value: "dark",
@@ -43,7 +45,9 @@ const ThemeSwitcher = () => {
label: (
<Center>
<TbSun size={16} />
<Box ml={10}>Light</Box>
<Box ml={10}>
<FormattedMessage id="account.theme.light" />
</Box>
</Center>
),
value: "light",
@@ -52,7 +56,9 @@ const ThemeSwitcher = () => {
label: (
<Center>
<TbDeviceLaptop size={16} />
<Box ml={10}>System</Box>
<Box ml={10}>
<FormattedMessage id="account.theme.system" />
</Box>
</Center>
),
value: "system",

View File

@@ -12,7 +12,11 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate, {
translateOutsideContext,
} from "../../hooks/useTranslate.hook";
import authService from "../../services/auth.service";
import toast from "../../utils/toast.util";
@@ -25,8 +29,9 @@ const showEnableTotpModal = (
password: string;
}
) => {
const t = translateOutsideContext();
return modals.openModal({
title: "Enable TOTP",
title: t("account.modal.totp.title"),
children: (
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
),
@@ -45,6 +50,7 @@ const CreateEnableTotpModal = ({
refreshUser: () => {};
}) => {
const modals = useModals();
const t = useTranslate();
const validationSchema = yup.object().shape({
code: yup
@@ -66,14 +72,19 @@ const CreateEnableTotpModal = ({
<div>
<Center>
<Stack>
<Text>Step 1: Add your authenticator</Text>
<Text>
<FormattedMessage id="account.modal.totp.step1" />
</Text>
<Image src={options.qrCode} alt="QR Code" />
<Center>
<span>OR</span>
<span>
{" "}
<FormattedMessage id="common.text.or" />
</span>
</Center>
<Tooltip label="Click to copy">
<Tooltip label={t("account.modal.totp.clickToCopy")}>
<Button
onClick={() => {
navigator.clipboard.writeText(options.secret);
@@ -84,17 +95,19 @@ const CreateEnableTotpModal = ({
</Button>
</Tooltip>
<Center>
<Text fz="xs">Enter manually</Text>
<Text fz="xs"></Text>
</Center>
<Text>Step 2: Validate your code</Text>
<Text>
<FormattedMessage id="account.modal.totp.step2" />
</Text>
<form
onSubmit={form.onSubmit((values) => {
authService
.verifyTOTP(values.code, options.password)
.then(() => {
toast.success("Successfully enabled TOTP");
toast.success(t("account.notify.totp.enable"));
modals.closeAll();
refreshUser();
})
@@ -105,14 +118,14 @@ const CreateEnableTotpModal = ({
<Col xs={9}>
<TextInput
variant="filled"
label="Code"
label={t("account.modal.totp.code")}
placeholder="******"
{...form.getInputProps("code")}
/>
</Col>
<Col xs={3}>
<Button variant="outline" type="submit">
Verify
<FormattedMessage id="account.modal.totp.verify" />
</Button>
</Col>
</Grid>

View File

@@ -1,14 +1,16 @@
import { Stack, TextInput } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
const showReverseShareLinkModal = (
modals: ModalsContextProps,
reverseShareToken: string,
appUrl: string
) => {
const t = translateOutsideContext();
const link = `${appUrl}/upload/${reverseShareToken}`;
return modals.openModal({
title: "Reverse share link",
title: t("account.reverseShares.modal.reverse-share-link"),
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />

View File

@@ -1,10 +1,12 @@
import { Text, Divider, Progress, Stack, Group, Flex } from "@mantine/core";
import { Divider, Flex, Progress, Stack, Text } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { MyShare } from "../../types/share.type";
import moment from "moment";
import { FormattedMessage } from "react-intl";
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
import { FileMetaData } from "../../types/File.type";
import { MyShare } from "../../types/share.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import CopyTextField from "../upload/CopyTextField";
import { FileMetaData } from "../../types/File.type";
const showShareInformationsModal = (
modals: ModalsContextProps,
@@ -12,6 +14,7 @@ const showShareInformationsModal = (
appUrl: string,
maxShareSize: number
) => {
const t = translateOutsideContext();
const link = `${appUrl}/share/${share.id}`;
let shareSize: number = 0;
@@ -29,34 +32,45 @@ const showShareInformationsModal = (
: moment(share.expiration).format("LLL");
return modals.openModal({
title: "Share informations",
title: t("account.shares.modal.share-informations"),
children: (
<Stack align="stretch" spacing="md">
<Text size="sm" color="lightgray">
<b>ID:</b> {share.id}
<b>
<FormattedMessage id="account.shares.table.id" />:{" "}
</b>
{share.id}
</Text>
<Text size="sm" color="lightgray">
<b>Description:</b> {share.description || "No description"}
<b>
<FormattedMessage id="account.shares.table.description" />:{" "}
</b>
{share.description || "No description"}
</Text>
<Text size="sm" color="lightgray">
<b>Created at:</b> {formattedCreatedAt}
<b>
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
</b>
{formattedCreatedAt}
</Text>
<Text size="sm" color="lightgray">
<b>Expires at:</b> {formattedExpiration}
<b>
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
</b>
{formattedExpiration}
</Text>
<Divider />
<CopyTextField link={link} />
<Divider />
<Text size="sm" color="lightgray">
<b>Size:</b> {formattedShareSize} / {formattedMaxShareSize} (
<b>
<FormattedMessage id="account.shares.table.size" />:{" "}
</b>
{formattedShareSize} / {formattedMaxShareSize} (
{shareSizeProgress.toFixed(1)}%)
</Text>

View File

@@ -1,14 +1,16 @@
import { Stack, TextInput } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
const showShareLinkModal = (
modals: ModalsContextProps,
shareId: string,
appUrl: string
) => {
const t = translateOutsideContext();
const link = `${appUrl}/share/${shareId}`;
return modals.openModal({
title: "Share link",
title: t("account.shares.modal.share-link"),
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />

View File

@@ -9,6 +9,7 @@ import {
} from "@mantine/core";
import Link from "next/link";
import { Dispatch, SetStateAction } from "react";
import { FormattedMessage } from "react-intl";
import useConfig from "../../../hooks/config.hook";
import Logo from "../../Logo";
@@ -42,7 +43,7 @@ const ConfigurationHeader = ({
</Link>
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
<Button variant="light" component={Link} href="/admin">
Go back
<FormattedMessage id="common.button.go-back" />
</Button>
</MediaQuery>
</Group>

View File

@@ -12,6 +12,7 @@ import {
import Link from "next/link";
import { Dispatch, SetStateAction } from "react";
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
const categories = [
{ name: "General", icon: <TbSquare /> },
@@ -53,7 +54,7 @@ const ConfigurationNavBar = ({
>
<Navbar.Section>
<Text size="xs" color="dimmed" mb="sm">
Configuration
<FormattedMessage id="admin.config.title" />
</Text>
<Stack spacing="xs">
{categories.map((category) => (
@@ -79,7 +80,11 @@ const ConfigurationNavBar = ({
>
{category.icon}
</ThemeIcon>
<Text size="sm">{category.name}</Text>
<Text size="sm">
<FormattedMessage
id={`admin.config.category.${category.name.toLowerCase()}`}
/>
</Text>
</Group>
</Box>
))}
@@ -87,7 +92,7 @@ const ConfigurationNavBar = ({
</Navbar.Section>
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
<Button mt="xl" variant="light" component={Link} href="/admin">
Go back
<FormattedMessage id="common.button.go-back" />
</Button>
</MediaQuery>
</Navbar>

View File

@@ -2,6 +2,8 @@ import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { Dispatch, SetStateAction } from "react";
import { TbUpload } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../../hooks/useTranslate.hook";
const LogoConfigInput = ({
logo,
@@ -11,14 +13,16 @@ const LogoConfigInput = ({
setLogo: Dispatch<SetStateAction<File | null>>;
}) => {
const isMobile = useMediaQuery("(max-width: 560px)");
const t = useTranslate();
return (
<Group position="apart">
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
<Title order={6}>Logo</Title>
<Title order={6}>
<FormattedMessage id="admin.config.general.logo" />
</Title>
<Text color="dimmed" size="sm" mb="xs">
Change your logo by uploading a new image. The image must be a PNG and
should have the format 1:1.
<FormattedMessage id="admin.config.general.logo.description" />
</Text>
</Stack>
<Stack></Stack>
@@ -29,7 +33,7 @@ const LogoConfigInput = ({
value={logo}
onChange={(v) => setLogo(v)}
accept=".png"
placeholder="Pick image"
placeholder={t("admin.config.general.logo.placeholder")}
/>
</Box>
</Group>

View File

@@ -1,6 +1,7 @@
import { Button, Stack, Text, Textarea } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import useUser from "../../../hooks/user.hook";
import configService from "../../../services/config.service";
import toast from "../../../utils/toast.util";
@@ -65,7 +66,7 @@ const TestEmailButton = ({
}
}}
>
Send test email
<FormattedMessage id="admin.config.smtp.button.test" />
</Button>
);
};

View File

@@ -3,6 +3,7 @@ import { useModals } from "@mantine/modals";
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
import User from "../../../types/user.type";
import showUpdateUserModal from "./showUpdateUserModal";
import { FormattedMessage, useIntl } from "react-intl";
const ManageUserTable = ({
users,
@@ -22,9 +23,15 @@ const ManageUserTable = ({
<Table verticalSpacing="sm">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Admin</th>
<th>
<FormattedMessage id="admin.users.table.username" />
</th>
<th>
<FormattedMessage id="admin.users.table.email" />
</th>
<th>
<FormattedMessage id="admin.users.table.admin" />
</th>
<th></th>
</tr>
</thead>

View File

@@ -8,7 +8,9 @@ import {
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate from "../../../hooks/useTranslate.hook";
import userService from "../../../services/user.service";
import toast from "../../../utils/toast.util";
@@ -34,6 +36,7 @@ const Body = ({
smtpEnabled: boolean;
getUsers: () => void;
}) => {
const t = useTranslate();
const form = useForm({
initialValues: {
username: "",
@@ -44,9 +47,14 @@ const Body = ({
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email(),
username: yup.string().min(3),
password: yup.string().min(8).optional(),
email: yup.string().email(t("common.error.invalid-email")),
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.optional(),
})
),
});
@@ -65,14 +73,22 @@ const Body = ({
})}
>
<Stack>
<TextInput label="Username" {...form.getInputProps("username")} />
<TextInput label="Email" {...form.getInputProps("email")} />
<TextInput
label={t("admin.users.modal.create.username")}
{...form.getInputProps("username")}
/>
<TextInput
label={t("admin.users.modal.create.email")}
{...form.getInputProps("email")}
/>
{smtpEnabled && (
<Switch
mt="xs"
labelPosition="left"
label="Set password manually"
description="If not checked, the user will receive an email with a link to set their password."
label={t("admin.users.modal.create.manual-password")}
description={t(
"admin.users.modal.create.manual-password.description"
)}
{...form.getInputProps("setPasswordManually", {
type: "checkbox",
})}
@@ -80,7 +96,7 @@ const Body = ({
)}
{(form.values.setPasswordManually || !smtpEnabled) && (
<PasswordInput
label="Password"
label={t("admin.users.modal.create.password")}
{...form.getInputProps("password")}
/>
)}
@@ -93,12 +109,14 @@ const Body = ({
}}
mt="xs"
labelPosition="left"
label="Admin privileges"
description="If checked, the user will be able to access the admin panel."
label={t("admin.users.modal.create.admin")}
description={t("admin.users.modal.create.admin.description")}
{...form.getInputProps("isAdmin", { type: "checkbox" })}
/>
<Group position="right">
<Button type="submit">Create</Button>
<Button type="submit">
<FormattedMessage id="common.button.create" />
</Button>
</Group>
</Stack>
</form>

View File

@@ -9,7 +9,11 @@ import {
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate, {
translateOutsideContext,
} from "../../../hooks/useTranslate.hook";
import userService from "../../../services/user.service";
import User from "../../../types/user.type";
import toast from "../../../utils/toast.util";
@@ -19,8 +23,9 @@ const showUpdateUserModal = (
user: User,
getUsers: () => void
) => {
const t = translateOutsideContext();
return modals.openModal({
title: `Update ${user.username}`,
title: t("admin.users.edit.update.title", { username: user.username }),
children: <Body user={user} modals={modals} getUsers={getUsers} />,
});
};
@@ -34,6 +39,8 @@ const Body = ({
user: User;
getUsers: () => void;
}) => {
const t = useTranslate();
const accountForm = useForm({
initialValues: {
username: user.username,
@@ -42,8 +49,10 @@ const Body = ({
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email(),
username: yup.string().min(3),
email: yup.string().email(t("common.error.invalid-email")),
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
})
),
});
@@ -54,7 +63,9 @@ const Body = ({
},
validate: yupResolver(
yup.object().shape({
password: yup.string().min(8),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 })),
})
),
});
@@ -75,21 +86,26 @@ const Body = ({
>
<Stack>
<TextInput
label="Username"
label={t("admin.users.table.username")}
{...accountForm.getInputProps("username")}
/>
<TextInput label="Email" {...accountForm.getInputProps("email")} />
<TextInput
label={t("admin.users.table.email")}
{...accountForm.getInputProps("email")}
/>
<Switch
mt="xs"
labelPosition="left"
label="Admin privileges"
label={t("admin.users.edit.update.admin-privileges")}
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
/>
</Stack>
</form>
<Accordion>
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
<Accordion.Control px={0}>Change password</Accordion.Control>
<Accordion.Control px={0}>
<FormattedMessage id="admin.users.edit.update.change-password.title" />
</Accordion.Control>
<Accordion.Panel>
<form
onSubmit={passwordForm.onSubmit(async (values) => {
@@ -97,17 +113,21 @@ const Body = ({
.update(user.id, {
password: values.password,
})
.then(() => toast.success("Password changed successfully"))
.then(() =>
toast.success(
t("admin.users.edit.update.notify.password.success")
)
)
.catch(toast.axiosError);
})}
>
<Stack>
<PasswordInput
label="New password"
label={t("admin.users.edit.update.change-password.field")}
{...passwordForm.getInputProps("password")}
/>
<Button variant="light" type="submit">
Save new password
<FormattedMessage id="admin.users.edit.update.change-password.button" />
</Button>
</Stack>
</form>
@@ -116,7 +136,7 @@ const Body = ({
</Accordion>
<Group position="right">
<Button type="submit" form="accountForm">
Save
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</Stack>

View File

@@ -15,8 +15,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import { TbInfoCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import toast from "../../utils/toast.util";
@@ -24,14 +26,18 @@ import toast from "../../utils/toast.util";
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
const config = useConfig();
const router = useRouter();
const t = useTranslate();
const { refreshUser } = useUser();
const [showTotp, setShowTotp] = React.useState(false);
const [loginToken, setLoginToken] = React.useState("");
const validationSchema = yup.object().shape({
emailOrUsername: yup.string().required(),
password: yup.string().min(8).required(),
emailOrUsername: yup.string().required(t("common.error.field-required")),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
});
const form = useForm({
@@ -54,8 +60,8 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
icon: <TbInfoCircle />,
color: "blue",
radius: "md",
title: "Two-factor authentication required",
message: "Please enter your two-factor authentication code",
title: t("signIn.notify.totp-required.title"),
message: t("signIn.notify.totp-required.description"),
});
setLoginToken(response.data["loginToken"]);
} else {
@@ -88,13 +94,13 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
return (
<Container size={420} my={40}>
<Title order={2} align="center" weight={900}>
Welcome back
<FormattedMessage id="signin.title" />
</Title>
{config.get("share.allowRegistration") && (
<Text color="dimmed" size="sm" align="center" mt={5}>
You don't have an account yet?{" "}
<FormattedMessage id="signin.description" />{" "}
<Anchor component={Link} href={"signUp"} size="sm">
{"Sign up"}
<FormattedMessage id="signin.button.signup" />
</Anchor>
</Text>
)}
@@ -107,20 +113,20 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
})}
>
<TextInput
label="Email or username"
placeholder="Your email or username"
label={t("signin.input.email-or-username")}
placeholder={t("signin.input.email-or-username.placeholder")}
{...form.getInputProps("emailOrUsername")}
/>
<PasswordInput
label="Password"
placeholder="Your password"
label={t("signin.input.password")}
placeholder={t("signin.input.password.placeholder")}
mt="md"
{...form.getInputProps("password")}
/>
{showTotp && (
<TextInput
variant="filled"
label="Code"
label={t("account.modal.totp.code")}
placeholder="******"
mt="md"
{...form.getInputProps("totp")}
@@ -129,12 +135,12 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
{config.get("smtp.enabled") && (
<Group position="right" mt="xs">
<Anchor component={Link} href="/auth/resetPassword" size="xs">
Forgot password?
<FormattedMessage id="resetPassword.title" />
</Anchor>
</Group>
)}
<Button fullWidth mt="xl" type="submit">
Sign in
<FormattedMessage id="signin.button.submit" />
</Button>
</form>
</Paper>

View File

@@ -11,8 +11,10 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import Link from "next/link";
import { useRouter } from "next/router";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import toast from "../../utils/toast.util";
@@ -20,12 +22,19 @@ import toast from "../../utils/toast.util";
const SignUpForm = () => {
const config = useConfig();
const router = useRouter();
const t = useTranslate();
const { refreshUser } = useUser();
const validationSchema = yup.object().shape({
email: yup.string().email().required(),
username: yup.string().min(3).required(),
password: yup.string().min(8).required(),
email: yup.string().email(t("common.error.invalid-email")).required(),
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 }))
.required(t("common.error.field-required")),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
});
const form = useForm({
@@ -54,13 +63,13 @@ const SignUpForm = () => {
return (
<Container size={420} my={40}>
<Title order={2} align="center" weight={900}>
Sign up
<FormattedMessage id="signup.title" />
</Title>
{config.get("share.allowRegistration") && (
<Text color="dimmed" size="sm" align="center" mt={5}>
You have an account already?{" "}
<FormattedMessage id="signup.description" />{" "}
<Anchor component={Link} href={"signIn"} size="sm">
Sign in
<FormattedMessage id="signup.button.signin" />
</Anchor>
</Text>
)}
@@ -71,24 +80,24 @@ const SignUpForm = () => {
)}
>
<TextInput
label="Username"
placeholder="Your username"
label={t("signup.input.username")}
placeholder={t("signup.input.username.placeholder")}
{...form.getInputProps("username")}
/>
<TextInput
label="Email"
placeholder="Your email"
label={t("signup.input.email")}
placeholder={t("signup.input.email.placeholder")}
mt="md"
{...form.getInputProps("email")}
/>
<PasswordInput
label="Password"
placeholder="Your password"
label={t("signin.input.password")}
placeholder={t("signin.input.password.placeholder")}
mt="md"
{...form.getInputProps("password")}
/>
<Button fullWidth mt="xl" type="submit">
Let's get started
<FormattedMessage id="signup.button.submit" />
</Button>
</form>
</Paper>

View File

@@ -3,6 +3,7 @@ import Link from "next/link";
import { TbDoorExit, TbSettings, TbUser } from "react-icons/tb";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import { FormattedMessage, useIntl } from "react-intl";
const ActionAvatar = () => {
const { user } = useUser();
@@ -16,7 +17,7 @@ const ActionAvatar = () => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
My account
<FormattedMessage id="navbar.avatar.account" />
</Menu.Item>
{user!.isAdmin && (
<Menu.Item
@@ -24,7 +25,7 @@ const ActionAvatar = () => {
href="/admin"
icon={<TbSettings size={14} />}
>
Administration
<FormattedMessage id="navbar.avatar.admin" />
</Menu.Item>
)}
@@ -34,7 +35,7 @@ const ActionAvatar = () => {
}}
icon={<TbDoorExit size={14} />}
>
Sign out
<FormattedMessage id="navbar.avatar.signout" />
</Menu.Item>
</Menu.Dropdown>
</Menu>

View File

@@ -16,6 +16,7 @@ import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import Logo from "../Logo";
import ActionAvatar from "./ActionAvatar";
import NavbarShareMenu from "./NavbarShareMenu";
@@ -112,6 +113,7 @@ const Header = () => {
const { user } = useUser();
const router = useRouter();
const config = useConfig();
const t = useTranslate();
const [opened, toggleOpened] = useDisclosure(false);
@@ -124,7 +126,7 @@ const Header = () => {
const authenticatedLinks: NavLink[] = [
{
link: "/upload",
label: "Upload",
label: t("navbar.upload"),
},
{
component: <NavbarShareMenu />,
@@ -137,27 +139,27 @@ const Header = () => {
let unauthenticatedLinks: NavLink[] = [
{
link: "/auth/signIn",
label: "Sign in",
label: t("navbar.signin"),
},
];
if (config.get("share.allowUnauthenticatedShares")) {
unauthenticatedLinks.unshift({
link: "/upload",
label: "Upload",
label: t("navbar.upload"),
});
}
if (config.get("general.showHomePage"))
unauthenticatedLinks.unshift({
link: "/",
label: "Home",
label: t("navbar.home"),
});
if (config.get("share.allowRegistration"))
unauthenticatedLinks.push({
link: "/auth/signUp",
label: "Sign up",
label: t("navbar.signup"),
});
const { classes, cx } = useStyles();

View File

@@ -1,6 +1,7 @@
import { ActionIcon, Menu } from "@mantine/core";
import Link from "next/link";
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
const NavbarShareMneu = () => {
return (
@@ -12,14 +13,14 @@ const NavbarShareMneu = () => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Item component={Link} href="/account/shares" icon={<TbLink />}>
My shares
<FormattedMessage id="navbar.links.shares" />
</Menu.Item>
<Menu.Item
component={Link}
href="/account/reverseShares"
icon={<TbArrowLoopLeft />}
>
Reverse shares
<FormattedMessage id="navbar.links.reverse" />
</Menu.Item>
</Menu.Dropdown>
</Menu>

View File

@@ -1,11 +1,15 @@
import { Button } from "@mantine/core";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import toast from "../../utils/toast.util";
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
const [isZipReady, setIsZipReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const t = useTranslate();
const downloadAll = async () => {
setIsLoading(true);
await shareService
@@ -39,13 +43,13 @@ const DownloadAllButton = ({ shareId }: { shareId: string }) => {
loading={isLoading}
onClick={() => {
if (!isZipReady) {
toast.error("The share is preparing. Try again in a few minutes.");
toast.error(t("share.notify.download-all-preparing"));
} else {
downloadAll();
}
}}
>
Download all
<FormattedMessage id="share.download-all" />
</Button>
);
};

View File

@@ -19,6 +19,8 @@ import { byteToHumanSizeString } from "../../utils/fileSize.util";
import toast from "../../utils/toast.util";
import TableSortIcon, { TableSort } from "../core/SortIcon";
import showFilePreviewModal from "./modals/showFilePreviewModal";
import useTranslate from "../../hooks/useTranslate.hook";
import { FormattedMessage } from "react-intl";
const FileList = ({
files,
@@ -34,6 +36,7 @@ const FileList = ({
const clipboard = useClipboard();
const config = useConfig();
const modals = useModals();
const t = useTranslate();
const [sort, setSort] = useState<TableSort>({
property: undefined,
@@ -68,10 +71,10 @@ const FileList = ({
if (window.isSecureContext) {
clipboard.copy(link);
toast.success("Your file link was copied to the keyboard.");
toast.success(t("common.notify.copied"));
} else {
modals.openModal({
title: "File link",
title: t("share.modal.file-link"),
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />
@@ -90,13 +93,13 @@ const FileList = ({
<tr>
<th>
<Group spacing="xs">
Name
<FormattedMessage id="share.table.name" />
<TableSortIcon sort={sort} setSort={setSort} property="name" />
</Group>
</th>
<th>
<Group spacing="xs">
Size
<FormattedMessage id="share.table.size" />
<TableSortIcon sort={sort} setSort={setSort} property="size" />
</Group>
</th>

View File

@@ -2,6 +2,7 @@ import { Button, Center, Stack, Text, Title } from "@mantine/core";
import { modals } from "@mantine/modals";
import Link from "next/link";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import api from "../../services/api.service";
const FilePreviewContext = React.createContext<{
@@ -144,10 +145,11 @@ const UnSupportedFile = () => {
return (
<Center style={{ minHeight: 200 }}>
<Stack align="center" spacing={10}>
<Title order={3}>Preview not supported</Title>
<Title order={3}>
<FormattedMessage id="share.modal.file-preview.error.not-supported.title" />
</Title>
<Text>
A preview for thise file type is unsupported. Please download the file
to view it.
<FormattedMessage id="share.modal.file-preview.error.not-supported.description" />
</Text>
</Stack>
</Center>

View File

@@ -1,6 +1,8 @@
import { Button, Stack } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import { translateOutsideContext } from "../../../hooks/useTranslate.hook";
import CopyTextField from "../../upload/CopyTextField";
const showCompletedReverseShareModal = (
@@ -8,11 +10,12 @@ const showCompletedReverseShareModal = (
link: string,
getReverseShares: () => void
) => {
const t = translateOutsideContext();
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
title: "Reverse share link",
title: t("account.reverseShares.modal.reverse-share-link"),
children: <Body link={link} getReverseShares={getReverseShares} />,
});
};
@@ -36,7 +39,7 @@ const Body = ({
getReverseShares();
}}
>
Done
<FormattedMessage id="common.button.done" />
</Button>
</Stack>
);

View File

@@ -12,6 +12,8 @@ import {
import { useForm } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { getExpirationPreview } from "../../../utils/date.util";
import toast from "../../../utils/toast.util";
@@ -42,6 +44,7 @@ const Body = ({
showSendEmailNotificationOption: boolean;
}) => {
const modals = useModals();
const t = useTranslate();
const form = useForm({
initialValues: {
@@ -79,7 +82,7 @@ const Body = ({
max={99999}
precision={0}
variant="filled"
label="Share expiration"
label={t("account.reverseShares.modal.expiration.label")}
{...form.getInputProps("expiration_num")}
/>
</Col>
@@ -91,27 +94,44 @@ const Body = ({
{
value: "-minutes",
label:
"Minute" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.minute-singular")
: t("upload.modal.expires.minute-plural"),
},
{
value: "-hours",
label:
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.hour-singular")
: t("upload.modal.expires.hour-plural"),
},
{
value: "-days",
label:
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.day-singular")
: t("upload.modal.expires.day-plural"),
},
{
value: "-weeks",
label:
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.week-singular")
: t("upload.modal.expires.week-plural"),
},
{
value: "-months",
label:
"Month" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.month-singular")
: t("upload.modal.expires.month-plural"),
},
{
value: "-years",
label:
form.values.expiration_num == 1
? t("upload.modal.expires.year-singular")
: t("upload.modal.expires.year-plural"),
},
]}
/>
@@ -125,11 +145,17 @@ const Body = ({
color: theme.colors.gray[6],
})}
>
{getExpirationPreview("reverse share", form)}
{getExpirationPreview(
{
expiresOn: t("account.reverseShare.expires-on"),
neverExpires: t("account.reverseShare.never-expires"),
},
form
)}
</Text>
</div>
<FileSizeInput
label="Max share size"
label={t("account.reverseShares.modal.max-size.label")}
value={form.values.maxShareSize}
onChange={(number) => form.setFieldValue("maxShareSize", number)}
/>
@@ -138,16 +164,18 @@ const Body = ({
max={1000}
precision={0}
variant="filled"
label="Max use count"
description="The maximum number of times this reverse share link can be used"
label={t("account.reverseShares.modal.max-use.label")}
description={t("account.reverseShares.modal.max-use.description")}
{...form.getInputProps("maxUseCount")}
/>
{showSendEmailNotificationOption && (
<Switch
mt="xs"
labelPosition="left"
label="Send email notification"
description="Send an email notification when a share is created with this reverse share link"
label={t("account.reverseShares.modal.send-email")}
description={t(
"account.reverseShares.modal.send-email.description"
)}
{...form.getInputProps("sendEmailNotification", {
type: "checkbox",
})}
@@ -155,7 +183,7 @@ const Body = ({
)}
<Button mt="md" type="submit">
Create
<FormattedMessage id="common.button.create" />
</Button>
</Stack>
</form>

View File

@@ -1,16 +1,21 @@
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import useTranslate, {
translateOutsideContext,
} from "../../hooks/useTranslate.hook";
const showEnterPasswordModal = (
modals: ModalsContextProps,
submitCallback: (password: string) => Promise<void>
) => {
const t = translateOutsideContext();
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
title: "Password required",
title: t("share.modal.password.title"),
children: <Body submitCallback={submitCallback} />,
});
};
@@ -22,10 +27,11 @@ const Body = ({
}) => {
const [password, setPassword] = useState("");
const [passwordWrong, setPasswordWrong] = useState(false);
const t = useTranslate();
return (
<Stack align="stretch">
<Text size="sm">
This access this share please enter the password for the share.
<FormattedMessage id="share.modal.password.description" />
</Text>
<form
@@ -37,13 +43,15 @@ const Body = ({
<Stack>
<PasswordInput
variant="filled"
placeholder="Password"
error={passwordWrong && "Wrong password"}
placeholder={t("share.modal.password")}
error={passwordWrong && t("share.modal.error.invalid-password")}
onFocus={() => setPasswordWrong(false)}
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<Button type="submit">Submit</Button>
<Button type="submit">
<FormattedMessage id="common.button.submit" />
</Button>
</Stack>
</form>
</Stack>

View File

@@ -2,6 +2,7 @@ import { Button, Stack, Text } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useRouter } from "next/router";
import { FormattedMessage } from "react-intl";
const showErrorModal = (
modals: ModalsContextProps,
@@ -31,7 +32,7 @@ const Body = ({ text }: { text: string }) => {
router.back();
}}
>
Go back
<FormattedMessage id="common.button.go-back" />
</Button>
</Stack>
</>

View File

@@ -2,10 +2,13 @@ import { ActionIcon, TextInput } from "@mantine/core";
import { useClipboard } from "@mantine/hooks";
import { useRef, useState } from "react";
import { TbCheck, TbCopy } from "react-icons/tb";
import useTranslate from "../../hooks/useTranslate.hook";
import toast from "../../utils/toast.util";
function CopyTextField(props: { link: string }) {
const clipboard = useClipboard({ timeout: 500 });
const t = useTranslate();
const [checkState, setCheckState] = useState(false);
const [textClicked, setTextClicked] = useState(false);
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
@@ -14,7 +17,7 @@ function CopyTextField(props: { link: string }) {
const copyLink = () => {
clipboard.copy(props.link);
toast.success("The link was copied to your clipboard.");
toast.success(t("common.notify.copied"));
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setCheckState(false);
@@ -25,7 +28,7 @@ function CopyTextField(props: { link: string }) {
return (
<TextInput
readOnly
label="Link"
label={t("common.text.link")}
variant="filled"
value={props.link}
onClick={() => {

View File

@@ -2,7 +2,8 @@ import { Button, Center, createStyles, Group, Text } from "@mantine/core";
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
import { TbCloudUpload, TbUpload } from "react-icons/tb";
import useConfig from "../../hooks/config.hook";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../hooks/useTranslate.hook";
import { FileUpload } from "../../types/File.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import toast from "../../utils/toast.util";
@@ -42,7 +43,7 @@ const Dropzone = ({
files: FileUpload[];
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
}) => {
const config = useConfig();
const t = useTranslate();
const { classes } = useStyles();
const openRef = useRef<() => void>();
@@ -62,9 +63,9 @@ const Dropzone = ({
if (fileSizeSum > maxShareSize) {
toast.error(
`Your files exceed the maximum share size of ${byteToHumanSizeString(
maxShareSize
)}.`
t("upload.dropzone.notify.file-too-big", {
maxSize: byteToHumanSizeString(maxShareSize),
})
);
} else {
newFiles = newFiles.map((newFile) => {
@@ -82,12 +83,13 @@ const Dropzone = ({
<TbCloudUpload size={50} />
</Group>
<Text align="center" weight={700} size="lg" mt="xl">
Upload files
<FormattedMessage id="upload.dropzone.title" />
</Text>
<Text align="center" size="sm" mt="xs" color="dimmed">
Drag&apos;n&apos;drop files here to start your share. We can accept
only files that are less than {byteToHumanSizeString(maxShareSize)}{" "}
in total.
<FormattedMessage
id="upload.dropzone.description"
values={{ maxSize: byteToHumanSizeString(maxShareSize) }}
/>
</Text>
</div>
</MantineDropzone>

View File

@@ -4,6 +4,7 @@ import { TbTrash } from "react-icons/tb";
import { FileUpload } from "../../types/File.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import UploadProgressIndicator from "./UploadProgressIndicator";
import { FormattedMessage } from "react-intl";
const FileList = ({
files,
@@ -41,8 +42,12 @@ const FileList = ({
<Table>
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>
<FormattedMessage id="upload.filelist.name" />
</th>
<th>
<FormattedMessage id="upload.filelist.size" />
</th>
<th></th>
</tr>
</thead>

View File

@@ -3,6 +3,10 @@ import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { useRouter } from "next/router";
import { FormattedMessage } from "react-intl";
import useTranslate, {
translateOutsideContext,
} from "../../../hooks/useTranslate.hook";
import { Share } from "../../../types/share.type";
import CopyTextField from "../CopyTextField";
@@ -11,11 +15,12 @@ const showCompletedUploadModal = (
share: Share,
appUrl: string
) => {
const t = translateOutsideContext();
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
title: "Share ready",
title: t("upload.modal.completed.share-ready"),
children: <Body share={share} appUrl={appUrl} />,
});
};
@@ -23,6 +28,7 @@ const showCompletedUploadModal = (
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
const modals = useModals();
const router = useRouter();
const t = useTranslate();
const link = `${appUrl}/share/${share.id}`;
@@ -37,10 +43,10 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
>
{/* If our share.expiration is timestamp 0, show a different message */}
{moment(share.expiration).unix() === 0
? "This share will never expire."
: `This share will expire on ${moment(share.expiration).format(
"LLL"
)}`}
? t("upload.modal.completed.never-expires")
: t("upload.modal.completed.expires-on", {
expiration: moment(share.expiration).format("LLL"),
})}
</Text>
<Button
@@ -49,7 +55,7 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
router.push("/upload");
}}
>
Done
<FormattedMessage id="common.button.done" />
</Button>
</Stack>
);

View File

@@ -13,14 +13,17 @@ import {
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate, {
translateOutsideContext,
} from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { CreateShare } from "../../../types/share.type";
import { getExpirationPreview } from "../../../utils/date.util";
@@ -36,8 +39,10 @@ const showCreateUploadModal = (
},
uploadCallback: (createShare: CreateShare) => void
) => {
const t = translateOutsideContext();
return modals.openModal({
title: "Share",
title: t("upload.modal.title"),
children: (
<CreateUploadModalBody
options={options}
@@ -61,6 +66,7 @@ const CreateUploadModalBody = ({
};
}) => {
const modals = useModals();
const t = useTranslate();
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
@@ -71,11 +77,11 @@ const CreateUploadModalBody = ({
const validationSchema = yup.object().shape({
link: yup
.string()
.required()
.min(3)
.max(50)
.required(t("common.error.field-required"))
.min(3, t("common.error.too-short", { length: 3 }))
.max(50, t("common.error.too-long", { length: 50 }))
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
message: "Can only contain letters, numbers, underscores and hyphens",
message: t("upload.modal.link.error.invalid"),
}),
password: yup.string().min(3).max(30),
maxViews: yup.number().min(1),
@@ -100,20 +106,19 @@ const CreateUploadModalBody = ({
withCloseButton
onClose={() => setShowNotSignedInAlert(false)}
icon={<TbAlertCircle size={16} />}
title="You're not signed in"
title={t("upload.modal.not-signed-in")}
color="yellow"
>
You will be unable to delete your share manually and view the visitor
count.
<FormattedMessage id="upload.modal.not-signed-in-description" />
</Alert>
)}
<form
onSubmit={form.onSubmit(async (values) => {
if (!(await shareService.isShareIdAvailable(values.link))) {
form.setFieldError("link", "This link is already in use");
form.setFieldError("link", t("upload.modal.link.error.taken"));
} else {
const expiration = form.values.never_expires
? "never"
? t("upload.modal.expires.never")
: form.values.expiration_num + form.values.expiration_unit;
uploadCallback({
id: values.link,
@@ -151,7 +156,7 @@ const CreateUploadModalBody = ({
)
}
>
Generate
<FormattedMessage id="common.button.generate" />
</Button>
</Col>
</Grid>
@@ -169,18 +174,6 @@ const CreateUploadModalBody = ({
{!options.isReverseShare && (
<>
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Col xs={6}>
<NumberInput
min={1}
max={99999}
precision={0}
variant="filled"
label="Expiration"
placeholder="n"
disabled={form.values.never_expires}
{...form.getInputProps("expiration_num")}
/>
</Col>
<Col xs={6}>
<Select
disabled={form.values.never_expires}
@@ -190,41 +183,51 @@ const CreateUploadModalBody = ({
{
value: "-minutes",
label:
"Minute" +
(form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.minute-singular")
: t("upload.modal.expires.minute-plural"),
},
{
value: "-hours",
label:
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.hour-singular")
: t("upload.modal.expires.hour-plural"),
},
{
value: "-days",
label:
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.day-singular")
: t("upload.modal.expires.day-plural"),
},
{
value: "-weeks",
label:
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.week-singular")
: t("upload.modal.expires.week-plural"),
},
{
value: "-months",
label:
"Month" +
(form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.month-singular")
: t("upload.modal.expires.month-plural"),
},
{
value: "-years",
label:
"Year" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.year-singular")
: t("upload.modal.expires.year-plural"),
},
]}
/>
</Col>
</Grid>
<Checkbox
label="Never Expires"
label={t("upload.modal.expires.never-long")}
{...form.getInputProps("never_expires")}
/>
<Text
@@ -234,18 +237,28 @@ const CreateUploadModalBody = ({
color: theme.colors.gray[6],
})}
>
{getExpirationPreview("share", form)}
{getExpirationPreview(
{
neverExpires: t("upload.modal.completed.never-expires"),
expiresOn: t("upload.modal.completed.expires-on"),
},
form
)}
</Text>
</>
)}
<Accordion>
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
<Accordion.Control>Description</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.description.title" />
</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<Textarea
variant="filled"
placeholder="Note for the recepients"
placeholder={t(
"upload.modal.accordion.description.placeholder"
)}
{...form.getInputProps("description")}
/>
</Stack>
@@ -253,11 +266,13 @@ const CreateUploadModalBody = ({
</Accordion.Item>
{options.enableEmailRecepients && (
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
<Accordion.Control>Email recipients</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.email.tile" />
</Accordion.Control>
<Accordion.Panel>
<MultiSelect
data={form.values.recipients}
placeholder="Enter email recipients"
placeholder={t("upload.modal.accordion.email.placeholder")}
searchable
{...form.getInputProps("recipients")}
creatable
@@ -266,7 +281,7 @@ const CreateUploadModalBody = ({
if (!query.match(/^\S+@\S+\.\S+$/)) {
form.setFieldError(
"recipients",
"Invalid email address"
t("upload.modal.accordion.email.invalid-email")
);
} else {
form.setFieldError("recipients", null);
@@ -283,28 +298,36 @@ const CreateUploadModalBody = ({
)}
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
<Accordion.Control>Security options</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.security.title" />
</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<PasswordInput
variant="filled"
placeholder="No password"
label="Password protection"
placeholder={t(
"upload.modal.accordion.security.password.placeholder"
)}
label={t("upload.modal.accordion.security.password.label")}
{...form.getInputProps("password")}
/>
<NumberInput
min={1}
type="number"
variant="filled"
placeholder="No limit"
label="Maximal views"
placeholder={t(
"upload.modal.accordion.security.max-views.placeholder"
)}
label={t("upload.modal.accordion.security.max-views.label")}
{...form.getInputProps("maxViews")}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Button type="submit">Share</Button>
<Button type="submit">
<FormattedMessage id="common.button.share" />
</Button>
</Stack>
</form>
</>

View File

@@ -0,0 +1,39 @@
import { getCookie } from "cookies-next";
import { createIntl, createIntlCache, useIntl } from "react-intl";
import i18nUtil from "../utils/i18n.util";
const useTranslate = () => {
const intl = useIntl();
return (
id: string,
values?: Parameters<typeof intl.formatMessage>[1],
opts?: Parameters<typeof intl.formatMessage>[2]
) => {
return intl.formatMessage({ id }, values, opts) as string;
};
};
const cache = createIntlCache();
export const translateOutsideContext = () => {
const locale =
getCookie("language")?.toString() ?? navigator.language.split("-")[0];
const intl = createIntl(
{
locale,
messages: i18nUtil.getLocaleByCode(locale)?.messages,
defaultLocale: "en",
},
cache
);
return (
id: string,
values?: Parameters<typeof intl.formatMessage>[1],
opts?: Parameters<typeof intl.formatMessage>[2]
) => {
return intl.formatMessage({ id }, values, opts) as string;
};
};
export default useTranslate;

View File

@@ -0,0 +1,51 @@
import danish from "./translations/da";
import german from "./translations/de";
import english from "./translations/en";
import spanish from "./translations/es";
import french from "./translations/fr";
import portugueze from "./translations/pt";
import thai from "./translations/th";
import chineseSimplified from "./translations/zh-CN";
export const LOCALES = {
ENGLISH: {
name: "English",
code: "en",
messages: english,
},
GERMAN: {
name: "Deutsch",
code: "de",
messages: german,
},
FRENCH: {
name: "Français",
code: "fr",
messages: french,
},
PORTUGUEZE: {
name: "Português",
code: "pt",
messages: portugueze,
},
DANISH: {
name: "Dansk",
code: "da",
messages: danish,
},
THAI: {
name: "ไทย",
code: "th",
messages: thai,
},
SPANISH: {
name: "Español",
code: "es",
messages: spanish,
},
CHINESE_SIMPLIFIED: {
name: "简体中文",
code: "zh-CN",
messages: chineseSimplified,
},
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -0,0 +1,435 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.color.title": "Color scheme",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informatons",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.link.placeholder": "myAwesomeShare",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthorized users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
// 404
"404.title": "404",
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Gring me back home",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};

View File

@@ -8,6 +8,7 @@ import {
} from "@mantine/core";
import Link from "next/link";
import Meta from "../components/Meta";
import { FormattedMessage } from "react-intl";
const useStyles = createStyles((theme) => ({
root: {
@@ -42,9 +43,11 @@ const ErrorNotFound = () => {
<>
<Meta title="Not found" />
<Container className={classes.root}>
<div className={classes.label}>404</div>
<div className={classes.label}>
<FormattedMessage id="404.title" />
</div>
<Title align="center" order={3}>
Oops this page doesn't exist.
<FormattedMessage id="404.description" />
</Title>
<Text
color="dimmed"
@@ -53,7 +56,7 @@ const ErrorNotFound = () => {
></Text>
<Group position="center">
<Button component={Link} href="/" variant="light">
Bring me back
<FormattedMessage id="404.button.home" />
</Button>
</Group>
</Container>

View File

@@ -13,11 +13,12 @@ import { GetServerSidePropsContext } from "next";
import type { AppProps } from "next/app";
import getConfig from "next/config";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { IntlProvider } from "react-intl";
import Header from "../components/header/Header";
import { ConfigContext } from "../hooks/config.hook";
import usePreferences from "../hooks/usePreferences";
import { UserContext } from "../hooks/user.hook";
import { LOCALES } from "../i18n/locales";
import authService from "../services/auth.service";
import configService from "../services/config.service";
import userService from "../services/user.service";
@@ -25,6 +26,8 @@ import GlobalStyle from "../styles/global.style";
import globalStyle from "../styles/mantine.style";
import Config from "../types/config.type";
import { CurrentUser } from "../types/user.type";
import i18nUtil from "../utils/i18n.util";
import userPreferences from "../utils/userPreferences.util";
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
@@ -33,7 +36,6 @@ function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
const preferences = usePreferences();
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
const [route, setRoute] = useState<string>(pageProps.route);
@@ -50,11 +52,20 @@ function App({ Component, pageProps }: AppProps) {
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
}, []);
useEffect(() => {
if (!pageProps.language) return;
const cookieLanguage = getCookie("language");
if (pageProps.language != cookieLanguage) {
i18nUtil.setLanguageCookie(pageProps.language);
if (cookieLanguage) location.reload();
}
}, []);
useEffect(() => {
const colorScheme =
preferences.get("colorScheme") == "system"
userPreferences.get("colorScheme") == "system"
? systemTheme
: preferences.get("colorScheme");
: userPreferences.get("colorScheme");
toggleColorScheme(colorScheme);
}, [systemTheme]);
@@ -66,52 +77,60 @@ function App({ Component, pageProps }: AppProps) {
});
};
const language = useRef(pageProps.language);
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{ colorScheme, ...globalStyle }}
<IntlProvider
messages={i18nUtil.getLocaleByCode(language.current)?.messages}
locale={language.current}
defaultLocale={LOCALES.ENGLISH.code}
>
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{ colorScheme, ...globalStyle }}
>
<GlobalStyle />
<Notifications />
<ModalsProvider>
<ConfigContext.Provider
value={{
configVariables,
refresh: async () => {
setConfigVariables(await configService.list());
},
}}
>
<UserContext.Provider
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<GlobalStyle />
<Notifications />
<ModalsProvider>
<ConfigContext.Provider
value={{
user,
refreshUser: async () => {
const user = await userService.getCurrentUser();
setUser(user);
return user;
configVariables,
refresh: async () => {
setConfigVariables(await configService.list());
},
}}
>
{excludeDefaultLayoutRoutes.includes(route) ? (
<Component {...pageProps} />
) : (
<>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</>
)}
</UserContext.Provider>
</ConfigContext.Provider>
</ModalsProvider>
</ColorSchemeProvider>
</MantineProvider>
<UserContext.Provider
value={{
user,
refreshUser: async () => {
const user = await userService.getCurrentUser();
setUser(user);
return user;
},
}}
>
{excludeDefaultLayoutRoutes.includes(route) ? (
<Component {...pageProps} />
) : (
<>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</>
)}
</UserContext.Provider>
</ConfigContext.Provider>
</ModalsProvider>
</ColorSchemeProvider>
</MantineProvider>
</IntlProvider>
);
}
@@ -125,11 +144,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
configVariables?: Config[];
route?: string;
colorScheme: ColorScheme;
language?: string;
} = {
route: ctx.resolvedUrl,
colorScheme:
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
};
if (ctx.req) {
const cookieHeader = ctx.req.headers.cookie;
@@ -142,8 +163,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
pageProps.route = ctx.req.url;
}
const requestLanguage = i18nUtil.getLanguageFromAcceptHeader(
ctx.req.headers["accept-language"]
);
pageProps.language = ctx.req.cookies["language"] ?? requestLanguage;
}
return { pageProps };
};

View File

@@ -14,18 +14,22 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { Tb2Fa } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
import Meta from "../../components/Meta";
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import userService from "../../services/user.service";
import toast from "../../utils/toast.util";
import LanguagePicker from "../../components/account/LanguagePicker";
const Account = () => {
const { user, refreshUser } = useUser();
const modals = useModals();
const t = useTranslate();
const accountForm = useForm({
initialValues: {
@@ -34,8 +38,10 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email(),
username: yup.string().min(3),
email: yup.string().email(t("common.error.invalid-email")),
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
})
),
});
@@ -47,8 +53,14 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
oldPassword: yup.string().min(8),
password: yup.string().min(8),
oldPassword: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
),
});
@@ -59,7 +71,10 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
password: yup.string().min(8),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
),
});
@@ -74,23 +89,23 @@ const Account = () => {
password: yup.string().min(8),
code: yup
.string()
.min(6)
.max(6)
.matches(/^[0-9]+$/, { message: "Code must be a number" }),
.min(6, t("common.error.exact-length", { length: 6 }))
.max(6, t("common.error.exact-length", { length: 6 }))
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
})
),
});
return (
<>
<Meta title="My account" />
<Meta title={t("account.title")} />
<Container size="sm">
<Title order={3} mb="xs">
My account
<FormattedMessage id="account.title" />
</Title>
<Paper withBorder p="xl">
<Title order={5} mb="xs">
Account Info
<FormattedMessage id="account.card.info.title" />
</Title>
<form
onSubmit={accountForm.onSubmit((values) =>
@@ -99,35 +114,37 @@ const Account = () => {
username: values.username,
email: values.email,
})
.then(() => toast.success("User updated successfully"))
.then(() => toast.success(t("account.notify.info.success")))
.catch(toast.axiosError)
)}
>
<Stack>
<TextInput
label="Username"
label={t("account.card.info.username")}
{...accountForm.getInputProps("username")}
/>
<TextInput
label="Email"
label={t("account.card.info.email")}
{...accountForm.getInputProps("email")}
/>
<Group position="right">
<Button type="submit">Save</Button>
<Button type="submit">
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</Stack>
</form>
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Password
<FormattedMessage id="account.card.password.title" />
</Title>
<form
onSubmit={passwordForm.onSubmit((values) =>
authService
.updatePassword(values.oldPassword, values.password)
.then(() => {
toast.success("Password updated successfully");
toast.success(t("account.notify.password.success"));
passwordForm.reset();
})
.catch(toast.axiosError)
@@ -135,15 +152,17 @@ const Account = () => {
>
<Stack>
<PasswordInput
label="Old password"
label={t("account.card.password.old")}
{...passwordForm.getInputProps("oldPassword")}
/>
<PasswordInput
label="New password"
label={t("account.card.password.new")}
{...passwordForm.getInputProps("password")}
/>
<Group position="right">
<Button type="submit">Save</Button>
<Button type="submit">
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</Stack>
</form>
@@ -151,7 +170,7 @@ const Account = () => {
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Security
<FormattedMessage id="account.card.security.title" />
</Title>
<Tabs defaultValue="totp">
@@ -169,7 +188,7 @@ const Account = () => {
authService
.disableTOTP(values.code, values.password)
.then(() => {
toast.success("Successfully disabled TOTP");
toast.success(t("account.notify.totp.disable"));
values.password = "";
values.code = "";
refreshUser();
@@ -179,21 +198,23 @@ const Account = () => {
>
<Stack>
<PasswordInput
description="Enter your current password to disable TOTP"
label="Password"
description={t(
"account.card.security.totp.disable.description"
)}
label={t("account.card.password.title")}
{...disableTotpForm.getInputProps("password")}
/>
<TextInput
variant="filled"
label="Code"
label={t("account.modal.totp.code")}
placeholder="******"
{...disableTotpForm.getInputProps("code")}
/>
<Group position="right">
<Button color="red" type="submit">
Disable
<FormattedMessage id="common.button.disable" />
</Button>
</Group>
</Stack>
@@ -218,12 +239,16 @@ const Account = () => {
>
<Stack>
<PasswordInput
label="Password"
description="Enter your current password to start enabling TOTP"
label={t("account.card.password.title")}
description={t(
"account.card.security.totp.enable.description"
)}
{...enableTotpForm.getInputProps("password")}
/>
<Group position="right">
<Button type="submit">Start</Button>
<Button type="submit">
<FormattedMessage id="account.card.security.totp.button.start" />
</Button>
</Group>
</Stack>
</form>
@@ -234,7 +259,13 @@ const Account = () => {
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Color scheme
<FormattedMessage id="account.card.language.title" />
</Title>
<LanguagePicker />
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
<FormattedMessage id="account.card.color.title" />
</Title>
<ThemeSwitcher />
</Paper>
@@ -245,15 +276,17 @@ const Account = () => {
color="red"
onClick={() =>
modals.openConfirmModal({
title: "Account deletion",
title: t("account.modal.delete.title"),
children: (
<Text size="sm">
Do you really want to delete your account including all
your active shares?
<FormattedMessage id="account.modal.delete.description" />
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
confirmProps: { color: "red" },
onConfirm: async () => {
await userService.removeCurrentUser();
@@ -262,7 +295,7 @@ const Account = () => {
})
}
>
Delete Account
<FormattedMessage id="account.button.delete" />
</Button>
</Stack>
</Center>

View File

@@ -17,12 +17,14 @@ import { useModals } from "@mantine/modals";
import moment from "moment";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import { MyReverseShare } from "../../types/share.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
@@ -31,13 +33,14 @@ import toast from "../../utils/toast.util";
const MyShares = () => {
const modals = useModals();
const clipboard = useClipboard();
const t = useTranslate();
const config = useConfig();
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
const appUrl = config.get("general.appUrl");
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
const getReverseShares = () => {
shareService
.getMyReverseShares()
@@ -51,15 +54,17 @@ const MyShares = () => {
if (!reverseShares) return <CenterLoader />;
return (
<>
<Meta title="My shares" />
<Meta title={t("account.reverseShares.title")} />
<Group position="apart" align="baseline" mb={20}>
<Group align="center" spacing={3} mb={30}>
<Title order={3}>My reverse shares</Title>
<Title order={3}>
<FormattedMessage id="account.reverseShares.title" />
</Title>
<Tooltip
position="bottom"
multiline
width={220}
label="A reverse share allows you to generate a unique URL that allows external users to create a share."
label={t("account.reverseShares.description")}
events={{ hover: true, focus: false, touch: true }}
>
<ActionIcon>
@@ -77,14 +82,18 @@ const MyShares = () => {
}
leftIcon={<TbPlus size={20} />}
>
Create
<FormattedMessage id="common.button.create" />
</Button>
</Group>
{reverseShares.length == 0 ? (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Title order={3}>It's empty here 👀</Title>
<Text>You don't have any reverse shares.</Text>
<Title order={3}>
<FormattedMessage id="account.reverseShares.title.empty" />
</Title>
<Text>
<FormattedMessage id="account.reverseShares.description.empty" />
</Text>
</Stack>
</Center>
) : (
@@ -92,10 +101,18 @@ const MyShares = () => {
<Table>
<thead>
<tr>
<th>Shares</th>
<th>Remaining uses</th>
<th>Max share size</th>
<th>Expires at</th>
<th>
<FormattedMessage id="account.reverseShares.table.shares" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.remaining" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.max-size" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.expires" />
</th>
<th></th>
</tr>
</thead>
@@ -105,7 +122,7 @@ const MyShares = () => {
<td style={{ width: 220 }}>
{reverseShare.shares.length == 0 ? (
<Text color="dimmed" size="sm">
No shares created yet
<FormattedMessage id="account.reverseShares.table.no-shares" />
</Text>
) : (
<Accordion>
@@ -115,9 +132,13 @@ const MyShares = () => {
>
<Accordion.Control p={0}>
<Text size="sm">
{`${reverseShare.shares.length} share${
reverseShare.shares.length > 1 ? "s" : ""
}`}
{reverseShare.shares.length == 1
? `1 ${t(
"account.reverseShares.table.count.singular"
)}`
: `${reverseShare.shares.length} ${t(
"account.reverseShares.table.count.plural"
)}`}
</Text>
</Accordion.Control>
<Accordion.Panel>
@@ -140,9 +161,7 @@ const MyShares = () => {
clipboard.copy(
`${appUrl}/share/${share.id}`
);
toast.success(
"The share link was copied to the keyboard."
);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
@@ -183,9 +202,7 @@ const MyShares = () => {
reverseShare.token
}`
);
toast.success(
"The link was copied to your clipboard."
);
toast.success(t("common.notify.copied"));
} else {
showReverseShareLinkModal(
modals,
@@ -203,18 +220,21 @@ const MyShares = () => {
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete reverse share`,
title: t(
"account.reverseShares.modal.delete.title"
),
children: (
<Text size="sm">
Do you really want to delete this reverse share?
If you do, the associated shares will be deleted
as well.
<FormattedMessage id="account.reverseShares.modal.delete.description" />
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Delete", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
onConfirm: () => {
shareService.removeReverseShare(reverseShare.id);
setReverseShares(

View File

@@ -17,11 +17,13 @@ import moment from "moment";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import { MyShare } from "../../types/share.type";
import toast from "../../utils/toast.util";
@@ -30,6 +32,7 @@ const MyShares = () => {
const modals = useModals();
const clipboard = useClipboard();
const config = useConfig();
const t = useTranslate();
const [shares, setShares] = useState<MyShare[]>();
@@ -41,18 +44,22 @@ const MyShares = () => {
return (
<>
<Meta title="My shares" />
<Meta title={t("account.shares.title")} />
<Title mb={30} order={3}>
My shares
<FormattedMessage id="account.shares.title" />
</Title>
{shares.length == 0 ? (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Title order={3}>It's empty here 👀</Title>
<Text>You don't have any shares.</Text>
<Title order={3}>
<FormattedMessage id="account.shares.title.empty" />
</Title>
<Text>
<FormattedMessage id="account.shares.description.empty" />
</Text>
<Space h={5} />
<Button component={Link} href="/upload" variant="light">
Create one
<FormattedMessage id="account.shares.button.create" />
</Button>
</Stack>
</Center>
@@ -61,13 +68,21 @@ const MyShares = () => {
<Table>
<thead>
<tr>
<th>Name</th>
<th>
<FormattedMessage id="account.shares.table.name" />
</th>
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
<th>Description</th>
<th>
<FormattedMessage id="account.shares.table.description" />
</th>
</MediaQuery>
<th>Visitors</th>
<th>Expires at</th>
<th>
<FormattedMessage id="account.shares.table.visitors" />
</th>
<th>
<FormattedMessage id="account.shares.table.expiresAt" />
</th>
<th></th>
</tr>
</thead>
@@ -121,9 +136,7 @@ const MyShares = () => {
share.id
}`
);
toast.success(
"The link was copied to your clipboard."
);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
@@ -141,16 +154,21 @@ const MyShares = () => {
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete share ${share.id}`,
title: t("account.shares.modal.delete.title", {
share: share.id,
}),
children: (
<Text size="sm">
Do you really want to delete this share?
<FormattedMessage id="account.shares.modal.delete.description" />
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
onConfirm: () => {
shareService.remove(share.id);
setShares(

View File

@@ -13,25 +13,28 @@ import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import Meta from "../../../components/Meta";
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
import CenterLoader from "../../../components/core/CenterLoader";
import Meta from "../../../components/Meta";
import useConfig from "../../../hooks/config.hook";
import configService from "../../../services/config.service";
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
import {
camelToKebab,
capitalizeFirstLetter,
configVariableToFriendlyName,
} from "../../../utils/string.util";
import toast from "../../../utils/toast.util";
import useTranslate from "../../../hooks/useTranslate.hook";
export default function AppShellDemo() {
const theme = useMantineTheme();
const router = useRouter();
const t = useTranslate();
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
const isMobile = useMediaQuery("(max-width: 560px)");
@@ -94,7 +97,7 @@ export default function AppShellDemo() {
return (
<>
<Meta title="Configuration" />
<Meta title={t("admin.config.title")} />
<AppShell
styles={{
main: {
@@ -134,26 +137,28 @@ export default function AppShellDemo() {
spacing={0}
>
<Title order={6}>
{configVariableToFriendlyName(configVariable.name)}
<FormattedMessage
id={`admin.config.${camelToKebab(
configVariable.key
)}`}
/>
</Title>
{configVariable.description.split("\n").length == 1 ? (
<Text color="dimmed" size="sm" mb="xs">
{configVariable.description}
</Text>
) : (
configVariable.description.split("\n").map((line) => (
<Text
key={line}
color="dimmed"
size="sm"
style={{
marginBottom: line === "" ? "1rem" : "0",
}}
>
{line}
</Text>
))
)}
<Text
sx={{
whiteSpace: "pre-line",
}}
color="dimmed"
size="sm"
mb="xs"
>
<FormattedMessage
id={`admin.config.${camelToKebab(
configVariable.key
)}.description`}
values={{ br: <br /> }}
/>
</Text>
</Stack>
<Stack></Stack>
<Box style={{ width: isMobile ? "100%" : "50%" }}>
@@ -176,7 +181,9 @@ export default function AppShellDemo() {
saveConfigVariables={saveConfigVariables}
/>
)}
<Button onClick={saveConfigVariables}>Save</Button>
<Button onClick={saveConfigVariables}>
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</>
)}

View File

@@ -11,7 +11,9 @@ import {
import Link from "next/link";
import { useEffect, useState } from "react";
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import useTranslate from "../../hooks/useTranslate.hook";
import configService from "../../services/config.service";
const useStyles = createStyles((theme) => ({
@@ -31,15 +33,16 @@ const useStyles = createStyles((theme) => ({
const Admin = () => {
const { classes, theme } = useStyles();
const t = useTranslate();
const [managementOptions, setManagementOptions] = useState([
{
title: "User management",
title: t("admin.button.users"),
icon: TbUsers,
route: "/admin/users",
},
{
title: "Configuration",
title: t("admin.button.config"),
icon: TbSettings,
route: "/admin/config/general",
},
@@ -63,9 +66,9 @@ const Admin = () => {
return (
<>
<Meta title="Administration" />
<Meta title={t("admin.title")} />
<Title mb={30} order={3}>
Administration
<FormattedMessage id="admin.title" />
</Title>
<Stack justify="space-between" style={{ height: "calc(100vh - 180px)" }}>
<Paper withBorder p={40}>
@@ -91,7 +94,7 @@ const Admin = () => {
<Center>
<Text size="xs" color="dimmed">
Version {process.env.VERSION}
<FormattedMessage id="admin.version" /> {process.env.VERSION}
</Text>
</Center>
</Stack>

View File

@@ -2,10 +2,12 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { useEffect, useState } from "react";
import { TbPlus } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import ManageUserTable from "../../components/admin/users/ManageUserTable";
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import userService from "../../services/user.service";
import User from "../../types/user.type";
import toast from "../../utils/toast.util";
@@ -16,6 +18,7 @@ const Users = () => {
const config = useConfig();
const modals = useModals();
const t = useTranslate();
const getUsers = () => {
setIsLoading(true);
@@ -27,14 +30,18 @@ const Users = () => {
const deleteUser = (user: User) => {
modals.openConfirmModal({
title: `Delete ${user.username}?`,
title: t("admin.users.edit.delete.title", {
username: user.username,
}),
children: (
<Text size="sm">
Do you really want to delete <b>{user.username}</b> and all his
shares?
<FormattedMessage id="admin.users.edit.delete.description" />
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
confirmProps: { color: "red" },
onConfirm: async () => {
userService
@@ -51,10 +58,10 @@ const Users = () => {
return (
<>
<Meta title="User management" />
<Meta title={t("admin.users.title")} />
<Group position="apart" align="baseline" mb={20}>
<Title mb={30} order={3}>
User management
<FormattedMessage id="admin.users.title" />
</Title>
<Button
onClick={() =>
@@ -62,7 +69,7 @@ const Users = () => {
}
leftIcon={<TbPlus size={20} />}
>
Create
<FormattedMessage id="common.button.create" />
</Button>
</Group>

View File

@@ -10,7 +10,9 @@ import {
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { useRouter } from "next/router";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate from "../../../hooks/useTranslate.hook";
import authService from "../../../services/auth.service";
import toast from "../../../utils/toast.util";
@@ -25,6 +27,7 @@ const useStyles = createStyles((theme) => ({
const ResetPassword = () => {
const { classes } = useStyles();
const router = useRouter();
const t = useTranslate();
const form = useForm({
initialValues: {
@@ -32,7 +35,10 @@ const ResetPassword = () => {
},
validate: yupResolver(
yup.object().shape({
password: yup.string().min(8).required(),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
),
});
@@ -42,10 +48,10 @@ const ResetPassword = () => {
return (
<Container size={460} my={30}>
<Title order={2} weight={900} align="center">
Reset password
<FormattedMessage id="resetPassword.text.resetPassword" />
</Title>
<Text color="dimmed" size="sm" align="center">
Enter your new password
<FormattedMessage id="resetPassword.text.enterNewPassword" />
</Text>
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
@@ -54,7 +60,7 @@ const ResetPassword = () => {
authService
.resetPassword(resetPasswordToken, values.password)
.then(() => {
toast.success("Your password has been reset successfully.");
toast.success(t("resetPassword.notify.passwordReset"));
router.push("/auth/signIn");
})
@@ -62,13 +68,13 @@ const ResetPassword = () => {
})}
>
<PasswordInput
label="New password"
label={t("resetPassword.text.password")}
placeholder="••••••••••"
{...form.getInputProps("password")}
/>
<Group position="right" mt="lg">
<Button type="submit" className={classes.control}>
Reset password
<FormattedMessage id="resetPassword.button.resetPassword" />
</Button>
</Group>
</form>

View File

@@ -15,7 +15,9 @@ import { useForm, yupResolver } from "@mantine/form";
import Link from "next/link";
import { useRouter } from "next/router";
import { TbArrowLeft } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate from "../../../hooks/useTranslate.hook";
import authService from "../../../services/auth.service";
import toast from "../../../utils/toast.util";
@@ -43,6 +45,7 @@ const useStyles = createStyles((theme) => ({
const ResetPassword = () => {
const { classes } = useStyles();
const router = useRouter();
const t = useTranslate();
const form = useForm({
initialValues: {
@@ -50,7 +53,10 @@ const ResetPassword = () => {
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email().required(),
email: yup
.string()
.email(t("common.error.invalid-email"))
.required(t("common.error.field-required")),
})
),
});
@@ -58,10 +64,10 @@ const ResetPassword = () => {
return (
<Container size={460} my={30}>
<Title order={2} weight={900} align="center">
Forgot your password?
<FormattedMessage id="resetPassword.title" />
</Title>
<Text color="dimmed" size="sm" align="center">
Enter your email to get a reset link
<FormattedMessage id="resetPassword.description" />
</Text>
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
@@ -70,15 +76,15 @@ const ResetPassword = () => {
authService
.requestResetPassword(values.email)
.then(() => {
toast.success("The email has been sent.");
toast.success(t("resetPassword.notify.success"));
router.push("/auth/signIn");
})
.catch(toast.axiosError)
)}
>
<TextInput
label="Your email"
placeholder="Your email"
label={t("signup.input.email")}
placeholder={t("signup.input.email.placeholder")}
{...form.getInputProps("email")}
/>
<Group position="apart" mt="lg" className={classes.controls}>
@@ -91,11 +97,13 @@ const ResetPassword = () => {
>
<Center inline>
<TbArrowLeft size={12} />
<Box ml={5}>Back to login page</Box>
<Box ml={5}>
<FormattedMessage id="resetPassword.button.back" />
</Box>
</Center>
</Anchor>
<Button type="submit" className={classes.control}>
Reset password
<FormattedMessage id="resetPassword.text.resetPassword" />
</Button>
</Group>
</form>

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import SignInForm from "../../components/auth/SignInForm";
import Meta from "../../components/Meta";
import useUser from "../../hooks/user.hook";
import useTranslate from "../../hooks/useTranslate.hook";
export function getServerSideProps(context: GetServerSidePropsContext) {
return {
@@ -15,6 +16,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
const { refreshUser } = useUser();
const router = useRouter();
const t = useTranslate();
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
@@ -34,7 +36,7 @@ const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
return (
<>
<Meta title="Sign In" />
<Meta title={t("signin.title")} />
<SignInForm redirectPath={redirectPath ?? "/upload"} />
</>
);

View File

@@ -1,10 +1,12 @@
import SignUpForm from "../../components/auth/SignUpForm";
import Meta from "../../components/Meta";
import useTranslate from "../../hooks/useTranslate.hook";
const SignUp = () => {
const t = useTranslate();
return (
<>
<Meta title="Sign Up" />
<Meta title={t("signup.title")} />
<SignUpForm />
</>
);

View File

@@ -12,6 +12,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { TbCheck } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Logo from "../components/Logo";
import Meta from "../components/Meta";
import useUser from "../hooks/user.hook";
@@ -89,12 +90,17 @@ export default function Home() {
<div className={classes.inner}>
<div className={classes.content}>
<Title className={classes.title}>
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
file sharing platform.
<FormattedMessage
id="home.title"
values={{
h: (chunks) => (
<span className={classes.highlight}>{chunks} </span>
),
}}
/>
</Title>
<Text color="dimmed" mt="md">
Do you really want to give your personal files in the hand of
third parties like WeTransfer?
<FormattedMessage id="home.description" />
</Text>
<List
@@ -109,19 +115,26 @@ export default function Home() {
>
<List.Item>
<div>
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
<b>
<FormattedMessage id="home.bullet.a.name" />
</b>{" "}
- <FormattedMessage id="home.bullet.a.description" />
</div>
</List.Item>
<List.Item>
<div>
<b>Privacy</b> - Your files are your files and should never
get into the hands of third parties.
<b>
<FormattedMessage id="home.bullet.b.name" />
</b>{" "}
- <FormattedMessage id="home.bullet.b.description" />
</div>
</List.Item>
<List.Item>
<div>
<b>No annoying file size limit</b> - Upload as big files as
you want. Only your hard drive will be your limit.
<b>
<FormattedMessage id="home.bullet.c.name" />
</b>{" "}
- <FormattedMessage id="home.bullet.c.description" />
</div>
</List.Item>
</List>
@@ -134,7 +147,7 @@ export default function Home() {
size="md"
className={classes.control}
>
Get started
<FormattedMessage id="home.button.start" />
</Button>
<Button
component={Link}
@@ -145,7 +158,7 @@ export default function Home() {
size="md"
className={classes.control}
>
Source code
<FormattedMessage id="home.button.source" />
</Button>
</Group>
</div>

View File

@@ -7,6 +7,7 @@ import DownloadAllButton from "../../../components/share/DownloadAllButton";
import FileList from "../../../components/share/FileList";
import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
import showErrorModal from "../../../components/share/showErrorModal";
import useTranslate from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { Share as ShareType } from "../../../types/share.type";
import toast from "../../../utils/toast.util";
@@ -20,6 +21,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
const Share = ({ shareId }: { shareId: string }) => {
const modals = useModals();
const [share, setShare] = useState<ShareType>();
const t = useTranslate();
const getShareToken = async (password?: string) => {
await shareService
@@ -33,8 +35,8 @@ const Share = ({ shareId }: { shareId: string }) => {
if (error == "share_max_views_exceeded") {
showErrorModal(
modals,
"Visitor limit exceeded",
"The visitor limit from this share has been exceeded."
t("share.error.visitor-limit-exceeded.title"),
t("share.error.visitor-limit-exceeded.description")
);
} else {
toast.axiosError(e);
@@ -52,12 +54,16 @@ const Share = ({ shareId }: { shareId: string }) => {
const { error } = e.response.data;
if (e.response.status == 404) {
if (error == "share_removed") {
showErrorModal(modals, "Share removed", e.response.data.message);
showErrorModal(
modals,
t("share.error.removed.title"),
e.response.data.message
);
} else {
showErrorModal(
modals,
"Not found",
"This share can't be found. Please check your link."
t("share.error.not-found.title"),
t("share.error.not-found.description")
);
}
} else if (error == "share_password_required") {
@@ -65,7 +71,7 @@ const Share = ({ shareId }: { shareId: string }) => {
} else if (error == "share_token_required") {
getShareToken();
} else {
showErrorModal(modals, "Error", "An unknown error occurred.");
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
}
});
};
@@ -77,8 +83,8 @@ const Share = ({ shareId }: { shareId: string }) => {
return (
<>
<Meta
title={`Share ${shareId}`}
description="Look what I've shared with you."
title={t("share.title", { shareId })}
description={t("share.description")}
/>
<Group position="apart" mb="lg">

View File

@@ -4,12 +4,14 @@ import { cleanNotifications } from "@mantine/notifications";
import { AxiosError } from "axios";
import pLimit from "p-limit";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import Dropzone from "../../components/upload/Dropzone";
import FileList from "../../components/upload/FileList";
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service";
import { FileUpload } from "../../types/File.type";
@@ -29,6 +31,7 @@ const Upload = ({
isReverseShare: boolean;
}) => {
const modals = useModals();
const t = useTranslate();
const { user } = useUser();
const config = useConfig();
@@ -126,7 +129,7 @@ const Upload = ({
if (fileErrorCount > 0) {
if (!errorToastShown) {
toast.error(
`${fileErrorCount} file(s) failed to upload. Trying again.`,
t("upload.notify.count-failed", { count: fileErrorCount }),
{
withCloseButton: false,
autoClose: false,
@@ -152,15 +155,13 @@ const Upload = ({
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
setFiles([]);
})
.catch(() =>
toast.error("An error occurred while finishing your share.")
);
.catch(() => toast.error(t("upload.notify.generic-error")));
}
}, [files]);
return (
<>
<Meta title="Upload" />
<Meta title={t("upload.title")} />
<Group position="right" mb={20}>
<Button
loading={isUploading}
@@ -183,7 +184,7 @@ const Upload = ({
);
}}
>
Share
<FormattedMessage id="common.button.share" />
</Button>
</Group>
<Dropzone

View File

@@ -1,7 +1,10 @@
import moment from "moment";
export const getExpirationPreview = (
name: string,
messages: {
neverExpires: string;
expiresOn: string;
},
form: {
values: {
never_expires?: boolean;
@@ -13,7 +16,7 @@ export const getExpirationPreview = (
const value = form.values.never_expires
? "never"
: form.values.expiration_num + form.values.expiration_unit;
if (value === "never") return `This ${name} will never expire.`;
if (value === "never") return messages.neverExpires;
const expirationDate = moment()
.add(
@@ -22,5 +25,8 @@ export const getExpirationPreview = (
)
.toDate();
return `This ${name} will expire on ${moment(expirationDate).format("LLL")}`;
return messages.expiresOn.replace(
"{expiration}",
moment(expirationDate).format("LLL")
);
};

View File

@@ -0,0 +1,42 @@
import { setCookie } from "cookies-next";
import { LOCALES } from "../i18n/locales";
const getLocaleByCode = (code: string) => {
return Object.values(LOCALES).find((l) => l.code === code) ?? LOCALES.ENGLISH;
};
// Parse the Accept-Language header and return the first supported language
const getLanguageFromAcceptHeader = (acceptLanguage?: string) => {
if (!acceptLanguage) return "en";
const languages = acceptLanguage.split(",").map((l) => l.split(";")[0]);
const supportedLanguages = Object.values(LOCALES).map((l) => l.code);
for (const language of languages) {
// Try to match the full language code first, then the language code without the region
if (supportedLanguages.includes(language)) {
return language;
} else if (supportedLanguages.includes(language.split("-")[0])) {
return language.split("-")[0];
}
}
return "en";
};
const isLanguageSupported = (code: string) => {
return Object.values(LOCALES).some((l) => l.code === code);
};
const setLanguageCookie = (code: string) => {
setCookie("language", code, {
sameSite: "lax",
expires: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
});
};
export default {
getLocaleByCode,
getLanguageFromAcceptHeader,
isLanguageSupported,
setLanguageCookie,
};

View File

@@ -1,6 +1,5 @@
export const configVariableToFriendlyName = (variable: string) => {
const splitted = variable.split(/(?=[A-Z])/).join(" ");
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
export const camelToKebab = (camelCaseString: string) => {
return camelCaseString.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
};
export const capitalizeFirstLetter = (string: string) => {

View File

@@ -3,6 +3,10 @@ const defaultPreferences = [
key: "colorScheme",
value: "system",
},
{
key: "locale",
value: "system",
},
];
const get = (key: string) => {
@@ -23,8 +27,9 @@ const set = (key: string, value: string) => {
localStorage.setItem("preferences", JSON.stringify(preferences));
}
};
const usePreferences = () => {
return { get, set };
const userPreferences = {
get,
set,
};
export default usePreferences;
export default userPreferences;