web: localize

This commit is contained in:
Ammar Ahmed
2024-09-05 15:49:50 +05:00
committed by Abdullah Atta
parent d28d34fad1
commit 1fff0f6a1c
85 changed files with 3337 additions and 7790 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -8108,7 +8108,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -8128,7 +8128,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -8159,7 +8159,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -13006,7 +13006,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"devOptional": true,
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -23518,6 +23518,7 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/jsesc": {
@@ -23568,6 +23569,7 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -24758,6 +24760,7 @@
}
},
"../../packages/intl": {
"name": "@notesnook/intl",
"version": "1.0.0",
"devDependencies": {
"@babel/core": "^7.24.9",
@@ -30129,21 +30132,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
"version": "7.25.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz",
"integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
@@ -30771,22 +30759,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz",
"integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.22.5",
"dev": true,
@@ -34820,13 +34792,6 @@
"version": "20.3.1",
"license": "MIT"
},
"node_modules/@types/node-forge": {
"version": "1.3.11",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node-forge": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
@@ -35448,15 +35413,6 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"dev": true,
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"dev": true,
@@ -37445,57 +37401,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-module-lexer": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-iterator-helpers": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz",
"integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.3",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.0.3",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"globalthis": "^1.0.3",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.0.3",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.7",
"iterator.prototype": "^1.1.2",
"safe-array-concat": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"dev": true,
@@ -39046,16 +38957,6 @@
"entities": "^4.4.0"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "4.5.0",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -39372,55 +39273,6 @@
"node": ">=6"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-interactive": {
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-number-object": {
"version": "1.0.7",
"dev": true,
"license": "MIT",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -39440,18 +39292,6 @@
"node": ">=8"
}
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
"integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -40820,14 +40660,6 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
"integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
"license": "MIT",
@@ -41244,25 +41076,6 @@
"node": ">=4"
}
},
"node_modules/listr-verbose-renderer/node_modules/mimic-fn": {
"version": "1.2.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/listr-verbose-renderer/node_modules/onetime": {
"version": "2.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-fn": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/listr-verbose-renderer/node_modules/restore-cursor": {
"version": "2.0.0",
"dev": true,
@@ -41359,16 +41172,6 @@
"version": "4.0.8",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"license": "MIT"
@@ -44780,6 +44583,7 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"

View File

@@ -981,7 +981,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.17",
"version": "3.0.18",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -992,6 +992,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@emotion/react": "11.11.1",
"@hazae41/foras": "^2.1.4",
"@henrygd/queue": "^1.0.6",
"@lingui/core": "4.11.2",
"@lingui/react": "4.11.2",
"@mdi/js": "^7.2.96",
@@ -1032,7 +1033,7 @@
"event-source-polyfill": "^1.0.25",
"fflate": "^0.8.0",
"file-saver": "^2.0.5",
"framer-motion": "^10.16.8",
"framer-motion": "^11.5.6",
"hash-wasm": "^4.9.0",
"hotkeys-js": "^3.8.3",
"katex": "0.16.2",

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ import { BuyDialog } from "./dialogs/buy-dialog";
import { FeatureDialog } from "./dialogs/feature-dialog";
import { AnnouncementDialog } from "./dialogs/announcement-dialog";
import { logger } from "./utils/logger";
import { strings } from "@notesnook/intl";
type AppEffectsProps = {
setShow: (show: boolean) => void;
@@ -89,7 +90,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
break;
}
showToast("error", sentence, [
{ text: "Upgrade now", onClick: () => BuyDialog.show({}) }
{ text: strings.upgradeNow(), onClick: () => BuyDialog.show({}) }
]);
return { type, result: false };
}

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { lazify } from "../utils/lazify";
import { logger } from "../utils/logger";
import { showToast } from "../utils/toast";
@@ -62,7 +63,9 @@ export async function saveAttachment(hash: string) {
console.error(e);
showToast(
"error",
`Failed to download attachment: ${hash} (error: ${(e as Error).message})`
`${strings.attachmentsDownloadFailed(1)}: ${hash} (error: ${
(e as Error).message
})`
);
}
}
@@ -106,7 +109,9 @@ export async function downloadAttachment<
console.error(e);
showToast(
"error",
`Failed to download attachment: ${hash} (error: ${(e as Error).message})`
`${strings.attachmentsDownloadFailed(1)}: ${hash} (error: ${
(e as Error).message
})`
);
}
}

View File

@@ -45,6 +45,7 @@ import { Cipher, SerializedKey } from "@notesnook/crypto";
import { ChunkedStream } from "../utils/streams/chunked-stream";
import { isFeatureSupported } from "../utils/feature-check";
import { NNCrypto } from "../interfaces/nncrypto";
import { strings } from "@notesnook/intl";
export const CREATE_BUTTON_MAP = {
notes: {
@@ -94,7 +95,7 @@ export async function createBackup(
const verified =
rescueMode || encryptBackups || noVerify || (await verifyAccount());
if (!verified) {
showToast("error", "Could not create a backup: user verification failed.");
showToast("error", `${strings.backupFailed()}: ${strings.verifyFailed()}.`);
return false;
}
@@ -168,11 +169,11 @@ export async function createBackup(
if (error) {
showToast(
"error",
`Could not create a backup of your data: ${(error as Error).message}`
`${strings.backupFailed()}: ${(error as Error).message}`
);
console.error(error);
} else {
showToast("success", `Backup saved at ${filePath}.`);
showToast("success", `${strings.backupSavedAt(filePath)}`);
return true;
}
return false;
@@ -316,7 +317,7 @@ export async function restoreBackupFile(backupFile: File) {
});
if (error) {
console.error(error);
showToast("error", `Failed to restore backup: ${error.message}`);
showToast("error", `${strings.restoreFailed()}: ${error.message}`);
}
}
}
@@ -408,12 +409,12 @@ async function restore(
) {
try {
await db.backup?.import(backup, { password, encryptionKey });
showToast("success", "Backup restored!");
showToast("success", strings.backupRestored());
} catch (e) {
logger.error(e as Error, "Could not restore the backup");
showToast(
"error",
`Could not restore the backup: ${(e as Error).message || e}`
`${strings.backupFailed()}: ${(e as Error).message || e}`
);
}
}

View File

@@ -29,6 +29,7 @@ import Vault from "./vault";
import { TaskManager } from "./task-manager";
import { pluralize } from "@notesnook/common";
import { ConfirmDialog, showMultiDeleteConfirmation } from "../dialogs/confirm";
import { strings } from "@notesnook/intl";
async function moveNotesToTrash(ids: string[], confirm = true) {
if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return;
@@ -48,16 +49,16 @@ async function moveNotesToTrash(ids: string[], confirm = true) {
await TaskManager.startTask({
type: "status",
id: "deleteNotes",
title: "Deleting notes",
title: strings.deletingItems("note", items.length),
action: async (report) => {
report({
text: `Deleting ${pluralize(items.length, "note")}...`
text: `${strings.deleting()} ${strings.notes(items.length)}...`
});
await noteStore.delete(...items);
}
});
showToast("success", `${pluralize(items.length, "note")} moved to trash`);
showToast("success", strings.movedToTrash("note", items.length));
}
async function moveNotebooksToTrash(ids: string[]) {
@@ -71,16 +72,19 @@ async function moveNotebooksToTrash(ids: string[]) {
await TaskManager.startTask({
type: "status",
id: "deleteNotebooks",
title: "Deleting notebooks",
title: strings.deletingItems("notebook", ids.length),
action: async (report) => {
report({
text: `Deleting ${pluralize(ids.length, "notebook")}...`
text: `${strings.deleting()} ${strings.itemsPlural(
"notebook",
ids.length
)}...`
});
await notebookStore.delete(...ids);
}
});
showToast("success", `${pluralize(ids.length, "notebook")} moved to trash`);
showToast("success", strings.movedToTrash("notebook", ids.length));
}
async function deleteAttachments(ids: string[]) {
@@ -98,7 +102,7 @@ async function deleteAttachments(ids: string[]) {
await TaskManager.startTask({
type: "status",
id: "deleteAttachments",
title: "Deleting attachments",
title: strings.deletingItems("attachment", ids.length),
action: async (report) => {
for (let i = 0; i < ids.length; ++i) {
const id = ids[i];
@@ -106,7 +110,10 @@ async function deleteAttachments(ids: string[]) {
if (!attachment) continue;
report({
text: `Deleting ${pluralize(ids.length, "attachment")}...`,
text: `${strings.deleting()} ${strings.itemsPlural(
"attachment",
ids.length
)}...`,
current: i,
total: ids.length
});
@@ -114,7 +121,7 @@ async function deleteAttachments(ids: string[]) {
}
}
});
showToast("success", `${pluralize(ids.length, "attachment")} deleted`);
showToast("success", strings.deleted("attachment", ids.length));
}
async function moveRemindersToTrash(ids: string[]) {
@@ -126,16 +133,19 @@ async function moveRemindersToTrash(ids: string[]) {
await TaskManager.startTask({
type: "status",
id: "deleteReminders",
title: "Deleting reminders",
title: strings.deletingItems("reminder", ids.length),
action: async (report) => {
report({
text: `Deleting ${pluralize(ids.length, "reminder")}...`
text: `${strings.deleting()} ${strings.itemsPlural(
"reminder",
ids.length
)}...`
});
await reminderStore.delete(...ids);
}
});
showToast("success", `${pluralize(ids.length, "reminder")} deleted.`);
showToast("success", strings.deleted("reminder", ids.length));
}
async function deleteTags(ids: string[]) {

View File

@@ -29,6 +29,7 @@ import { showToast } from "../utils/toast";
import { TaskScheduler } from "../utils/task-scheduler";
import { BuyDialog } from "../dialogs/buy-dialog";
import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog";
import { strings } from "@notesnook/intl";
export type NoticeType = "autoBackupsOff" | "login" | "email" | "recoverykey";
@@ -205,10 +206,10 @@ async function saveBackup(mode: "full" | "partial" = "partial") {
if (openedToast !== null) return;
openedToast = showToast(
"success",
"Your backup is ready for download.",
strings.backupReadyToDownload(),
[
{
text: "Later",
text: strings.later(),
onClick: async () => {
await db.backup.updateBackupTime();
openedToast?.hide();
@@ -217,7 +218,7 @@ async function saveBackup(mode: "full" | "partial" = "partial") {
type: "paragraph"
},
{
text: "Download",
text: strings.network.download(),
onClick: async () => {
await createBackup({ mode });
openedToast?.hide();

View File

@@ -21,10 +21,10 @@ import { db } from "./db";
import { store as notestore } from "../stores/note-store";
import { store as nbstore } from "../stores/notebook-store";
import { showToast } from "../utils/toast";
import { strings } from "@notesnook/intl";
async function showUnpinnedToast(itemId, itemType) {
const noun = itemType === "note" ? "Note" : "Notebook";
const messageText = `${noun} unpinned successfully!`;
const messageText = strings.action(itemType, 1, "unpinned");
const undoAction = async () => {
toast.hide();
if (itemType === "note") await notestore.pin(itemId);
@@ -35,8 +35,7 @@ async function showUnpinnedToast(itemId, itemType) {
}
function showItemDeletedToast(item) {
const noun = item.type === "note" ? "Note" : "Notebook";
const messageText = `${noun} deleted successfully!`;
const messageText = strings.action(item.type, 1, "deleted");
const undoAction = async () => {
toast.hide();
let trashItem = db.trash.all.find((i) => i.id === item.id);
@@ -57,7 +56,7 @@ async function showUndoableToast(message, onAction, onPermanentAction, onUndo) {
toast.hide();
onUndo();
};
let actions = [{ text: "Undo", onClick: undoAction }];
let actions = [{ text: strings.undo(), onClick: undoAction }];
var toast = showToast("success", message, actions);
}

View File

@@ -21,19 +21,20 @@ import { db } from "./db";
import { showPasswordDialog } from "../dialogs/password-dialog";
import { showToast } from "../utils/toast";
import { VAULT_ERRORS } from "@notesnook/core";
import { strings } from "@notesnook/intl";
class Vault {
static async createVault() {
if (await db.vault.exists()) return false;
return showPasswordDialog({
title: "Create your vault",
subtitle: "A vault stores your notes in a password-encrypted storage.",
title: strings.createVault(),
subtitle: strings.createVaultDesc(),
inputs: {
password: { label: "Password", autoComplete: "new-password" }
password: { label: strings.password(), autoComplete: "new-password" }
},
validate: async ({ password }) => {
await db.vault.create(password);
showToast("success", "Vault created.");
showToast("success", strings.vaultCreated());
return true;
}
});
@@ -43,11 +44,13 @@ class Vault {
if (!(await db.vault.exists())) return false;
return showPasswordDialog({
title: "Clear your vault",
subtitle:
"Enter vault password to unlock and remove all notes from the vault.",
title: strings.clearVault(),
subtitle: strings.clearVaultDesc(),
inputs: {
password: { label: "Password", autoComplete: "current-password" }
password: {
label: strings.password(),
autoComplete: "current-password"
}
},
validate: async ({ password }) => {
await db.vault.clear(password);
@@ -59,14 +62,17 @@ class Vault {
static async deleteVault() {
if (!(await db.vault.exists())) return false;
const result = await showPasswordDialog({
title: "Delete your vault",
subtitle: "Enter your account password to delete your vault.",
title: strings.deleteVault(),
subtitle: strings.deleteVaultDesc(),
inputs: {
password: { label: "Password", autoComplete: "current-password" }
password: {
label: strings.password(),
autoComplete: "current-password"
}
},
checks: {
deleteAllLockedNotes: {
text: "Delete all locked notes?",
text: strings.deleteAllNotes(),
default: false
}
},
@@ -83,10 +89,13 @@ class Vault {
static unlockVault() {
return showPasswordDialog({
title: "Unlock vault",
subtitle: "Please enter your vault password to continue.",
title: strings.unlockVault(),
subtitle: strings.unlockVaultDesc(),
inputs: {
password: { label: "Password", autoComplete: "current-password" }
password: {
label: strings.password(),
autoComplete: "current-password"
}
},
validate: ({ password }) => {
return db.vault.unlock(password).catch(() => false);
@@ -96,18 +105,21 @@ class Vault {
static changeVaultPassword() {
return showPasswordDialog({
title: "Change vault password",
subtitle: "All locked notes will be re-encrypted with the new password.",
title: strings.changeVaultPassword(),
subtitle: strings.changeVaultPasswordDesc(),
inputs: {
oldPassword: {
label: "Old password",
label: strings.oldPassword(),
autoComplete: "current-password"
},
newPassword: { label: "New password", autoComplete: "new-password" }
newPassword: {
label: strings.newPassword(),
autoComplete: "new-password"
}
},
validate: async ({ oldPassword, newPassword }) => {
await db.vault.changePassword(oldPassword, newPassword);
showToast("success", "Vault password changed.");
showToast("success", strings.passwordChangedSuccessfully());
return true;
}
});
@@ -115,10 +127,13 @@ class Vault {
static unlockNote(id: string) {
return showPasswordDialog({
title: "Unlock note",
subtitle: "Your note will be unencrypted and removed from the vault.",
title: strings.unlockNote(),
subtitle: strings.unlockNoteDesc(),
inputs: {
password: { label: "Password", autoComplete: "current-password" }
password: {
label: strings.password(),
autoComplete: "current-password"
}
},
validate: async ({ password }) => {
return db.vault
@@ -156,10 +171,13 @@ class Vault {
static askPassword(action: (password: string) => Promise<boolean>) {
return showPasswordDialog({
title: "Unlock vault",
subtitle: "Please enter your vault password to continue.",
title: strings.unlockVault(),
subtitle: strings.unlockVaultDesc(),
inputs: {
password: { label: "Password", autoComplete: "current-password" }
password: {
label: strings.password(),
autoComplete: "current-password"
}
},
validate: async ({ password }) => {
return action(password);

View File

@@ -24,6 +24,7 @@ import { useStore as useAnnouncementStore } from "../../stores/announcement-stor
import Notice from "../notice";
import { alpha } from "@theme-ui/color";
import { strings } from "@notesnook/intl";
function Announcements() {
const announcements = useAnnouncementStore(
@@ -56,7 +57,7 @@ function Announcements() {
cursor: "pointer",
alignSelf: "end"
}}
title="Dismiss announcement"
title={strings.dismissAnnouncement()}
onClick={() => {
dismiss(announcement.id);
}}

View File

@@ -58,6 +58,7 @@ import { useEditorStore } from "../../stores/editor-store";
import { PromptDialog } from "../../dialogs/prompt";
import { DialogManager } from "../../common/dialog-manager";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { strings } from "@notesnook/intl";
const FILE_ICONS: Record<string, Icon> = {
"image/": FileImage,
@@ -203,13 +204,13 @@ export function Attachment({
sx={{ flexShrink: 0 }}
color={"accent"}
size={16}
title={"Uploaded"}
title={strings.uploaded()}
/>
) : (
<Checkmark
sx={{ flexShrink: 0 }}
size={16}
title={"Waiting for upload"}
title={strings.waitingForUpload()}
/>
)}
</Text>

View File

@@ -26,6 +26,7 @@ import Jason from "../../assets/testimonials/jason.jpg";
import Cameron from "../../assets/testimonials/cameron.jpg";
import { hosts } from "@notesnook/core";
import { SettingsDialog } from "../../dialogs/settings";
import { strings } from "@notesnook/intl";
const testimonials = [
{
@@ -58,20 +59,14 @@ const testimonials = [
}
];
const titles = [
"Write with freedom.",
"Privacy comes first.",
"Take notes privately.",
"Encrypted, private, secure.",
"❤️ = 🔒 + 🗒️"
];
function randomTestimonial() {
return testimonials[getRandom(0, testimonials.length - 1)];
}
function randomTitle() {
return titles[getRandom(0, titles.length - 1)];
return strings.webAuthTitles[
getRandom(0, strings.webAuthTitles.length - 1)
]();
}
function AuthContainer(props) {

View File

@@ -17,13 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Flex, Text, Button, ButtonProps } from "@theme-ui/components";
import { Loading, Close } from "../icons";
import ReactModal from "react-modal";
import { FlexScrollContainer } from "../scroll-container";
import { Button, ButtonProps, Flex, Text } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
import React from "react";
import ReactModal from "react-modal";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { Close, Loading } from "../icons";
import { FlexScrollContainer } from "../scroll-container";
import { ScopedThemeProvider } from "../theme-provider";
ReactModal.setAppElement("#root");

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Button, Text, FlexProps } from "@theme-ui/components";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
type ContentToggle = {
isSelected: boolean;
@@ -54,7 +55,7 @@ function ContentToggle(props: ContentToggle) {
p={1}
px={2}
>
Save copy
{strings.saveACopy()}
</Button>
)}
<Button
@@ -69,7 +70,11 @@ function ContentToggle(props: ContentToggle) {
p={1}
px={2}
>
{isSelected ? "Undo" : isOtherSelected ? "Discard" : "Keep"}
{isSelected
? strings.undo()
: isOtherSelected
? strings.discard()
: strings.keep()}
</Button>
</Flex>
)}

View File

@@ -33,6 +33,7 @@ import { ContentItem, Note } from "@notesnook/core";
import { UnlockView } from "../unlock";
import { getFormattedDate } from "@notesnook/common";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
type DiffViewerProps = { session: ConflictedEditorSession };
function DiffViewer(props: DiffViewerProps) {
@@ -129,7 +130,7 @@ function DiffViewer(props: DiffViewerProps) {
}}
>
<Restore size={18} />
<Text ml={1}>Restore this version</Text>
<Text ml={1}>{strings.restoreThisVersion()}</Text>
</Button>
<Button
variant="secondary"
@@ -152,7 +153,7 @@ function DiffViewer(props: DiffViewerProps) {
}}
>
<Copy size={18} />
<Text ml={1}>Save a copy</Text>
<Text ml={1}>{strings.saveACopy()}</Text>
</Button>
</>
) : null}
@@ -178,8 +179,8 @@ function DiffViewer(props: DiffViewerProps) {
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle="Please enter the password to view this version"
buttonTitle="Unlock"
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
@@ -196,7 +197,9 @@ function DiffViewer(props: DiffViewerProps) {
<>
<ContentToggle
label={
session.type === "diff" ? "Older version" : "Current note"
session.type === "diff"
? strings.olderVersion()
: strings.currentNote()
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
@@ -255,8 +258,8 @@ function DiffViewer(props: DiffViewerProps) {
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle="Please enter the password to view this version"
buttonTitle="Unlock"
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
@@ -276,8 +279,8 @@ function DiffViewer(props: DiffViewerProps) {
resolveConflict={onResolveContent}
label={
session.type === "diff"
? "Current version"
: "Incoming note"
? strings.currentNote()
: strings.incomingNote()
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}

View File

@@ -23,6 +23,7 @@ import { Loading, Saved, NotSaved } from "../icons";
import { useNoteStatistics } from "./manager";
import { getFormattedDate } from "@notesnook/common";
import { MAX_AUTO_SAVEABLE_WORDS } from "./types";
import { strings } from "@notesnook/intl";
const SAVE_STATE_ICON_MAP = {
"-1": NotSaved,
@@ -48,7 +49,7 @@ function EditorFooter() {
variant="subBody"
sx={{ color: "paragraph" }}
>
Auto save: off
{strings.autoSaveOff()}
</Text>
) : null}
<Text
@@ -57,8 +58,8 @@ function EditorFooter() {
variant="subBody"
sx={{ color: "paragraph" }}
>
{words.total + " words"}
{words.selected ? ` (${words.selected} selected)` : ""}
{words.total + " " + strings.words()}
{words.selected ? ` (${words.selected} ${strings.selected()})` : ""}
</Text>
{dateEdited > 0 ? (
<Text

View File

@@ -30,6 +30,7 @@ import { useMenuTrigger } from "../../hooks/use-menu";
import { MenuItem } from "@notesnook/ui";
import { navigate } from "../../navigation";
import { Tag } from "@notesnook/core";
import { strings } from "@notesnook/intl";
type HeaderProps = { readonly: boolean; id: string };
function Header(props: HeaderProps) {
@@ -194,7 +195,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
fontSize: "body",
alignSelf: "flex-start"
}}
placeholder="Add a tag..."
placeholder={strings.addATag()}
data-test-id="editor-tag-input"
onFocus={async () => {
const text = getInputValue();

View File

@@ -70,6 +70,7 @@ import { EditorActionBar } from "./action-bar";
import { logger } from "../../utils/logger";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
import { strings } from "@notesnook/intl";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -90,12 +91,7 @@ async function saveContent(
}),
new Promise((_, reject) =>
setTimeout(
() =>
reject(
new Error(
"Saving this note is taking too long. Copy your changes and restart the app to prevent data loss. If the problem persists, please report it to us at support@streetwriters.co."
)
),
() => reject(new Error(strings.savingNoteTakingTooLong())),
30 * 1000
)
)
@@ -105,7 +101,7 @@ async function saveContent(
(e as Error).message,
[
{
text: "Dismiss",
text: strings.dismiss(),
onClick: () => hide()
}
],
@@ -382,7 +378,9 @@ function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
flexDirection: "column"
}}
>
<Text variant="title">Downloading attachment ({progress}%)</Text>
<Text variant="title">
{strings.downloadingAttachments()} ({progress}%)
</Text>
<Progress
value={progress}
max={100}
@@ -397,7 +395,7 @@ function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
if (id) db.fs().cancel(id).catch(console.error);
}}
>
Cancel
{strings.cancel()}
</Button>
</Flex>
);
@@ -485,7 +483,7 @@ export function Editor(props: EditorProps) {
const dataurl = await downloadAttachment(hash, "base64", id);
if (!dataurl)
return showToast("error", "This image cannot be previewed.");
return showToast("error", strings.imagePreviewFailed());
ReactDOM.render(
<ScopedThemeProvider>
@@ -684,7 +682,7 @@ function DropZone(props: DropZoneProps) {
>
<Attachment size={72} />
<Text variant={"heading"} sx={{ color: "icon", mt: 2 }}>
Drop your files here to attach
{strings.dropFilesToAttach()}
</Text>
</Flex>
</Box>
@@ -815,8 +813,8 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
ref={root}
>
<UnlockView
buttonTitle="Open note"
subtitle="Please enter the password to unlock this note."
buttonTitle={strings.openNote()}
subtitle={strings.enterPasswordToUnlockNote()}
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.id, password);

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Text } from "@theme-ui/components";
import { Loading } from "../icons";
import { strings } from "@notesnook/intl";
function EditorLoading({ text }: { text?: string }) {
return (
@@ -32,7 +33,7 @@ function EditorLoading({ text }: { text?: string }) {
>
<Loading color="accent" sx={{ mt: 2 }} />
<Text variant="body" mt={2} sx={{ textAlign: "center" }}>
{text || "Loading editor. Please wait..."}
{text || `${strings.loadingEditor()}. ${strings.pleaseWait()}...`}
</Text>
</Flex>
);

View File

@@ -47,6 +47,7 @@ import { scrollIntoViewById } from "@notesnook/editor";
import { Button, Flex, Text } from "@theme-ui/components";
import { useEditorManager } from "./manager";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { strings } from "@notesnook/intl";
type TableOfContentsProps = {
sessionId: string;
@@ -122,7 +123,7 @@ function TableOfContents(props: TableOfContentsProps) {
>
<ScrollContainer>
<Section
title="Table of contents"
title={strings.toc()}
button={
<ArrowLeft
data-test-id="toc-close"
@@ -140,7 +141,7 @@ function TableOfContents(props: TableOfContentsProps) {
pl: 1
}}
>
No headings found.
{strings.noHeadingsFound()}.
</Text>
) : (
tableOfContents.map((t) => (

View File

@@ -28,6 +28,7 @@ import { getFontById } from "@notesnook/editor";
import { replaceDateTime } from "@notesnook/editor";
import { useStore as useSettingsStore } from "../../stores/setting-store";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { strings } from "@notesnook/intl";
type TitleBoxProps = {
id: string;
@@ -118,7 +119,7 @@ function TitleBox(props: TitleBoxProps) {
id="editor-title"
data-test-id="editor-title"
className="editorTitle"
placeholder={"Note title"}
placeholder={strings.noteTitle()}
readOnly={readonly}
dir="auto"
sx={{

View File

@@ -28,6 +28,7 @@ import {
} from "react-error-boundary";
import { useKeyStore } from "../../interfaces/key-store";
import { isFeatureSupported } from "../../utils/feature-check";
import { strings } from "@notesnook/intl";
export function GlobalErrorHandler(props: PropsWithChildren) {
const { showBoundary } = useErrorBoundary();
@@ -111,17 +112,17 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
variant="heading"
sx={{ borderBottom: "1px solid var(--border)", pb: 1 }}
>
Something went wrong
{strings.somethingWentWrong()}
</Text>
<ErrorText error={error} />
{help ? (
<>
<Text variant="subtitle" sx={{ mt: 2 }}>
What went wrong?
{strings.whatWentWrong()}
</Text>
<Text variant="body">{help.explanation}</Text>
<Text variant="subtitle" sx={{ mt: 1 }}>
How to fix it?
{strings.howToFix()}
</Text>
<Text variant="body">{help.action}</Text>
</>
@@ -138,7 +139,7 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
})
}
>
Fix it
{strings.fixIt()}
</Button>
) : (
<Button
@@ -146,7 +147,7 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
sx={{ alignSelf: "start", px: 30, mt: 1 }}
onClick={() => window.location.reload()}
>
Reload app
{strings.reloadApp()}
</Button>
)}
<>
@@ -157,7 +158,7 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
navigator.clipboard.writeText(errorToString(error));
}}
>
Copy
{strings.copy()}
</Button>
<Button
variant="secondary"
@@ -179,7 +180,7 @@ ${getDeviceInfo()}`
window.open(mailto.toString(), "_blank");
}}
>
Contact support
{strings.contactSupport()}
</Button>
</>
</Flex>
@@ -201,9 +202,8 @@ function getErrorHelp(props: FallbackProps) {
errorText.includes("corrupted migrations:")
) {
return {
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
action:
"This error can only be fixed by wiping & reseting the database. Beware that this will wipe all your data inside the database with no way to recover it later on. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
explanation: strings.databaseCorruptExplain(),
action: strings.databaseCorruptFix(),
fix: async () => {
await resetDatabase();
resetErrorBoundary();
@@ -211,9 +211,8 @@ function getErrorHelp(props: FallbackProps) {
};
} else if (errorText.includes("Could not decrypt key.")) {
return {
explanation: `This error means the at rest encryption key could not be decrypted. This can be due to data corruption or implementation change.`,
action:
"This error can only be fixed by wiping & reseting the Key Store and the database. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
explanation: strings.decryptKeyErrorExplain(),
action: strings.decryptKeyErrorFix(),
fix: async () => {
await resetDatabase();
resetErrorBoundary();
@@ -221,9 +220,8 @@ function getErrorHelp(props: FallbackProps) {
};
} else if (errorText.includes("database disk image is malformed")) {
return {
explanation: `This error usually means the search index is corrupted.`,
action:
"This error can be fixed by rebuilding the search index. This action won't result in any kind of data loss.",
explanation: strings.searchIndexCorrupt(),
action: strings.searchIndexCorruptFix(),
fix: async () => {
const { db } = await import("../../common/db");
await db.lookup.rebuild();

View File

@@ -22,6 +22,7 @@ import Field from "../field";
import { Plus, Search } from "../icons";
import { Button, Text } from "@theme-ui/components";
import { VirtualizedList, VirtualizedListProps } from "../virtualized-list";
import { strings } from "@notesnook/intl";
type FilteredListProps<T> = {
placeholders: { filter: string; empty: string };
@@ -93,7 +94,7 @@ export function FilteredList<T>(props: FilteredListProps<T>) {
await _createNewItem(query);
}}
>
<Text variant={"body"}>{`Add "${query}"`}</Text>
<Text variant={"body"}>{`${strings.add()} "${query}"`}</Text>
<Plus size={16} color="accent" />
</Button>
) : (

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Text } from "@theme-ui/components";
import { ThemeUICSSObject } from "@theme-ui/core";
import { Close, Icon } from "../icons";
import { strings } from "@notesnook/intl";
type IconTagProps = {
text: string;
@@ -105,7 +106,7 @@ function IconTag(props: IconTagProps) {
{onDismiss && (
<Close
size={12}
title="Remove"
title={strings.remove()}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View File

@@ -23,15 +23,7 @@ import Config from "../../utils/config";
import { getDownloadLink, getPlatform } from "../../utils/platform";
import DropdownButton from "../dropdown-button";
import { BaseThemeProvider } from "../theme-provider";
const nativeFeatures = [
"Native high-performance encryption",
"Automatic backups",
"Pin notes in notifications drawer",
"Share & append to notes from anywhere",
"Quick note widgets",
"App lock"
];
import { strings } from "@notesnook/intl";
const platform = getPlatform();
const isMobile = platform === "Android" || platform === "iOS";
@@ -67,10 +59,8 @@ export default function InstallNotice({ onClose }) {
}}
p={2}
>
<Text variant={"title"}>Install Notesnook</Text>
<Text variant={"body"}>
For a more integrated user experience, try out Notesnook for {platform}.
</Text>
<Text variant={"title"}>{strings.installNotesnook()}</Text>
<Text variant={"body"}>{strings.installNotesnookDesc(platform)}.</Text>
{isMobile && (
<Box
sx={{
@@ -81,7 +71,7 @@ export default function InstallNotice({ onClose }) {
mt: 1
}}
>
{nativeFeatures.map((feature) => (
{strings.nativeFeatures().map((feature) => (
<Flex
key={feature}
p={1}
@@ -109,7 +99,7 @@ export default function InstallNotice({ onClose }) {
}}
sx={{ alignSelf: "start" }}
>
{`Don't show again`}
{strings.dontShowAgain()}
</Button>
</Flex>
</Flex>

View File

@@ -28,6 +28,7 @@ import useHashLocation, {
} from "../../hooks/use-hash-location";
import makeMatcher from "wouter/matcher";
import { Lock } from "../icons";
import { strings } from "@notesnook/intl";
const matcher = makeMatcher();
const EDITOR_MARGINS = Config.get("editor:margins", true);
@@ -89,9 +90,9 @@ export const EditorLoader = memo(function EditorLoader() {
}}
>
{isNoteLoading ? (
<TextScramble text="Loading" nextLetterSpeed={50} />
<TextScramble text={strings.loading()} nextLetterSpeed={50} />
) : (
"Note title"
strings.noteTitle()
)}
</Text>
<Skeleton

View File

@@ -72,6 +72,7 @@ import { Notebook, Tag } from "@notesnook/core";
import { handleDrop } from "../../common/drop-handler";
import { Menu } from "../../hooks/use-menu";
import { RenameColorDialog } from "../../dialogs/item-dialog";
import { strings } from "@notesnook/intl";
type Route = {
id: string;
@@ -471,7 +472,7 @@ function NavigationMenu(props: NavigationMenuProps) {
<NavigationItem
id="login"
isTablet={isTablet}
title="Login"
title={strings.login()}
icon={Login}
onClick={() => hardNavigate("/login")}
/>
@@ -480,7 +481,7 @@ function NavigationMenu(props: NavigationMenuProps) {
<NavigationItem
id="change-theme"
isTablet={isTablet}
title={theme === "dark" ? "Light mode" : "Dark mode"}
title={theme === "dark" ? strings.light() : strings.dark()}
icon={theme === "dark" ? LightMode : DarkMode}
onClick={() => {
setFollowSystemTheme(false);
@@ -505,7 +506,7 @@ function NavigationMenu(props: NavigationMenuProps) {
{isTablet ? null : (
<Button
variant={"icon"}
title="Toggle dark/light mode"
title={strings.toggleDarkLightMode()}
sx={{ borderLeft: "1px solid var(--separator)" }}
onClick={() => {
setFollowSystemTheme(false);

View File

@@ -93,6 +93,7 @@ import { CreateColorDialog } from "../../dialogs/create-color-dialog";
import { ConfirmDialog } from "../../dialogs/confirm";
import { MoveNoteDialog } from "../../dialogs/move-note-dialog";
import { AddReminderDialog } from "../../dialogs/add-reminder-dialog";
import { strings } from "@notesnook/intl";
type NoteProps = NoteResolvedData & {
item: NoteType;
@@ -218,7 +219,7 @@ function Note(props: NoteProps) {
) : null}
{attachments?.failed ? (
<Flex title={`Errors in ${attachments.failed} attachments.`}>
<Flex title={strings.errorsInAttachments(attachments.failed)}>
<AttachmentError size={13} color="var(--icon-error)" />
<Text ml={"2px"}>{attachments.failed}</Text>
</Flex>
@@ -242,10 +243,11 @@ function Note(props: NoteProps) {
data-test-id={`tag-item`}
key={tag.id}
variant="anchor"
title={`Go to #${tag.title}`}
title={`${strings.goTo()} #${tag.title}`}
onClick={(e) => {
e.stopPropagation();
if (!tag.id) return showToast("error", "Tag not found.");
if (!tag.id)
return showToast("error", strings.tagNotFound());
navigate(`/tags/${tag.id}`);
}}
sx={{

View File

@@ -45,6 +45,7 @@ import { handleDrop } from "../../common/drop-handler";
import { useDragHandler } from "../../hooks/use-drag-handler";
import { ConfirmDialog } from "../../dialogs/confirm";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { strings } from "@notesnook/intl";
type NotebookProps = {
item: NotebookType;
@@ -95,7 +96,7 @@ function Notebook(props: NotebookProps) {
<>
{compact ? (
<>
<Text variant="subBody">{pluralize(totalNotes, "note")}</Text>
<Text variant="subBody">{strings.notes(totalNotes)}</Text>
</>
) : (
<>
@@ -130,7 +131,7 @@ function Notebook(props: NotebookProps) {
</Text>
<Text sx={{ color: "inherit" }}>
{pluralize(totalNotes, "note")}
{strings.notes(totalNotes)}
</Text>
</Flex>
</>

View File

@@ -23,6 +23,7 @@ import { useStore as useAppStore } from "../../stores/app-store";
import { NoticesData } from "../../common/notices";
import { Dismiss } from "../icons";
import Config from "../../utils/config";
import { strings } from "@notesnook/intl";
function Notice() {
const notices = useAppStore((store) => store.notices);
@@ -86,7 +87,7 @@ function Notice() {
onClick={(e) => {
e.stopPropagation();
const dontShowAgain = window.confirm(
"Don't show again on this device?"
strings.dontShowAgainConfirm()
);
dismissNotices(notice);
if (dontShowAgain) {

View File

@@ -38,6 +38,7 @@ import Field from "../field";
import { LinkPlugin } from "./links-plugin";
import Config from "../../utils/config";
import { ErrorText } from "../error-text";
import { strings } from "@notesnook/intl";
export type PdfPreviewProps = {
fileUrl: string | Uint8Array;
@@ -118,7 +119,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{(props) => (
<ToolbarButton
icon={Search}
title="Search"
title={strings.search()}
onClick={props.onClick}
/>
)}
@@ -128,7 +129,7 @@ export function PdfPreview(props: PdfPreviewProps) {
<ToolbarButton
icon={ChevronUp}
disabled={props.isDisabled}
title="Go to previous page"
title={strings.goToPreviousPage()}
onClick={props.onClick}
/>
)}
@@ -146,7 +147,7 @@ export function PdfPreview(props: PdfPreviewProps) {
<ToolbarButton
icon={ChevronDown}
disabled={props.isDisabled}
title="Go to next page"
title={strings.goToNextPage()}
onClick={props.onClick}
/>
)}
@@ -165,7 +166,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{(props) => (
<ToolbarButton
icon={ZoomOut}
title="Zoom out"
title={strings.zoomOut()}
onClick={props.onClick}
/>
)}
@@ -181,7 +182,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{(props) => (
<ToolbarButton
icon={ZoomIn}
title="Zoom in"
title={strings.zoomIn()}
onClick={props.onClick}
/>
)}
@@ -200,7 +201,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{(props) => (
<ToolbarButton
icon={Download}
title="Download"
title={strings.network.download()}
onClick={props.onClick}
/>
)}
@@ -209,7 +210,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{(props) => (
<ToolbarButton
icon={Fullscreen}
title="Enter fullscreen"
title={strings.enterFullScreen()}
onClick={props.onClick}
/>
)}
@@ -218,7 +219,7 @@ export function PdfPreview(props: PdfPreviewProps) {
{onClose && (
<ToolbarButton
icon={Close}
title="Close"
title={strings.close()}
onClick={onClose}
/>
)}
@@ -269,7 +270,7 @@ export function PdfPreview(props: PdfPreviewProps) {
variant="heading"
sx={{ fontSize: 28, textAlign: "center" }}
>
Unlock document
{strings.pdfLocked()}
</Text>
</Flex>
<Text
@@ -278,13 +279,13 @@ export function PdfPreview(props: PdfPreviewProps) {
mb={4}
sx={{ textAlign: "center", color: "paragraph" }}
>
Please enter the password to unlock this document.
{strings.pdfLockedDesc()}.
</Text>
<Field
id="document-password"
autoFocus
sx={{ width: "95%", maxWidth: 400 }}
placeholder="Enter password"
placeholder={strings.enterPassword()}
type="password"
onKeyUp={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
@@ -293,7 +294,7 @@ export function PdfPreview(props: PdfPreviewProps) {
}}
/>
{props.passwordStatus === PasswordStatus.WrongPassword && (
<ErrorText error="Wrong password" />
<ErrorText error={strings.passwordIncorrect()} />
)}
<Button
mt={3}
@@ -302,7 +303,7 @@ export function PdfPreview(props: PdfPreviewProps) {
sx={{ borderRadius: 100, px: 30 }}
onClick={async () => {}}
>
Unlock
{strings.unlock()}
</Button>
</Flex>
)}

View File

@@ -22,6 +22,7 @@ import { TipContext, useTip } from "../../hooks/use-tip";
import { Info, Sync } from "../icons";
import { useStore as useAppStore } from "../../stores/app-store";
import { toTitleCase } from "@notesnook/common";
import { strings } from "@notesnook/intl";
type PlaceholderProps = { context: TipContext; text?: string };
function Placeholder(props: PlaceholderProps) {
@@ -53,13 +54,13 @@ function Placeholder(props: PlaceholderProps) {
>
<Sync color="accent" size={12} sx={{ mr: "small" }} />
<Text variant="subBody" sx={{ fontSize: 10 }} color="accent">
Syncing your {context}
{strings.syncingYour(context)}
</Text>
</Flex>
<Text variant="subBody" sx={{ fontSize: "body", mt: 1 }}>
{toTitleCase(syncStatus.type || "sync")}ing {syncStatus.progress}{" "}
items
{strings.items()}
</Text>
</Flex>
);
@@ -88,7 +89,7 @@ function Placeholder(props: PlaceholderProps) {
>
<Info color="accent" size={13} sx={{ mr: "small" }} />
<Text variant="subBody" sx={{ fontSize: 10 }} color="accent">
TIP
{strings.tip()}
</Text>
</Flex>
<Text variant="subBody" sx={{ fontSize: "body", mt: 1 }}>

View File

@@ -62,6 +62,7 @@ import {
import { VirtualizedTable } from "../virtualized-table";
import { TextSlice } from "@notesnook/core";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { strings } from "@notesnook/intl";
const tools = [
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
@@ -152,7 +153,7 @@ function EditorProperties(props: EditorPropertiesProps) {
>
<ScrollContainer>
<Section
title="Properties"
title={strings.properties()}
button={
<ArrowLeft
data-test-id="properties-close"
@@ -277,8 +278,8 @@ function InternalLinks({ noteId }: { noteId: string }) {
(result.value.length === 0 ? (
<Text variant="body" mx={1}>
{tabIndex === InternalLinksTabs.LINKED_NOTES
? "This note does not link to other notes."
: "This note is not referenced in other notes."}
? strings.notLinked()
: strings.notReferenced()}
</Text>
) : (
<VirtualizedList
@@ -597,7 +598,7 @@ function Notebooks({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title="Notebooks">
<Section title={strings.notebooks()}>
<VirtualizedList
mode="fixed"
estimatedSize={50}
@@ -626,7 +627,7 @@ function Reminders({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title="Reminders">
<Section title={strings.dataTypesPluralCamelCase.reminder()}>
<VirtualizedList
mode="fixed"
estimatedSize={54}
@@ -654,7 +655,7 @@ function Attachments({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title="Attachments">
<Section title={strings.dataTypesPluralCamelCase.attachment()}>
<VirtualizedTable
estimatedSize={30}
getItemKey={(index) => result.value.key(index)}
@@ -688,8 +689,8 @@ function SessionHistory({ noteId }: { noteId: string }) {
return (
<Section
title="Previous Sessions"
subtitle={"Your session history is local only."}
title={strings.noteHistory()}
subtitle={strings.noteHistoryNotice[0]()}
>
<VirtualizedList
mode="dynamic"

View File

@@ -32,6 +32,7 @@ import { useStore } from "../../stores/monograph-store";
import ReactModal from "react-modal";
import { DialogButton } from "../dialog";
import { Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
type PublishViewProps = {
note: Note;
@@ -98,10 +99,10 @@ function PublishView(props: PublishViewProps) {
justifyContent: "center"
}}
>
<Text>Please wait...</Text>
<Text>{strings.pleaseWait()}...</Text>
{processingStatus && (
<Text variant="subBody" mt={1}>
Downloading images ({processingStatus.current}/
{strings.downloadingImages()} ({processingStatus.current}/
{processingStatus.total})
</Text>
)}
@@ -114,7 +115,7 @@ function PublishView(props: PublishViewProps) {
variant="body"
sx={{ fontWeight: "bold", color: "paragraph" }}
>
Published at
{strings.publishedAt()}
</Text>
<Flex
sx={{
@@ -154,20 +155,18 @@ function PublishView(props: PublishViewProps) {
</Flex>
) : (
<Text variant="body" sx={{ color: "paragraph" }}>
This note will be published to a public URL.
{strings.monographDesc()}
</Text>
)}
<Toggle
title="Self destruct?"
onTip="Note will be automatically unpublished after first view."
offTip="Note will stay published until manually unpublished."
title={strings.monographSelfDestructHeading()}
tip={strings.monographSelfDestructDesc()}
isToggled={selfDestruct}
onToggled={() => setSelfDestruct((s) => !s)}
/>
<Toggle
title="Password protect?"
onTip="Protect published note with a password."
offTip="Do not protect published note with a password."
title={strings.monographPassHeading()}
tip={strings.monographPassDesc()}
isToggled={isPasswordProtected}
onToggled={() => setIsPasswordProtected((s) => !s)}
/>
@@ -177,7 +176,7 @@ function PublishView(props: PublishViewProps) {
autoFocus
type="password"
id="publishPassword"
placeholder="Enter password to encrypt this note"
placeholder={strings.enterPassword()}
required
sx={{ my: 1 }}
/>
@@ -204,19 +203,19 @@ function PublishView(props: PublishViewProps) {
password
});
setPublishId(publishId);
showToast("success", "Note published.");
showToast("success", strings.notePublished());
} catch (e) {
console.error(e);
showToast(
"error",
"Note could not be published: " + (e as Error).message
`${strings.failedToPublish()}: ${(e as Error).message}`
);
} finally {
setIsPublishing(false);
}
}}
loading={isPublishing}
text={publishId ? "Update" : "Publish"}
text={publishId ? strings.update() : strings.publish()}
/>
{publishId && (
<DialogButton

View File

@@ -25,6 +25,7 @@ import { useStore as useSearchStore } from "../../stores/search-store";
import useMobile from "../../hooks/use-mobile";
import { debounce, usePromise } from "@notesnook/common";
import Field from "../field";
import { strings } from "@notesnook/intl";
export type RouteContainerButtons = {
search?: {
@@ -82,7 +83,7 @@ function Header(props: RouteContainerProps) {
sx={{ m: 0, flex: 1 }}
styles={{ input: { p: "7px" } }}
defaultValue={query}
placeholder="Type your query here"
placeholder={strings.typeAKeyword()}
onChange={debounce(
(e) => useSearchStore.setState({ query: e.target.value }),
250

View File

@@ -23,6 +23,7 @@ import { Flex, Text } from "@theme-ui/components";
import TimeAgo from "../time-ago";
import { Lock } from "../icons";
import { useEditorStore } from "../../stores/editor-store";
import { strings } from "@notesnook/intl";
type SessionItemProps = {
session: HistorySession;
@@ -47,7 +48,7 @@ export function SessionItem(props: SessionItemProps) {
alignItems: "center",
justifyContent: "space-between"
}}
title="Click to preview"
title={strings.clickToPreview()}
onClick={() =>
useEditorStore.getState().openDiffSession(noteId, session.id)
}

View File

@@ -39,6 +39,7 @@ import { checkForUpdate, installUpdate } from "../../utils/updater";
import { getTimeAgo, toTitleCase } from "@notesnook/common";
import { User } from "@notesnook/core";
import { showUpdateAvailableNotice } from "../../dialogs/confirm";
import { strings } from "@notesnook/intl";
function StatusBar() {
const user = useUserStore((state) => state.user);
@@ -84,7 +85,7 @@ function StatusBar() {
>
<Circle size={7} color={"var(--icon-error)"} />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Email not confirmed
{strings.emailNotConfirmed()}
</Text>
</Button>
)}
@@ -104,7 +105,7 @@ function StatusBar() {
>
<Circle size={7} color="var(--icon-error)" />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Not logged in
{strings.notLoggedIn()}
</Text>
</Button>
) : null}
@@ -171,13 +172,15 @@ export default StatusBar;
function statusToInfoText(status: UpdateStatus) {
const { type } = status;
return type === "checking"
? "Checking for updates..."
? strings.checkingForUpdates()
: type === "downloading"
? `${Math.round(status.progress)}% updating...`
? `${Math.round(status.progress)}% ${strings.updating()}...`
: type === "completed"
? `v${status.version} downloaded (restart required)`
? `v${
status.version
} ${strings.network.downloaded()} (${strings.restartRequired()})`
: type === "available"
? `v${status.version} available`
? `v${status.version} ${strings.available()}`
: "";
}

View File

@@ -21,6 +21,7 @@ import { alpha } from "@theme-ui/color";
import { Flex, Text } from "@theme-ui/components";
import { Circle, Notes, Notebook, StarOutline, Tag, Plus } from "../icons";
import { ThemeMetadata } from "@notesnook/themes-server";
import { strings } from "@notesnook/intl";
export type ThemePreviewProps = {
theme: ThemeMetadata;
@@ -124,7 +125,7 @@ export function ThemePreview(props: ThemePreviewProps) {
fontSize: 7
}}
>
Notes
{strings.dataTypesPluralCamelCase.note()}
</Text>
<Plus
color={theme.previewColors.list.accentForeground}

View File

@@ -34,7 +34,8 @@ function Toggle(props) {
onlyIf,
premium,
testId,
disabled
disabled,
tip
} = props;
const [isLoading, setIsLoading] = useState(false);
const onClick = useCallback(async () => {
@@ -68,7 +69,11 @@ function Toggle(props) {
"& > label": { width: "auto", flexShrink: 0 }
}}
>
<Tip text={title} tip={isToggled ? onTip : offTip} sx={{ mr: 2 }} />
<Tip
text={title}
tip={tip ? tip : isToggled ? onTip : offTip}
sx={{ mr: 2 }}
/>
{isLoading ? (
<Loading size={18} />
) : (

View File

@@ -29,6 +29,7 @@ import { TrashItem as TrashItemType } from "@notesnook/core";
import { useEditorStore } from "../../stores/editor-store";
import { showMultiPermanentDeleteConfirmation } from "../../dialogs/confirm";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { strings } from "@notesnook/intl";
type TrashItemProps = { item: TrashItemType; date: number };
function TrashItem(props: TrashItemProps) {
@@ -78,18 +79,18 @@ const menuItems: (item: TrashItemType, ids?: string[]) => MenuItem[] = (
{
type: "button",
key: "restore",
title: "Restore",
title: strings.restore(),
icon: Restore.path,
onClick: async () => {
await store.restore(...ids);
showToast("success", `${pluralize(ids.length, "item")} restored`);
showToast("success", strings.itemsRestored(ids.length));
},
multiSelect: true
},
{
type: "button",
key: "delete",
title: "Delete",
title: strings.delete(),
icon: DeleteForver.path,
variant: "dangerous",
onClick: async () => {

View File

@@ -23,6 +23,7 @@ import { Lock } from "../icons";
import Field from "../field";
import { showToast } from "../../utils/toast";
import { ErrorText } from "../error-text";
import { strings } from "@notesnook/intl";
type UnlockViewProps = {
title: string;
@@ -104,7 +105,7 @@ export function UnlockView(props: UnlockViewProps) {
inputRef={passwordRef}
autoFocus
sx={{ width: ["95%", "95%", "max(30%, 400px)"] }}
placeholder="Enter password"
placeholder={strings.enterPassword()}
type="password"
onKeyUp={async (e) => {
if (e.key === "Enter") {
@@ -125,7 +126,7 @@ export function UnlockView(props: UnlockViewProps) {
await submit();
}}
>
{isUnlocking ? "Unlocking..." : buttonTitle}
{isUnlocking ? strings.unlocking() + "..." : buttonTitle}
</Button>
</Flex>
);

View File

@@ -27,6 +27,7 @@ import { store as notebookStore } from "../stores/notebook-store";
import { store as appStore } from "../stores/app-store";
import { db } from "../common/db";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type AddNotebookDialogProps = BaseDialogProps<boolean> & {
parentId?: string;
@@ -92,7 +93,7 @@ export const AddNotebookDialog = DialogManager.register(
data-test-id="title-input"
autoFocus
required
label="Title"
label={strings.title()}
name="title"
id="title"
onChange={(e) => (title.current = e.target.value)}
@@ -104,12 +105,12 @@ export const AddNotebookDialog = DialogManager.register(
/>
<Field
data-test-id="description-input"
label="Description"
label={strings.description()}
name="description"
id="description"
onChange={(e) => (description.current = e.target.value)}
defaultValue={description.current}
helpText="Optional"
helpText={strings.optional()}
sx={{ mt: 1 }}
/>
</Dialog>

View File

@@ -37,6 +37,7 @@ import { getFormattedDate } from "@notesnook/common";
import { MONTHS_FULL, getTimeFormat } from "@notesnook/core";
import { Note, Reminder } from "@notesnook/core";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
dayjs.extend(customParseFormat);
@@ -213,7 +214,7 @@ export const AddReminderDialog = DialogManager.register(
>
<Field
id="title"
label="Title"
label={strings.title()}
required
value={title}
data-test-id="title-input"
@@ -224,9 +225,9 @@ export const AddReminderDialog = DialogManager.register(
<Field
as="textarea"
id="description"
label="Description"
label={strings.description()}
data-test-id="description-input"
helpText="Optional"
helpText={strings.optional()}
value={description}
styles={{
input: {
@@ -367,7 +368,7 @@ export const AddReminderDialog = DialogManager.register(
<>
<Field
id="date"
label="Date"
label={strings.date()}
required
inputRef={dateInputRef}
data-test-id="date-input"
@@ -438,7 +439,7 @@ export const AddReminderDialog = DialogManager.register(
<>
<LabeledSelect
id="month"
label="Month"
label={strings.month()}
value={`${dayjs(date).month()}`}
options={MONTHS_FULL.map((month, index) => ({
value: `${index}`,
@@ -450,7 +451,7 @@ export const AddReminderDialog = DialogManager.register(
/>
<LabeledSelect
id="day"
label="Day"
label={strings.day()}
value={`${dayjs(date).date()}`}
options={new Array(dayjs(date).daysInMonth())
.fill("0")
@@ -466,7 +467,7 @@ export const AddReminderDialog = DialogManager.register(
) : null}
<Field
id="time"
label="Time"
label={strings.time()}
required
data-test-id="time-input"
helpText={`${
@@ -514,20 +515,20 @@ export const AddReminderDialog = DialogManager.register(
<Text variant="subBody" sx={{ mt: 1 }}>
{selectedDays.length === 0 && recurringMode !== RecurringModes.DAY
? recurringMode === RecurringModes.WEEK
? "Select day of the week to repeat the reminder."
: "Select nth day(s) of the month to repeat the reminder."
? strings.reminderRepeatStrings.week.selectDays()
: strings.reminderRepeatStrings.month.selectDays()
: repeatsDaily
? `Repeats daily at ${date.format(timeFormat())}.`
: `Repeats every ${recurringMode} on ${getSelectedDaysText(
selectedDays,
recurringMode
)} at ${date.format(timeFormat())}.`}
? strings.reminderRepeatStrings.day(date.format(timeFormat()))
: strings.reminderRepeatStrings.repeats(
1,
recurringMode,
getSelectedDaysText(selectedDays, recurringMode),
date.format(timeFormat())
)}
</Text>
) : (
<Text variant="subBody" sx={{ mt: 1 }}>
{`The reminder will start on ${date} at ${date.format(
timeFormat()
)}.`}
{strings.reminderStarts(date.toString(), date.format(timeFormat()))}
</Text>
)}
</Dialog>

View File

@@ -35,6 +35,7 @@ import { create } from "zustand";
import { VirtualizedGrouping } from "@notesnook/core";
import { ResolvedItem } from "@notesnook/common";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type SelectedReference = {
id: string;
@@ -118,8 +119,8 @@ export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
sx={{ mt: 2 }}
itemGap={5}
placeholders={{
empty: "Add a new tag",
filter: "Search or add a new tag"
empty: strings.addATag(),
filter: strings.searchForTags()
}}
filter={async (query) => {
setTags(

View File

@@ -67,6 +67,7 @@ import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { ConfirmDialog } from "./confirm";
import { showToast } from "../utils/toast";
import { Loader } from "../components/loader";
import { strings } from "@notesnook/intl";
type ToolbarAction = {
title: string;
@@ -346,7 +347,7 @@ export const AttachmentsDialog = DialogManager.register(
renderRow={AttachmentRow}
/>
) : (
<Loader title="Loading attachments..." />
<Loader title={strings.loading() + "..."} />
)}
</FlexScrollContainer>
</Flex>
@@ -461,7 +462,7 @@ const Sidebar = memo(
<Input
id="search"
name="search"
placeholder="Search"
placeholder={strings.search()}
sx={{ m: 2, mb: 0, width: "auto", bg: "background", py: "7px" }}
onChange={(e) => {
setRoute(e.target.value ? "none" : "all");
@@ -485,7 +486,7 @@ const Sidebar = memo(
<Flex sx={{ flexDirection: "column" }}>
<Flex sx={{ pl: 2, m: 2, mt: 1, justifyContent: "space-between" }}>
<Flex sx={{ flexDirection: "column" }}>
<Text variant="body">{pluralize(counts.all, "file")}</Text>
<Text variant="body">{strings.files(counts.all)}</Text>
{result.status === "fulfilled" && (
<Text variant="subBody">
{formatBytes(result.value || 0)}
@@ -503,28 +504,18 @@ const Sidebar = memo(
height: 38
}}
disabled={!!downloadStatus}
title="Clear cache"
title={strings.clearCache()}
onClick={async () => {
if (
await ConfirmDialog.show({
title: "Clear attachments cache?",
message: `Clearing attachments cache will perform the following actions:
- Downloaded images & files: **cleared**
- Pending uploads: **cleared**
- Uploaded images & files: _unaffected_
All attachments will be downloaded & cached again on access.
---
**Only use this for troubleshooting purposes. If you are having persistent issues, it is recommended that you reach out to us via support@streetwriters.co so we can help you resolve it permanently.**`,
title: strings.clearCacheConfirm(),
message: strings.clearCacheConfirmDesc(),
negativeButtonText: "No",
positiveButtonText: "Yes"
})
) {
await db.fs().clear();
showToast("success", "Attachments cache cleared!");
showToast("success", strings.cacheCleared());
}
}}
>
@@ -539,7 +530,7 @@ All attachments will be downloaded & cached again on access.
width: 38,
height: 38
}}
title="Download all attachments"
title={strings.downloadAllAttachments()}
onClick={async () => {
if (downloadStatus) {
await cancelDownload();

View File

@@ -22,6 +22,7 @@ import Field from "../components/field";
import Dialog from "../components/dialog";
import { Box, Button, Text } from "@theme-ui/components";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type BackupPasswordDialogProps = BaseDialogProps<boolean> & {
validate: (outputs: {
@@ -88,7 +89,7 @@ export const BackupPasswordDialog = DialogManager.register(
required
autoFocus
data-test-id="encryption-key"
label="Encryption key"
label={strings.encryptionKey()}
type="password"
id="key"
name="key"
@@ -98,7 +99,7 @@ export const BackupPasswordDialog = DialogManager.register(
required
autoFocus
data-test-id="password"
label="Password"
label={strings.password()}
type="password"
autoComplete="current-password"
id="password"
@@ -107,9 +108,7 @@ export const BackupPasswordDialog = DialogManager.register(
)}
</Box>
<Button variant="anchor" onClick={() => setIsEncryptionKey((s) => !s)}>
{isEncryptionKey
? "Don't have encryption key? Use password."
: "Forgot password? Use encryption key."}
{strings.useEncryptionKey()}
</Button>
{error && (

View File

@@ -68,6 +68,7 @@ import {
CustomToolbar,
SyncOff
} from "../../components/icons";
import { strings } from "@notesnook/intl";
type Feature = {
id: string;
@@ -458,7 +459,7 @@ export function Features() {
>
<Pro color="accent" size={16} />
<Text variant="body" ml={"2px"} sx={{ color: "accent" }}>
Pro
{strings.pro()}
</Text>
</Flex>
)}
@@ -499,7 +500,7 @@ export function Features() {
ml={"2px"}
sx={{ color: "accent" }}
>
Pro
{strings.pro()}
</Text>
</Flex>
)}

View File

@@ -25,6 +25,7 @@ import tinycolor from "tinycolor2";
import { db } from "../common/db";
import { showToast } from "../utils/toast";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type CreateColorDialogProps = BaseDialogProps<string | false>;
export const CreateColorDialog = DialogManager.register(
@@ -68,7 +69,7 @@ export const CreateColorDialog = DialogManager.register(
>
<Field
required
label="Title"
label={strings.title()}
id="title"
name="title"
autoFocus
@@ -78,7 +79,7 @@ export const CreateColorDialog = DialogManager.register(
<Field
inputRef={colorRef}
required
label="Color"
label={strings.color()}
id="color"
name="color"
data-test-id="color-input"

View File

@@ -28,6 +28,7 @@ import { db } from "../common/db";
import { showToast } from "../utils/toast";
import { useStore as useSettingStore } from "../stores/setting-store";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type EditProfilePictureDialogProps = BaseDialogProps<boolean> & {
profile?: Profile;
@@ -129,8 +130,8 @@ export const EditProfilePictureDialog = DialogManager.register(
}}
>
{profilePicture
? "Change profile picture"
: "Select profile picture"}
? strings.changeProfilePicture()
: strings.selectProfilePicture()}
</Button>
{profilePicture ? (
<Button
@@ -144,7 +145,9 @@ export const EditProfilePictureDialog = DialogManager.register(
setScale(1);
}}
>
{typeof profilePicture === "string" ? "Clear" : "Reset"}
{typeof profilePicture === "string"
? strings.clear()
: strings.reset()}
</Button>
) : null}
</Flex>

View File

@@ -25,6 +25,7 @@ import Field from "../components/field";
import { Loading } from "../components/icons";
import Dialog from "../components/dialog";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type EmailChangeState = {
newEmail: string;
@@ -128,8 +129,8 @@ export const EmailChangeDialog = DialogManager.register(
inputRef={codeRef}
id="code"
name="code"
label="6-digit code"
helpText={`Enter 6-digit code sent to ${emailChangeState.newEmail}`}
label={strings.sixDigitCode()}
helpText={strings.enterSixDigitCode()}
type="text"
required
action={{
@@ -139,9 +140,9 @@ export const EmailChangeDialog = DialogManager.register(
{isSending ? (
<Loading size={18} />
) : enabled ? (
`Resend code`
strings.resendCode()
) : (
`Resend in ${elapsed}`
strings.resendCode(elapsed)
)}
</Text>
),
@@ -156,7 +157,7 @@ export const EmailChangeDialog = DialogManager.register(
inputRef={emailRef}
id="newEmail"
name="newEmail"
label="New email"
label={strings.newEmail()}
type="email"
required
/>
@@ -164,12 +165,12 @@ export const EmailChangeDialog = DialogManager.register(
inputRef={passwordRef}
id="password"
name="password"
label="Your account password"
label={strings.accountPassword()}
type="password"
required
/>
<Text variant="subBody" sx={{ mt: 1 }}>
You will be logged out from all your other devices.
{strings.changeEmailNotice()}
</Text>
</>
)}

View File

@@ -26,6 +26,7 @@ import { useState } from "react";
import { useSessionState } from "../hooks/use-session-state";
import Accordion from "../components/accordion";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
let interval = 0;
type EmailVerificationDialogProps = BaseDialogProps<boolean>;
@@ -98,11 +99,7 @@ export const EmailVerificationDialog = DialogManager.register(
variant="body"
sx={{ borderRadius: "default", alignSelf: "stretch" }}
>
We have sent the email confirmation link at{" "}
<Text as="b" sx={{ color: "accent" }}>
{user.email}
</Text>
.
{strings.emailConfirmationLinkSent()}
</Text>
<Accordion
isClosed
@@ -114,10 +111,9 @@ export const EmailVerificationDialog = DialogManager.register(
}}
>
<Text variant={"body"} px={1} pb={1}>
{`If you didn't get an email from us or the confirmation link isn't
working,`}{" "}
<b>please send us an email from your registered email address</b>{" "}
and we will manually confirm your account.
{strings.emailConfirmationNotice()[0]}{" "}
<b>{strings.emailConfirmationNotice()[1]}</b>{" "}
{strings.emailConfirmationNotice()[2]}.
</Text>
</Accordion>
</Flex>

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Text } from "@theme-ui/components";
import { Flex, Link, Text } from "@theme-ui/components";
import { appVersion } from "../utils/version";
import Field from "../components/field";
import Dialog from "../components/dialog";
@@ -31,17 +31,11 @@ import { ErrorText } from "../components/error-text";
import { Debug } from "@notesnook/core";
import { ConfirmDialog } from "./confirm";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
const PLACEHOLDERS = {
title: "Briefly describe what happened",
body: `Tell us more about the issue you are facing.
For example things like:
1. Steps to reproduce the issue
2. Things you have tried so far
3. etc.
This is all optional, of course.`
title: strings.issueTitlePlaceholder(),
body: strings.issuePlaceholder()
};
type IssueDialogProps = BaseDialogProps<boolean>;
@@ -54,15 +48,18 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
return (
<Dialog
isOpen={true}
title={"Report an issue"}
title={strings.reportAnIssue()}
onClose={() => props.onClose(false)}
positiveButton={{
text: "Submit",
text: strings.submit(),
form: "issueForm",
loading: isSubmitting,
disabled: isSubmitting
}}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
negativeButton={{
text: strings.cancel(),
onClick: () => props.onClose(false)
}}
>
<Flex
id="issueForm"
@@ -99,7 +96,7 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
>
<Field
required
label="Title"
label={strings.title()}
id="title"
name="title"
placeholder={PLACEHOLDERS.title}
@@ -109,7 +106,7 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
as="textarea"
required
variant="forms.input"
label="Description"
label={strings.description()}
id="body"
name="body"
placeholder={PLACEHOLDERS.body}
@@ -127,8 +124,16 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
p={1}
sx={{ borderRadius: "default" }}
>
Your bug report is public. Do NOT include sensitive information
(email, passwords etc) in the issue title or description.
{strings.issueNotice[0]()}{" "}
<Link
href="https://github.com/streetwriters/notesnook/issues"
title="github.com/streetwriters/notesnook/issues"
/>{" "}
{strings.issueNotice[1]()}{" "}
<Link
href="https://discord.gg/zQBK97EE22"
title={strings.issueNotice[2]()}
/>
</Text>
<Text variant="subBody" mt={1}>
{getDeviceInfo()

View File

@@ -29,6 +29,7 @@ import { useStore as useTagStore } from "../stores/tag-store";
import { useStore as useNoteStore } from "../stores/note-store";
import { useStore as useAppStore } from "../stores/app-store";
import { Color, Tag } from "@notesnook/core";
import { strings } from "@notesnook/intl";
type ItemDialogProps = BaseDialogProps<false | string> & {
title: string;
@@ -74,7 +75,7 @@ export const ItemDialog = DialogManager.register(function ItemDialog(
>
<Field
required
label="Title"
label={strings.title()}
id="title"
name="title"
autoFocus

View File

@@ -58,6 +58,7 @@ import {
import { ErrorText } from "../../components/error-text";
import { AuthenticatorType } from "@notesnook/core";
import { MultifactorDialog } from "./multi-factor-dialog";
import { strings } from "@notesnook/intl";
const QRCode = React.lazy(() => import("../../re-exports/react-qrcode-logo"));
@@ -292,7 +293,7 @@ function ChooseAuthenticator(props: ChooseAuthenticatorProps) {
px={1}
sx={{ borderRadius: "default", color: "accent" }}
>
Recommended
{strings.recommended()}
</Text>
) : (
false
@@ -348,14 +349,10 @@ function SetupAuthenticatorApp(props: SetupAuthenticatorProps) {
return (
<VerifyAuthenticatorForm
codeHelpText={
"After scanning the QR code image, the app will display a code that you can enter below."
}
codeHelpText={strings.mfaScanQrCodeHelpText()}
onSubmitCode={onSubmitCode}
>
<Text variant={"body"}>
Scan the QR code below with your authenticator app.
</Text>
<Text variant={"body"}>{strings.mfaScanQrCode()}</Text>
<Box sx={{ alignSelf: "center" }}>
{authenticatorDetails.authenticatorUri ? (
<Suspense fallback={<Loading />}>
@@ -369,10 +366,7 @@ function SetupAuthenticatorApp(props: SetupAuthenticatorProps) {
<Loading />
)}
</Box>
<Text variant={"subBody"}>
{`If you can't scan the QR code above, enter this text instead (spaces
don't matter):`}
</Text>
<Text variant={"subBody"}>{`${strings.scanQrError()}:`}</Text>
<Flex
bg="var(--background-secondary)"
mt={2}
@@ -433,9 +427,7 @@ function SetupEmail(props: SetupAuthenticatorProps) {
return (
<VerifyAuthenticatorForm
codeHelpText={
"You will receive a 2FA code on your email address which you can enter below"
}
codeHelpText={strings.mfaEmailDesc()}
onSubmitCode={onSubmitCode}
>
<Flex
@@ -475,9 +467,9 @@ function SetupEmail(props: SetupAuthenticatorProps) {
{isSending ? (
<Loading size={18} />
) : enabled ? (
`Send code`
strings.sendCode()
) : (
`Resend (${elapsed})`
strings.resendCode(elapsed)
)}
</Button>
</Flex>
@@ -496,17 +488,15 @@ function SetupSMS(props: SetupAuthenticatorProps) {
return (
<VerifyAuthenticatorForm
codeHelpText={
"You will receive a 2FA code on your phone number which you can enter below"
}
codeHelpText={strings.mfaSmsDesc()}
onSubmitCode={onSubmitCode}
>
<Field
inputRef={inputRef}
id="phone-number"
name="phone-number"
helpText="Authentication codes will be sent to this number"
label="Phone number"
helpText={strings.mfaSmsDesc()}
label={strings.phoneNumber()}
sx={{ mt: 2 }}
autoFocus
required
@@ -534,15 +524,15 @@ function SetupSMS(props: SetupAuthenticatorProps) {
{isSending ? (
<Loading size={18} />
) : enabled ? (
`Send code`
strings.sendCode()
) : (
`Resend (${elapsed})`
strings.resendCode(elapsed)
)}
</Text>
),
onClick: async () => {
if (!phoneNumber) {
setError("Please provide a phone number.");
setError(strings.phoneNumberNotEntered());
return;
}
@@ -711,21 +701,21 @@ function TwoFactorEnabled(props: TwoFactorEnabledProps) {
mt={2}
sx={{ fontSize: "subheading", textAlign: "center" }}
>
Two-factor authentication enabled!
{strings.twoFactorAuthEnabled()}
</Text>
<Text
variant={"body"}
mt={1}
sx={{ textAlign: "center", color: "var(--paragraph-secondary)" }}
>
Your account is now 100% secure against unauthorized logins.
{strings.mfaDone()}
</Text>
<Button
mt={2}
sx={{ borderRadius: 100, px: 6 }}
onClick={() => props.onClose?.(true)}
>
Done
{strings.done()}
</Button>
<Button
@@ -767,23 +757,21 @@ function Fallback2FAEnabled(props: Fallback2FAEnabledProps) {
mt={2}
sx={{ fontSize: "subheading", textAlign: "center" }}
>
Fallback 2FA method enabled!
{strings.fallbackMethodEnabled()}
</Text>
<Text
variant={"body"}
mt={1}
sx={{ textAlign: "center", color: "var(--paragraph-secondary)" }}
>
You will now receive your 2FA codes on your{" "}
{mfaMethodToPhrase(fallbackMethod)} in case you lose access to your{" "}
{mfaMethodToPhrase(primaryMethod)}.
{strings.mfaFallbackMethodText(fallbackMethod, primaryMethod)}
</Text>
<Button
mt={2}
sx={{ borderRadius: 100, px: 6 }}
onClick={() => onClose?.(true)}
>
Done
{strings.done()}
</Button>
</Flex>
);
@@ -812,7 +800,7 @@ function VerifyAuthenticatorForm(props: VerifyAuthenticatorFormProps) {
id="code"
name="code"
helpText={codeHelpText}
label="Enter the 6-digit code"
label={strings.enterSixDigitCode()}
sx={{ alignItems: "center", mt: 2 }}
required
placeholder="010101"

View File

@@ -26,6 +26,7 @@ import { TaskManager } from "../common/task-manager";
import Dialog from "../components/dialog";
import { ErrorText } from "../components/error-text";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type MigrationProgressEvent = {
collection: string;
@@ -102,9 +103,9 @@ export const MigrationDialog = DialogManager.register(function MigrationDialog(
}}
/>
<Text as="p" variant={"subBody"} sx={{ mt: 2 }}>
If this continues to happen, please reach out to us via{" "}
<a href="https://discord.com/invite/zQBK97EE22">Discord</a> or email
us at{" "}
{strings.migrationErrorNotice()[0]}{" "}
<a href="https://discord.com/invite/zQBK97EE22">Discord</a>{" "}
{strings.migrationErrorNotice()[1]}{" "}
<a href="mailto:support@streetwriters.co">support@streetwriters.co</a>
</Text>
</Dialog>

View File

@@ -49,6 +49,7 @@ import { pluralize } from "@notesnook/common";
import Field from "../components/field";
import { AddNotebookDialog } from "./add-notebook-dialog";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type MoveNoteDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
type NotebookReference = {
@@ -176,7 +177,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
styles={{
input: { p: "7.5px" }
}}
placeholder={"Search notebooks"}
placeholder={strings.searchNotebooks()}
onChange={async (e) => {
const query = e.target.value.trim();
const ids = await (query
@@ -199,7 +200,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
}}
sx={{ textDecoration: "none", mb: 2 }}
>
Reset selection
{strings.resetSelection()}
</Button>
)}
{notebooks.length > 0 ? (
@@ -315,9 +316,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
alignItems: "center"
}}
>
<Text variant="body">
Please add a notebook to start linking notes.
</Text>
<Text variant="body">{strings.notebooksEmpty()}</Text>
<Button
data-test-id="add-new-notebook"
variant="secondary"
@@ -332,7 +331,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
)
}
>
Add new notebook
{strings.addNotebook()}
</Button>
</Flex>
)}
@@ -448,7 +447,7 @@ function NotebookItem(props: {
>
<Plus
size={18}
title="New notebook"
title={strings.newNotebook()}
onClick={async (e) => {
e.stopPropagation();
await AddNotebookDialog.show({ parentId: notebook.id });

View File

@@ -35,6 +35,7 @@ import { NoteResolvedData, ResolvedItem } from "@notesnook/common";
import { Lock } from "../components/icons";
import { ellipsize } from "@notesnook/core";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type NoteLinkingDialogProps = BaseDialogProps<LinkAttributes | false> & {
attributes?: LinkAttributes;
@@ -82,7 +83,7 @@ export const NoteLinkingDialog = DialogManager.register(
<>
<Field
autoFocus
placeholder="Type # to only search headings"
placeholder={strings.searchSectionToLinkPlaceholder()}
sx={{ mx: 0 }}
onChange={async (e) => {
const query = e.target.value.trim().toLowerCase();
@@ -113,15 +114,16 @@ export const NoteLinkingDialog = DialogManager.register(
setBlocks([]);
}}
>
Selected note: {selectedNote.title} (click to deselect)
{strings.linkNoteSelectedNote()}: {selectedNote.title} (
{strings.clickToDeselect()})
</Button>
{isNoteLocked ? (
<Text variant="body" sx={{ mt: 1 }}>
Linking to a specific block is not available for locked notes.
{strings.noteLockedBlockLink()}
</Text>
) : blocks.length <= 0 ? (
<Text variant="body" sx={{ mt: 1 }}>
There are no blocks in this note.
{strings.noBlocksOnNote()}
</Text>
) : null}
<ScrollContainer>
@@ -158,7 +160,7 @@ export const NoteLinkingDialog = DialogManager.register(
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
>
{ellipsize(item.content, 200, "end").trim() ||
"(empty block)"}
strings.linkNoteEmptyBlock()}
</Text>
<Text
variant="subBody"
@@ -182,7 +184,7 @@ export const NoteLinkingDialog = DialogManager.register(
<>
<Field
autoFocus
placeholder="Search for a note to link to..."
placeholder={strings.searchNoteToLinkPlaceholder()}
sx={{ mx: 0 }}
onChange={async (e) => {
const query = e.target.value.trim();

View File

@@ -47,6 +47,7 @@ import { isMacStoreApp } from "../utils/platform";
import { ErrorText } from "../components/error-text";
import { BuyDialog } from "./buy-dialog";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type Step = {
title: string;
@@ -240,7 +241,7 @@ function JoinCause({ onNext }: { onNext: () => void }) {
onNext();
}}
>
Join the community
{strings.joinDiscord()}
</Button>
<Button
variant={"anchor"}
@@ -248,7 +249,7 @@ function JoinCause({ onNext }: { onNext: () => void }) {
onClick={() => onNext()}
sx={{ color: "var(--paragraph-secondary)" }}
>
Skip for now
{strings.skip()}
</Button>
</Flex>
);
@@ -295,7 +296,7 @@ function Importer({ onClose }: { onClose: () => void }) {
onClose();
}}
>
Start importing now
{strings.startImportingNow()}
</Button>
<Button
variant={"anchor"}
@@ -303,7 +304,7 @@ function Importer({ onClose }: { onClose: () => void }) {
onClick={() => onClose()}
sx={{ color: "var(--paragraph-secondary)" }}
>
Skip for now
{strings.skip()}
</Button>
</Flex>
);
@@ -478,7 +479,7 @@ function TrialOffer({ onClose }: { onClose: () => void }) {
BuyDialog.show({ plan: "monthly", couponCode: "TRIAL2PRO" });
}}
>
Upgrade now
{strings.upgradeToPro()}
</Button>
<Button
variant={"secondary"}
@@ -489,7 +490,7 @@ function TrialOffer({ onClose }: { onClose: () => void }) {
const result = await TaskManager.startTask({
type: "status",
id: "trialActivation",
title: "Activating trial",
title: strings.activatingTrial(),
action: () => db.user.activateTrial()
});
if (result) onClose();
@@ -504,7 +505,7 @@ function TrialOffer({ onClose }: { onClose: () => void }) {
}
}}
>
{loading ? <Loading size={16} /> : "Try free for 14 days"}
{loading ? <Loading size={16} /> : strings.tryFreeFor14Days()}
</Button>
</Flex>
<Button
@@ -513,7 +514,7 @@ function TrialOffer({ onClose }: { onClose: () => void }) {
onClick={() => onClose()}
sx={{ color: "var(--paragraph-secondary)" }}
>
Skip for now
{strings.skip()}
</Button>
</Flex>
);

View File

@@ -21,6 +21,7 @@ import { useEffect, useState } from "react";
import { Box, Flex, Text } from "@theme-ui/components";
import Dialog from "../components/dialog";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
type Progress = {
total?: number;
@@ -62,7 +63,7 @@ export const ProgressDialog = DialogManager.register(function ProgressDialog<T>(
{current > 0 ? (
<>
<Text variant="subBody">
{current} of {total}
{current} {strings.of()} {total}
</Text>
<Box
sx={{

View File

@@ -31,6 +31,7 @@ import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { usePromise } from "@notesnook/common";
import { Loader } from "../components/loader";
import { showToast } from "../utils/toast";
import { strings } from "@notesnook/intl";
const QRCode = React.lazy(() => import("../re-exports/react-qrcode-logo"));
type RecoveryKeyDialogProps = BaseDialogProps<false>;
@@ -45,10 +46,10 @@ export const RecoveryKeyDialog = DialogManager.register(
<Dialog
testId="recovery-key-dialog"
isOpen={true}
title="Backup your recovery key"
title={strings.saveRecoveryKey()}
width={400}
positiveButton={{
text: "I have backed up my key",
text: strings.keyBackedUp(),
onClick: () => {
Config.set("recoveryKeyBackupDate", Date.now());
props.onClose(false);
@@ -56,14 +57,11 @@ export const RecoveryKeyDialog = DialogManager.register(
}}
>
{key.status !== "fulfilled" ? (
<Loader title="Getting your encryption key..." />
<Loader title={strings.gettingEncryptionKey()} />
) : (
<Flex sx={{ overflow: "hidden", flex: 1, flexDirection: "column" }}>
<Flex sx={{ overflowY: "auto", flexDirection: "column" }}>
<ErrorText
error="In case you forget your password, your recovery key is the only way to recover your data."
mt={0}
/>
<ErrorText error={strings.saveRecoveryKeyDesc()} mt={0} />
<Text
data-test-id="recovery-key"
className="selectable"
@@ -138,7 +136,7 @@ export const RecoveryKeyDialog = DialogManager.register(
}}
sx={{ fontSize: "body" }}
>
Download QR Code
{strings.saveQRCode()}
</Button>
<Button
variant="secondary"
@@ -156,7 +154,7 @@ export const RecoveryKeyDialog = DialogManager.register(
}}
sx={{ fontSize: "body" }}
>
Download file
{strings.network.download()}
</Button>
</Flex>
</Flex>

View File

@@ -26,6 +26,7 @@ import { Clock, Refresh } from "../components/icons";
import Note from "../components/note";
import { getFormattedReminderTime, usePromise } from "@notesnook/common";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type ReminderPreviewDialogProps = BaseDialogProps<false> & {
reminder: Reminder;
@@ -90,7 +91,7 @@ export const ReminderPreviewDialog = DialogManager.register(
<IconTag icon={Clock} text={getFormattedReminderTime(reminder)} />
</Flex>
<Text variant="body">Remind me in:</Text>
<Text variant="body">{strings.remindMeIn()}:</Text>
<Flex
sx={{
alignItems: "center",
@@ -124,7 +125,7 @@ export const ReminderPreviewDialog = DialogManager.register(
referencedNotes.status === "fulfilled" &&
referencedNotes.value.length > 0 && (
<>
<Text variant="body">References:</Text>
<Text variant="body">{strings.references()}:</Text>
{referencedNotes.value.map((item, index) => (
<Note
key={item.id}

View File

@@ -17,12 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { Loading } from "../../../components/icons";
import { Box, Flex, Link, Text } from "@theme-ui/components";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { Box, Flex, Link, Text } from "@theme-ui/components";
import { useEffect, useState } from "react";
import { db } from "../../../common/db";
import { Transaction, TransactionStatus } from "@notesnook/core";
import { TransactionStatus, Transaction } from "@notesnook/core";
import { Loading } from "../../../components/icons";
const TransactionStatusToText: Record<TransactionStatus, string> = {
completed: "Completed",
@@ -129,7 +130,7 @@ export function BillingHistory() {
{transaction.amount} {transaction.currency}
</Text>
<Text as="td" variant="body">
{TransactionStatusToText[transaction.status]}
{strings.transactionStatusToText[transaction.status]()}
</Text>
<Text as="td" variant="body">
<Link
@@ -139,7 +140,7 @@ export function BillingHistory() {
variant="text.subBody"
sx={{ color: "accent" }}
>
View receipt
{strings.viewReceipt()}
</Link>
</Text>
</Box>

View File

@@ -64,6 +64,7 @@ import { Pro } from "../../../components/icons";
import { Icon } from "@notesnook/ui";
import { CURRENT_TOOLBAR_VERSION } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export function CustomizeToolbar() {
const sensors = useSensors(
@@ -158,10 +159,10 @@ export function CustomizeToolbar() {
alignItems: "center",
p: 1
}}
title="Add group"
title={strings.createAGroup()}
onClick={() => {
setItems(addGroup);
showToast("success", "Group added successfully");
showToast("success", strings.groupAdded());
}}
>
<Icon path={Icons.plus} color="paragraph" size={18} />

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Button, Text } from "@theme-ui/components";
import { FlexScrollContainer } from "../../../components/scroll-container";
import { useSpellChecker } from "../../../hooks/use-spell-checker";
import { strings } from "@notesnook/intl";
export function DictionaryWords() {
const words = useSpellChecker((store) => store.words);
@@ -36,10 +37,11 @@ export function DictionaryWords() {
}}
>
<Text variant="body" sx={{ my: 1 }}>
You have {words.length} custom dictionary words.
{strings.customDictWords(words.length)}
</Text>
{words.map((word) => (
<Button
key={word}
variant="menuitem"
sx={{ textAlign: "left", p: 1 }}
onClick={() => deleteWord(word)}

View File

@@ -17,12 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useStore as useAppStore } from "../../../stores/app-store";
import { strings } from "@notesnook/intl";
import { Button, Flex, Input, Link, Text, Box } from "@theme-ui/components";
import { useCallback, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
import { Box, Button, Flex, Input, Link, Text } from "@theme-ui/components";
import { db } from "../../../common/db";
import { importFiles } from "../../../utils/importer";
import { CheckCircleOutline } from "../../../components/icons";
import Accordion from "../../../components/accordion";
@@ -91,7 +90,7 @@ export function Importer() {
{isImporting ? (
<>
<Text variant="title" sx={{ textAlign: "center", mb: 4, mt: 150 }}>
<span ref={notesCounter}>0</span> notes imported.
<span ref={notesCounter}>0</span> {strings.notesImported()}.
</Text>
<Flex
@@ -109,7 +108,7 @@ export function Importer() {
<>
<CheckCircleOutline color="accent" sx={{ mt: 150 }} />
<Text variant="body" my={2} sx={{ textAlign: "center" }}>
Import completed. {errors.length} errors occured.
{strings.importCompleted()}. {strings.errorsOccured(errors.length)}
</Text>
<Button
variant="secondary"
@@ -121,7 +120,7 @@ export function Importer() {
setIsImporting(false);
}}
>
Start over
{strings.startOver()}
</Button>
{errors.length > 0 && (
<Flex
@@ -232,10 +231,10 @@ export function Importer() {
<Input {...getInputProps()} />
<Text variant="body" sx={{ textAlign: "center" }}>
{isDragActive
? "Drop the files here"
: "Drag & drop files here, or click to select files"}
? strings.dropFilesHere()
: strings.dragAndDropFiles()}
<br />
<Text variant="subBody">Only .zip files are supported.</Text>
<Text variant="subBody">{strings.onlyZipSupported()}</Text>
</Text>
<Box sx={{ display: "flex", flexWrap: "wrap", mt: 2 }}>
{files.map((file, i) => (

View File

@@ -25,6 +25,7 @@ import { useStore as useUserStore } from "../../../stores/user-store";
import { ErrorText } from "../../../components/error-text";
import { TaskManager } from "../../../common/task-manager";
import { isServerCompatible } from "@notesnook/core";
import { strings } from "@notesnook/intl";
export const ServerIds = ["notesnook-sync", "auth", "sse"] as const;
export type ServerId = (typeof ServerIds)[number];
@@ -44,23 +45,23 @@ const SERVERS: Server[] = [
{
id: "notesnook-sync",
host: "API_HOST",
title: "Sync server",
title: strings.syncServer(),
example: "http://localhost:4326",
description: "Server used to sync your notes & other data between devices."
description: strings.syncServerDesc()
},
{
id: "auth",
host: "AUTH_HOST",
title: "Auth server",
title: strings.authServer(),
example: "http://localhost:5326",
description: "Server used for login/sign up and authentication."
description: strings.authServerDesc()
},
{
id: "sse",
host: "SSE_HOST",
title: "Events server",
title: strings.sseServer(),
example: "http://localhost:7326",
description: "Server used to receive important notifications & events."
description: strings.sseServerDesc()
}
];
export function ServersConfiguration() {
@@ -74,7 +75,7 @@ export function ServersConfiguration() {
<>
{isLoggedIn ? (
<ErrorText
error="You must log out in order to change/reset server URLs."
error={strings.logoutToChangeServerUrls()}
sx={{ mb: 2, mt: 0 }}
/>
) : null}
@@ -111,7 +112,7 @@ export function ServersConfiguration() {
borderRadius: "default"
}}
>
Connected to all servers successfully.
{strings.connectedToServer()}
</Text>
) : null}
<Flex sx={{ mt: 1, justifyContent: "end", gap: 1 }}>
@@ -123,9 +124,8 @@ export function ServersConfiguration() {
useStore.getState().setServerUrls(urls);
await TaskManager.startTask({
type: "modal",
title: "App will reload in 5 seconds",
subtitle:
"Your changes have been saved and will be reflected after the app has refreshed.",
title: strings.appWillReloadIn(5),
subtitle: strings.changesReflectOnStart(),
action() {
return new Promise((resolve) => {
setTimeout(() => {
@@ -137,7 +137,7 @@ export function ServersConfiguration() {
});
}}
>
Save
{strings.save()}
</Button>
<Button
disabled={isLoggedIn}
@@ -148,21 +148,22 @@ export function ServersConfiguration() {
for (const host of HostIds) {
const url = urls[host];
const server = SERVERS.find((s) => s.host === host);
if (!server)
throw new Error(`Server with host ${host} not found.`);
if (!url) throw new Error("All server urls are required.");
if (!server) throw new Error(strings.serverNotFound(host));
if (!url) throw new Error(strings.allServerUrlsRequired());
const version = await fetch(`${url}/version`)
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(`Could not connect to ${server.title}.`);
throw new Error(
`${strings.couldNotConnectTo()} ${server.title}.`
);
if (version.id !== server.id)
throw new Error(
`The URL you have given (${url}) does not point to the ${server.title}.`
`${strings.incorrectServerUrl(url)} ${server.title}.`
);
if (!isServerCompatible(version.version))
throw new Error(
`The ${server.title} at ${url} is not compatible with this client.`
strings.serverVersionMismatch(server.title, url)
);
}
setSuccess(true);
@@ -171,7 +172,7 @@ export function ServersConfiguration() {
}
}}
>
Test connection
{strings.testConnection()}
</Button>
<Button
disabled={isLoggedIn}
@@ -181,9 +182,8 @@ export function ServersConfiguration() {
useStore.getState().setServerUrls();
await TaskManager.startTask({
type: "modal",
title: "App will reload in 5 seconds",
subtitle:
"Your changes have been saved and will be reflected after the app has refreshed.",
title: strings.appWillReloadIn(5),
subtitle: strings.changesReflectOnStart(),
action() {
return new Promise((resolve) => {
setTimeout(() => {
@@ -195,7 +195,7 @@ export function ServersConfiguration() {
});
}}
>
Reset
{strings.reset()}
</Button>
</Flex>
</Flex>

View File

@@ -22,6 +22,7 @@ import { Input, Label } from "@theme-ui/components";
import { useCallback, useEffect, useState } from "react";
import { deleteItem } from "@notesnook/core";
import { FlexScrollContainer } from "../../../components/scroll-container";
import { strings } from "@notesnook/intl";
export function SpellCheckerLanguages() {
const spellChecker = useSpellChecker();
@@ -52,7 +53,7 @@ export function SpellCheckerLanguages() {
return (
<>
<Input
placeholder="Filter languages"
placeholder={strings.filterLanguages()}
sx={{ mx: "2px", my: 2, width: "auto" }}
onChange={(e) => filter(e.currentTarget.value.toLowerCase().trim())}
/>

View File

@@ -29,6 +29,7 @@ import { Loading } from "../../../components/icons";
import { Features } from "../../buy-dialog/features";
import { ConfirmDialog } from "../../confirm";
import { BuyDialog } from "../../buy-dialog";
import { strings } from "@notesnook/intl";
const PROVIDER_MAP = {
0: "Streetwriters",
@@ -108,7 +109,7 @@ export function SubscriptionStatus() {
color: "accent"
}}
>
CURRENT PLAN
{strings.currentPlan()}
</Text>
<Text
variant="heading"
@@ -164,7 +165,7 @@ export function SubscriptionStatus() {
}
}}
>
Cancel subscription
{strings.cancelSub()}
</Button>
)}
<Button
@@ -201,7 +202,7 @@ export function SubscriptionStatus() {
{!isPro && (
<>
<Button variant="accent" onClick={() => BuyDialog.show({})}>
{isProCancelled ? "Resubscribe" : "Upgrade to Pro"}
{isProCancelled ? strings.resubToPro() : strings.upgradeToPro()}
</Button>
{isBasic && (
<Button
@@ -212,7 +213,7 @@ export function SubscriptionStatus() {
{isActivatingTrial ? (
<Loading size={16} />
) : (
"Try free for 14 days"
strings.tryFreeFor14Days()
)}
</Button>
)}

View File

@@ -41,6 +41,7 @@ import { showToast } from "../../../utils/toast";
import { showFilePicker, readFile } from "../../../utils/file-picker";
import { VirtualizedGrid } from "../../../components/virtualized-grid";
import { ThemeDetailsDialog } from "../../theme-details-dialog";
import { strings } from "@notesnook/intl";
const ThemesClient = ThemesTRPC.createClient({
links: [
@@ -145,7 +146,7 @@ function ThemesList() {
return (
<>
<Input
placeholder="Search themes"
placeholder={strings.searchThemes()}
sx={{ mt: 2 }}
onChange={debounce((e) => setSearchQuery(e.target.value), 500)}
/>
@@ -202,7 +203,7 @@ function ThemesList() {
flexShrink: 0
}}
>
Load from file
{strings.loadFromFile()}
</Button>
</Flex>
</Flex>
@@ -299,8 +300,10 @@ function ThemeItem(props: ThemeItemProps) {
>
{isApplying ? (
<Loading color="accent" size={18} />
) : theme.colorScheme === "dark" ? (
strings.setAsDarkTheme()
) : (
`Set as ${theme.colorScheme}`
strings.setAsLightTheme()
)}
</Button>
)}

View File

@@ -30,6 +30,7 @@ import { db } from "../../../common/db";
import { showToast } from "../../../utils/toast";
import { EditProfilePictureDialog } from "../../edit-profile-picture-dialog";
import { PromptDialog } from "../../prompt";
import { strings } from "@notesnook/intl";
export function UserProfile() {
const user = useUserStore((store) => store.user);
@@ -81,10 +82,8 @@ export function UserProfile() {
<User size={30} />
</Flex>
<Flex sx={{ flexDirection: "column" }}>
<Text variant={"title"}>You are not logged in</Text>
<Text variant={"subBody"}>
Login or create an account to sync your notes.
</Text>
<Text variant={"title"}>{strings.loginMessage()}</Text>
<Text variant={"subBody"}>{strings.loginMessageActionText()}</Text>
</Flex>
</Flex>
);
@@ -138,13 +137,13 @@ export function UserProfile() {
visibility: "collapse",
cursor: "pointer"
}}
title="Edit profile picture"
title={strings.editProfilePicture()}
onClick={async () => {
await EditProfilePictureDialog.show({ profile });
}}
>
<Text variant="body" sx={{ color: "white" }}>
Edit
{strings.edit()}
</Text>
</Flex>
</Flex>
@@ -165,16 +164,15 @@ export function UserProfile() {
</Text>
<Text variant={"title"}>
{profile?.fullName || "Your full name"}{" "}
{profile?.fullName || strings.yourFullName()}{" "}
<Edit
sx={{ display: "inline-block", cursor: "pointer" }}
size={12}
title="Edit full name"
title={strings.editFullName()}
onClick={async () => {
const fullName = await PromptDialog.show({
title: "Edit your full name",
description:
"Your profile data is 100% end-to-end encrypted.",
title: strings.editFullName(),
description: strings.setFullNameDesc(),
defaultValue: profile?.fullName
});
try {
@@ -190,7 +188,7 @@ export function UserProfile() {
/>
</Text>
<Text variant={"subBody"}>
{user.email} Member since{" "}
{user.email} {strings.memberSince()}{" "}
{getFormattedDate(getObjectIdTimestamp(user.id), "date")}
</Text>
</Flex>

View File

@@ -76,6 +76,7 @@ import { ScopedThemeProvider } from "../../components/theme-provider";
import { AppLockSettings } from "./app-lock-settings";
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
import { ServersSettings } from "./servers-settings";
import { strings } from "@notesnook/intl";
type SettingsDialogProps = BaseDialogProps<false> & {
activeSection?: SectionKeys;
@@ -269,7 +270,7 @@ function SettingsSideBar(props: SettingsSideBarProps) {
<Input
id="search"
name="search"
placeholder="Search"
placeholder={strings.search()}
data-test-id="settings-search"
sx={{
m: 2,

View File

@@ -23,6 +23,7 @@ import { ThemePreview } from "../components/theme-preview";
import { Flex, Link, Text } from "@theme-ui/components";
import { useStore as useThemeStore } from "../stores/theme-store";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type ThemeDetailsDialogProps = BaseDialogProps<boolean> & {
theme: ThemeMetadata;
@@ -65,11 +66,11 @@ export const ThemeDetailsDialog = DialogManager.register(
</Text>
{theme.totalInstalls && theme.totalInstalls > 0 ? (
<Text variant="subBody" sx={{ fontSize: "subtitle" }}>
{theme.totalInstalls} installs
{theme.totalInstalls} {strings.installs()}
</Text>
) : null}
<Text variant="subBody" sx={{ fontSize: "subtitle" }}>
Licensed under {theme.license}
{strings.licenseUnder()} {theme.license}
</Text>
<Flex sx={{ gap: 1, mt: 1 }}>
{theme.homepage && (

View File

@@ -25,13 +25,14 @@ import { getServiceWorkerVersion } from "./utils/version";
import { register as registerStreamSaver } from "./utils/stream-saver/mitm";
import { ThemeDark, ThemeLight, themeToCSS } from "@notesnook/theme";
import Config from "./utils/config";
import Intl from "@notesnook/intl";
import { $en, setI18nGlobal, $de } from "@notesnook/intl";
import { i18n } from "@lingui/core";
i18n.load({
en: Intl.$en
en: $en,
de: $de
});
i18n.activate("en");
Intl.setI18nGlobal(i18n);
i18n.activate("de");
setI18nGlobal(i18n);
const colorScheme = JSON.parse(
window.localStorage.getItem("colorScheme") || '"light"'

View File

@@ -35,6 +35,7 @@ import { onPageVisibilityChanged } from "../utils/page-visibility";
import { WebAuthn } from "../utils/webauthn";
import { getDocumentTitle, setDocumentTitle } from "../utils/dom";
import { CredentialWithoutSecret, useKeyStore } from "../interfaces/key-store";
import { strings } from "@notesnook/intl";
export default function AppLock(props: PropsWithChildren<unknown>) {
const credentials = useKeyStore((store) => store.activeCredentials());
@@ -144,7 +145,7 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
mt={25}
sx={{ fontSize: 36, textAlign: "center" }}
>
Unlock your notes
{strings.unlockNotes()}
</Text>
</Flex>
<Text
@@ -157,7 +158,7 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
color: "var(--paragraph-secondary)"
}}
>
Please verify it&apos;s you.
{strings.verifyItsYou()}
</Text>
{isUnlocking ? (
@@ -185,7 +186,7 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
autoFocus
required
sx={{ width: ["95%", "95%", "25%"] }}
placeholder="Enter password"
placeholder={strings.enterPassword()}
type="password"
onKeyUp={async (e) => {
if (e.key === "Enter")
@@ -198,7 +199,7 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
sx={{ borderRadius: 100, px: 30 }}
onClick={() => unlockWithPassword(credential)}
>
Continue
{strings.continue()}
</Button>
</>
);
@@ -224,7 +225,7 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
}
}}
>
Unlock with security key
{strings.unlockWithSecurityKey()}
</Button>
);
}

View File

@@ -42,6 +42,7 @@ import { ErrorText } from "../components/error-text";
import { AuthenticatorType, User } from "@notesnook/core";
import { showLogoutConfirmation } from "../dialogs/confirm";
import { TaskManager } from "../common/task-manager";
import { strings } from "@notesnook/intl";
type EmailFormData = {
email: string;
@@ -211,17 +212,17 @@ function LoginEmail(props: BaseAuthComponentProps<"login:email">) {
return (
<AuthForm
type="login:email"
title="Welcome back!"
title={strings.welcomeBack()}
canSkip
subtitle={
<SubtitleWithAction
text="Don't have an account?"
action={{ text: "Sign up", onClick: () => navigate("signup") }}
text={strings.dontHaveAccount()}
action={{ text: strings.signUp(), onClick: () => navigate("signup") }}
/>
}
loading={{
title: "Verifying your email",
subtitle: "Please wait while you are authenticated."
title: strings.verifyingEmail(),
subtitle: strings.authWait()
}}
onSubmit={async (form) => {
const { primaryMethod, phoneNumber, secondaryMethod } =
@@ -246,10 +247,7 @@ function LoginEmail(props: BaseAuthComponentProps<"login:email">) {
>
<Warn size={16} color="icon-error" />
<Text variant="body" ml={1}>
You are logging into the beta version of Notesnook. Switching
between beta &amp; stable versions can cause weird issues
including data loss. It is recommended that you do not use both
simultaneously.
{strings.betaLoginNotice()}
</Text>
</Flex>
) : null}
@@ -257,11 +255,11 @@ function LoginEmail(props: BaseAuthComponentProps<"login:email">) {
id="email"
type="email"
autoComplete="email"
label="Enter email"
label={strings.enterEmailAddress()}
autoFocus
defaultValue={form?.email}
/>
<SubmitButton text="Continue" />
<SubmitButton text={strings.continue()} />
</>
)}
</AuthForm>
@@ -279,12 +277,12 @@ function LoginPassword(props: BaseAuthComponentProps<"login:password">) {
return (
<AuthForm
type="login:password"
title="Your account password"
subtitle={"Your password is always hashed before leaving this device."}
title={strings.accountPassword()}
subtitle={strings.accountPassDesc()}
loadForever
loading={{
title: "Logging you in",
subtitle: "Please wait while you are authenticated."
title: strings.loggingIn(),
subtitle: strings.authWait()
}}
onSubmit={async (form) => {
await userstore.login(
@@ -305,7 +303,7 @@ function LoginPassword(props: BaseAuthComponentProps<"login:password">) {
id="password"
type="password"
autoComplete="current-password"
label="Enter password"
label={strings.enterPassword()}
autoFocus
defaultValue={form?.password}
/>
@@ -317,9 +315,9 @@ function LoginPassword(props: BaseAuthComponentProps<"login:password">) {
onClick={() => navigate("recover", { email: formData.email })}
sx={{ color: "paragraph", alignSelf: "end" }}
>
Forgot password?
{strings.forgotPassword()}
</Button>
<SubmitButton text="Login to your account" />
<SubmitButton text={strings.loginToYourAccount()} />
</>
)}
</AuthForm>
@@ -332,21 +330,24 @@ function Signup(props: BaseAuthComponentProps<"signup">) {
return (
<AuthForm
type="signup"
title="Create an account"
title={strings.createAccount()}
canSkip
subtitle={
<SubtitleWithAction
text="Already have an account?"
action={{ text: "Log in", onClick: () => navigate("login:email") }}
text={strings.alreadyHaveAccount()}
action={{
text: strings.login(),
onClick: () => navigate("login:email")
}}
/>
}
loading={{
title: "Creating your account",
subtitle: "Please wait while we finalize your account."
title: strings.creatingAccount(),
subtitle: strings.creatingAccountDesc()
}}
onSubmit={async (form) => {
if (form.password !== form["confirm-password"]) {
throw new Error("Passwords do not match.");
throw new Error(strings.passwordNotMatched());
}
await userstore.signup(form);
@@ -359,7 +360,7 @@ function Signup(props: BaseAuthComponentProps<"signup">) {
id="email"
type="email"
autoComplete="email"
label="Enter email"
label={strings.enterEmailAddress()}
autoFocus
defaultValue={form?.email}
/>
@@ -367,41 +368,40 @@ function Signup(props: BaseAuthComponentProps<"signup">) {
id="password"
type="password"
autoComplete="new-password"
label="Set password"
label={strings.newPassword()}
defaultValue={form?.password}
/>
<AuthField
id="confirm-password"
type="password"
autoComplete="new-password"
label="Confirm password"
label={strings.confirmPassword()}
defaultValue={form?.["confirm-password"]}
/>
<SubmitButton text="Create account" />
<SubmitButton text={strings.createAccount()} />
<Text
mt={4}
variant="subBody"
sx={{ fontSize: 13, textAlign: "center" }}
>
By pressing {`"Create account" button, you agree to our`}{" "}
{strings.signupAgreement[0]()}{" "}
<Link
target="_blank"
rel="noreferrer"
href="https://notesnook.com/tos"
sx={{ color: "accent" }}
>
Terms of Service
{strings.signupAgreement[1]()}
</Link>{" "}
&amp;{" "}
{strings.signupAgreement[2]()}{" "}
<Link
rel="noreferrer"
href="https://notesnook.com/privacy"
sx={{ color: "accent" }}
>
Privacy Policy
{strings.signupAgreement[3]()}
</Link>
. You also agree to receiving marketing emails from us which you can
opt-out of from the app settings.
. {strings.signupAgreement[4]()}
</Text>
</>
)}
@@ -428,21 +428,17 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
return (
<AuthForm
type="sessionExpiry"
title="Your session has expired"
title={strings.sessionExpired()}
subtitle={
<Flex bg="shade" p={1} sx={{ borderRadius: "default" }}>
<Text as="span" sx={{ fontSize: "body", color: "accent" }}>
<b>
All your local changes are safe and will be synced after you
login.
</b>{" "}
Please enter your password to continue.
{strings.sessionExpiredDesc(user?.email as string)}
</Text>
</Flex>
}
loading={{
title: "Logging you in",
subtitle: "Please wait while you are authenticated."
title: strings.loggingIn(),
subtitle: strings.pleaseWaitLogin()
}}
onSubmit={async () => {
if (!user) return;
@@ -463,7 +459,7 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
id="email"
type="email"
autoComplete={"false"}
label="Enter email"
label={strings.enterEmailAddress()}
placeholder={user ? maskEmail(user.email) : undefined}
autoFocus
disabled
@@ -476,9 +472,9 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
onClick={() => user && navigate("recover", { email: user.email })}
sx={{ color: "paragraph", alignSelf: "end" }}
>
Forgot password?
{strings.forgotPassword()}
</Button>
<SubmitButton text="Relogin to your account" />
<SubmitButton text={strings.reloginToYourAccount()} />
<Button
type="button"
variant="anchor"
@@ -494,15 +490,15 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
if (await showLogoutConfirmation()) {
await TaskManager.startTask({
type: "modal",
title: "You are being logged out",
title: strings.loggingOut(),
action: () => db.user.logout(true),
subtitle: "Please wait..."
subtitle: strings.loggingOutDesc()
});
openURL("/login");
}
}}
>
Logout permanently
{strings.logout()}
</Button>
</AuthForm>
);
@@ -515,16 +511,19 @@ function AccountRecovery(props: BaseAuthComponentProps<"recover">) {
return (
<AuthForm
type="recover"
title="Recover your account"
title={strings.accountRecovery()}
subtitle={
<SubtitleWithAction
text="Remembered your password?"
action={{ text: "Log in", onClick: () => navigate("login:email") }}
text={strings.rememberedYourPassword()}
action={{
text: strings.login(),
onClick: () => navigate("login:email")
}}
/>
}
loading={{
title: "Sending recovery email",
subtitle: "Please wait while we send you recovery instructions."
title: strings.sendingRecoveryEmail(),
subtitle: strings.sendingRecoveryEmailDesc()
}}
onSubmit={async (form) => {
if (!form.email) {
@@ -538,9 +537,7 @@ function AccountRecovery(props: BaseAuthComponentProps<"recover">) {
window.open(url, "_self");
return;
}
setSuccess(
`Recovery email sent. Please check your inbox (and spam folder) for further instructions.`
);
setSuccess(strings.recoveryEmailSentDesc());
}}
>
{success ? (
@@ -551,7 +548,7 @@ function AccountRecovery(props: BaseAuthComponentProps<"recover">) {
{success}
</Text>
</Flex>
<SubmitButton text="Send again" />
<SubmitButton text={strings.confirmEmail()} />
</>
) : (
<>
@@ -559,12 +556,12 @@ function AccountRecovery(props: BaseAuthComponentProps<"recover">) {
id="email"
type="email"
autoComplete={"email"}
label="Enter your account email"
helpText="You will receive instructions on how to recover your account on this email"
label={strings.enterEmailAddress()}
helpText={strings.accountRecoverHelpText()}
defaultValue={formData ? formData.email : ""}
autoFocus
/>
<SubmitButton text="Send recovery email" />
<SubmitButton text={strings.sendRecoveryEmail()} />
</>
)}
</AuthForm>
@@ -656,14 +653,14 @@ function MFACode(props: BaseAuthComponentProps<"mfa:code">) {
return (
<AuthForm
type="mfa:code"
title="Two-factor authentication"
title={strings["2fa"]()}
subtitle={texts.subtitle}
loading={{
title: "Verifying 2FA code",
subtitle: "Please wait while you are authenticated."
title: strings.verifying2faCode(),
subtitle: strings.authWait()
}}
onSubmit={async (form) => {
if (!form.code) throw new Error("2FA code is required.");
if (!form.code) throw new Error(strings.coreRequired());
const loginForm: MFALoginFormData = {
code: form.code,
@@ -695,9 +692,9 @@ function MFACode(props: BaseAuthComponentProps<"mfa:code">) {
{isSending ? (
<Loading size={18} />
) : enabled ? (
`Resend code`
strings.resendCode()
) : (
`Resend in ${elapsed}`
strings.resendCode(elapsed)
)}
</Text>
),
@@ -708,7 +705,7 @@ function MFACode(props: BaseAuthComponentProps<"mfa:code">) {
: undefined
}
/>
<SubmitButton text="Submit" />
<SubmitButton text={strings.submit()} />
<Button
type="button"
mt={4}
@@ -755,11 +752,11 @@ function MFASelector(props: BaseAuthComponentProps<"mfa:select">) {
return (
<AuthForm
type="mfa:select"
title="Select two-factor authentication method"
subtitle={`Where should we send you the authentication code?`}
title={strings.select2faMethod()}
subtitle={strings.select2faCodeHelpText()}
loading={{
title: "Logging you in",
subtitle: "Please wait while you are authenticated."
title: strings.loggingIn(),
subtitle: strings.authWait()
}}
onSubmit={async () => {
const selectedType = MFAMethods[selected];
@@ -907,7 +904,7 @@ export function AuthForm<T extends AuthRoutes>(props: AuthFormProps<T>) {
openURL("/notes/");
}}
>
Skip & go directly to the app
{strings.skipAndGoToApp()}
</Button>
)}

View File

@@ -22,6 +22,7 @@ import { useEffect } from "react";
import { Flex, Text } from "@theme-ui/components";
import { useQueryParams } from "../navigation";
import { MailCheck, Discord, Twitter, Reddit } from "../components/icons";
import { strings } from "@notesnook/intl";
function EmailConfirmed() {
const [{ userId }] = useQueryParams();
@@ -73,7 +74,7 @@ function EmailConfirmed() {
color: "icon"
}}
>
Your email has been confirmed.
{strings.emailConfirmed()}
</Text>
<Text
variant="body"
@@ -85,7 +86,7 @@ function EmailConfirmed() {
color: "var(--paragraph-secondary)"
}}
>
Thank you for choosing end-to-end encrypted note taking.
{strings.confirmEmailThankyou()}
</Text>
</Flex>
<Flex
@@ -130,7 +131,7 @@ function BlogPromoBanner() {
}}
>
<Text variant="heading" sx={{ fontSize: "1.2em", textAlign: "center" }}>
Share Notesnook with your friends!
{strings.shareWithFriends()}
</Text>
<Text
variant="body"
@@ -142,7 +143,7 @@ function BlogPromoBanner() {
color: "paragraph"
}}
>
{`Because where's the fun in nookin' alone?`}
{strings.shareWithFriendsDesc()}
</Text>
<Flex mt={5}>
{social.map((account) => (
@@ -166,11 +167,11 @@ function BlogPromoBanner() {
color: "paragraph"
}}
>
Use{" "}
{strings.tagPromoWinText()[0]}{" "}
<Text as="span" sx={{ fontWeight: "bold", color: "accent" }}>
#notesnook
{strings.tagPromoWinText()[1]}
</Text>{" "}
and get a chance to win free promo codes.
{strings.tagPromoWinText()[2]}
</Text>
{/* <p>
Listen. We want you to buy Notesnook Pro. It's as simple as that. Since

View File

@@ -63,6 +63,7 @@ import {
ImperativePanelHandle
} from "react-resizable-panels";
import { AddNotebookDialog } from "../dialogs/add-notebook-dialog";
import { strings } from "@notesnook/intl";
type NotebookProps = {
rootId: string;
@@ -231,7 +232,7 @@ function SubNotebooks({
<Flex sx={{ alignItems: "center" }}>
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
<Text variant="subBody" sx={{ fontSize: 11 }}>
NOTEBOOKS
{strings.notebooksAllCaps()}
</Text>
</Flex>
<Flex sx={{ alignItems: "center" }}>
@@ -470,7 +471,7 @@ function NotebookHeader({
variant="icon"
sx={{ p: 0, flexShrink: 0 }}
onClick={() => navigateCrumb("notebooks")}
title="Notebooks"
title={strings.notebooks()}
>
<Notebook2 size={14} />
</Button>
@@ -576,7 +577,7 @@ function NotebookHeader({
variant="secondary"
sx={{ borderRadius: 100, width: 30, height: 30 }}
p={0}
title="Edit notebook"
title={strings.editNotebook()}
onClick={() => hashNavigate(`/notebooks/${notebook.id}/edit`)}
>
<Edit size={16} />
@@ -591,7 +592,7 @@ function NotebookHeader({
)}
<Text as="em" variant="subBody" mt={2}>
{/* {pluralize(topics.length, "topic")}, */}
{pluralize(totalNotes, "note")}
{strings.notes(totalNotes || 0)}
</Text>
</Flex>
);

View File

@@ -30,6 +30,7 @@ import Config from "../utils/config";
import { ErrorText } from "../components/error-text";
import { EVENTS, User } from "@notesnook/core";
import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog";
import { strings } from "@notesnook/intl";
type RecoveryMethodType = "key" | "backup" | "reset";
type RecoveryMethodsFormData = Record<string, unknown>;
@@ -173,8 +174,8 @@ function Recovery(props: RecoveryProps) {
>
{isAuthenticating ? (
<Loader
title="Authenticating user"
text={"Please wait while you are authenticated."}
title={strings.authenticatingUser()}
text={strings.authWait()}
/>
) : (
<>
@@ -190,7 +191,7 @@ function Recovery(props: RecoveryProps) {
}}
variant={"body"}
>
Authenticated as {user?.email}
{strings.authenticatedAs()} {user?.email}
</Text>
<Button
sx={{
@@ -203,7 +204,7 @@ function Recovery(props: RecoveryProps) {
variant={"secondary"}
onClick={() => openURL("/login")}
>
Remembered your password?
{strings.rememberedYourPassword()}
</Button>
</Flex>
{Route && (
@@ -225,39 +226,16 @@ export default Recovery;
type RecoveryMethod = {
type: RecoveryMethodType;
title: string;
title: () => string;
testId: string;
description: string;
description: () => string;
isDangerous?: boolean;
};
const recoveryMethods: RecoveryMethod[] = [
{
type: "key",
testId: "step-recovery-key",
title: "Use recovery key",
description:
"Your data recovery key is basically a hashed version of your password (plus some random salt). It can be used to decrypt your data for re-encryption."
},
{
type: "backup",
testId: "step-backup",
title: "Use a backup file",
description:
"If you don't have a recovery key, you can recover your data by restoring a Notesnook data backup file (.nnbackup)."
},
{
type: "reset",
testId: "step-reset-account",
title: "Clear data & reset account",
description:
"EXTREMELY DANGEROUS! This action is irreversible. All your data including notes, notebooks, attachments & settings will be deleted. This is a full account reset. Proceed with caution.",
isDangerous: true
}
];
function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
const { navigate } = props;
const [selected, setSelected] = useState(0);
const recoveryMethods = strings.accountRecoveryMethods as RecoveryMethod[];
if (isSessionExpired()) {
navigate("new");
@@ -268,8 +246,8 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
<RecoveryForm
testId="step-recovery-methods"
type="methods"
title="Choose a recovery method"
subtitle="How do you want to recover your account?"
title={strings.chooseRecoveryMethod()}
subtitle={strings.chooseRecoveryMethodDesc()}
onSubmit={async () => {
const selectedMethod = recoveryMethods[selected].type;
navigate(`method:${selectedMethod}`, {
@@ -304,7 +282,7 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
color: method.isDangerous ? "var(--heading-error)" : "heading"
}}
>
{method.title}
{method.title()}
</Text>
<Text
variant={"body"}
@@ -314,7 +292,7 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
: "var(--paragraph-secondary)"
}}
>
{method.description}
{method.description()}
</Text>
</Button>
))}
@@ -341,17 +319,17 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
<RecoveryForm
testId="step-recovery-key"
type="method:key"
title="Recover your account"
subtitle={"Use a data recovery key to reset your account password."}
title={strings.accountRecovery()}
subtitle={strings.accountRecoveryWithKey()}
loading={{
title: `Downloading your data (${progress})`,
subtitle: "Please wait while your data is downloaded & decrypted."
title: `${strings.network.downloading()} (${progress})`,
subtitle: strings.keyRecoveryProgressDesc()
}}
onSubmit={async (form) => {
setProgress(0);
const user = await db.user.getUser();
if (!user) throw new Error("User not authenticated");
if (!user) throw new Error(strings.notLoggedIn());
await db.storage().write(`_uk_@${user.email}@_k`, form.recoveryKey);
await db.sync({ type: "fetch", force: true });
navigate("backup");
@@ -360,12 +338,12 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
<AuthField
id="recoveryKey"
type="password"
label="Enter your data recovery key"
helpText="Your data recovery key will be used to decrypt your data"
label={strings.enterRecoveryKey()}
helpText={strings.enterRecoveryKeyHelp()}
autoComplete="none"
autoFocus
/>
<SubmitButton text="Start account recovery" />
<SubmitButton text={strings.startAccountRecovery()} />
<Button
type="button"
@@ -374,7 +352,7 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
onClick={() => navigate("methods")}
sx={{ color: "paragraph" }}
>
{`Don't have your recovery key?`}
{strings.dontHaveRecoveryKey()}
</Button>
</RecoveryForm>
);
@@ -396,11 +374,11 @@ function BackupFileMethod(props: BaseRecoveryComponentProps<"method:backup">) {
<RecoveryForm
testId="step-backup-file"
type="method:backup"
title="Recover your account"
title={strings.accountRecovery()}
subtitle={
<ErrorText
sx={{ fontSize: "body" }}
error="All the data in your account will be overwritten with the data in the backup file. There is no way to reverse this action."
error={strings.backupFileRecoveryError()}
/>
}
onSubmit={async () => {
@@ -410,19 +388,19 @@ function BackupFileMethod(props: BaseRecoveryComponentProps<"method:backup">) {
<AuthField
id="backupFile"
type="text"
label="Select backup file"
helpText="Backup files have .nnbackup extension"
label={strings.selectBackupFile()}
helpText={strings.backupFileHelpText()}
autoComplete="none"
autoFocus
disabled
action={{
component: <Text variant={"body"}>Browse</Text>,
component: <Text variant={"body"}>{strings.browse()}</Text>,
onClick: async () => {
setBackupFile(await selectBackupFile());
}
}}
/>
<SubmitButton text="Start account recovery" />
<SubmitButton text={strings.startAccountRecovery()} />
<Button
type="button"
@@ -431,7 +409,7 @@ function BackupFileMethod(props: BaseRecoveryComponentProps<"method:backup">) {
onClick={() => navigate("methods")}
sx={{ color: "paragraph" }}
>
{`Don't have a backup file?`}
{strings.dontHaveBackupFile()}
</Button>
</RecoveryForm>
);
@@ -444,21 +422,18 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
<RecoveryForm
testId="step-backup-data"
type="backup"
title="Backup your data"
subtitle={
"Please download a backup of your data as your account will be cleared before recovery."
}
title={strings.backupYourData()}
subtitle={strings.backupYourDataDesc()}
loading={{
title: "Creating backup...",
subtitle:
"Please wait while we create a backup file for you to download."
title: strings.backingUpData() + "...",
subtitle: strings.backingUpDataWait()
}}
onSubmit={async () => {
await createBackup({ rescueMode: true, mode: "full" });
navigate("new");
}}
>
<SubmitButton text="Download backup file" />
<SubmitButton text={strings.downloadBackupFile()} />
</RecoveryForm>
);
}
@@ -480,13 +455,11 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
<RecoveryForm
testId="step-new-password"
type="new"
title="Reset account password"
subtitle={
"Notesnook is E2E encrypted — your password never leaves this device."
}
title={strings.resetAccountPassword()}
subtitle={strings.accountPassDesc()}
loading={{
title: `Resetting account password (${progress})`,
subtitle: "Please wait while we reset your account password."
title: `${strings.resettingAccountPassword()} (${progress})`,
subtitle: strings.resetPasswordWait()
}}
onSubmit={async (form) => {
setProgress(0);
@@ -514,18 +487,18 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
id="password"
type="password"
autoComplete="current-password"
label="Set new password"
helpText="Your account password must be strong & unique."
label={strings.newPassword()}
helpText={strings.newPasswordHelp()}
defaultValue={form?.password}
/>
<AuthField
id="confirmPassword"
type="password"
autoComplete="confirm-password"
label="Confirm new password"
label={strings.confirmPassword()}
defaultValue={form?.confirmPassword}
/>
<SubmitButton text="Continue" />
<SubmitButton text={strings.continue()} />
</>
)}
</RecoveryForm>
@@ -553,15 +526,15 @@ function Final(_props: BaseRecoveryComponentProps<"final">) {
<RecoveryForm
testId="step-finished"
type="final"
title="Recovery successful!"
subtitle={"Your account has been recovered."}
title={strings.recoverySuccess()}
subtitle={strings.recoverySuccessDesc()}
onSubmit={async () => {
openURL(isSessionExpired() ? "/sessionexpired" : "/login");
}}
>
<SubmitButton
text={
isSessionExpired() ? "Continue with login" : "Login to your account"
isSessionExpired() ? strings.continue() : strings.loginToYourAccount()
}
/>
</RecoveryForm>

245
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4457,7 +4457,7 @@
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"devOptional": true
"dev": true
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4481,7 +4481,7 @@
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"devOptional": true,
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4516,7 +4516,7 @@
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
@@ -9801,7 +9801,7 @@
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"devOptional": true,
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -17897,20 +17897,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff