mobile: localize

This commit is contained in:
Ammar Ahmed
2024-08-15 15:39:53 +05:00
committed by Abdullah Atta
parent dfe7b2e4a6
commit 014866c330
35 changed files with 11046 additions and 798 deletions

View File

@@ -33,11 +33,11 @@ import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
export async function setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key)
throw new Error("Database setup failed, could not get database key");
if (!key) throw new Error(strings.databaseSetupFailed());
console.log("Opening database with key:", !!key);

View File

@@ -24,6 +24,7 @@ import type {
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location: string };
@@ -97,7 +98,7 @@ class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
throw new Error(strings.streamingNotSupported());
}
async executeQuery<R>(

View File

@@ -58,7 +58,7 @@ export const ForgotPassword = () => {
lastRecoveryEmailTime &&
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
) {
throw new Error("Please wait before requesting another email");
throw new Error(strings.pleaseWaitBeforeSendEmail());
}
await db.user.recoverAccount(email.current.toLowerCase());
SettingsService.set({

View File

@@ -98,7 +98,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
}
}, mfaInfo);
} else {
finishWithError(new Error("Unable to send 2FA code"));
finishWithError(new Error(strings.unableToSend2faCode()));
}
break;
}
@@ -132,7 +132,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishLogin = async () => {
const user = await db.user.getUser();
if (!user) throw new Error("Email or password incorrect!");
if (!user) throw new Error(strings.emailOrPasswordIncorrect());
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();

View File

@@ -155,7 +155,7 @@ const Intro = ({ navigation }) => {
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData()}
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,

View File

@@ -48,6 +48,7 @@ import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
import { strings } from "@notesnook/intl";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
@@ -516,7 +517,7 @@ export const PricingPlans = ({
setBuying(true);
try {
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
throw new Error(strings.errorApplyingPromoCode());
ToastManager.show({
heading: "Discount applied!",
type: "success",

View File

@@ -69,7 +69,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
const verified = await db.user?.verifyPassword(
emailChangeData.current.password
);
if (!verified) throw new Error("Password is incorrect");
if (!verified) throw new Error(strings.passwordIncorrect());
await db.user?.sendVerificationEmail(emailChangeData.current.email);
setStep(EmailChangeSteps.changeEmail);
setLoading(false);

View File

@@ -169,9 +169,7 @@ export default function ReminderSheet({
async function saveReminder() {
try {
if (!(await Notifications.checkAndRequestPermissions(true)))
throw new Error(
"App does not have permission to schedule notifications"
);
throw new Error(strings.noNotificationPermission());
if (!date && reminderMode !== ReminderModes.Permanent) return;
if (
reminderMode === ReminderModes.Repeat &&
@@ -179,12 +177,12 @@ export default function ReminderSheet({
recurringMode !== "year" &&
selectedDays.length === 0
)
throw new Error("Please select the day to repeat the reminder on");
throw new Error(strings.selectDayError());
if (!title.current) throw new Error("Please set title of the reminder");
if (!title.current) throw new Error(strings.setTitleError());
if (date.getTime() < Date.now() && reminderMode === "once") {
titleRef?.current?.focus();
throw new Error("Reminder date must be set in future");
throw new Error(strings.dateError());
}
date.setSeconds(0, 0);

View File

@@ -87,7 +87,13 @@ export const SideMenu = React.memo(
return (
<MenuItem
key={item.name}
item={item}
item={{
...item,
title:
strings.routes[
item.name as keyof typeof strings.routes
]?.() || item.name
}}
testID={item.name}
index={index}
/>

View File

@@ -113,10 +113,10 @@ function _MenuItem({
/>
{isFocused ? (
<Heading color={colors.selected.heading} size={SIZE.md}>
{item.name}
{item.title || item.name}
</Heading>
) : (
<Paragraph size={SIZE.md}>{item.name}</Paragraph>
<Paragraph size={SIZE.md}>{item.title || item.name}</Paragraph>
)}
{item.isBeta ? (

View File

@@ -60,6 +60,7 @@ import {
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const style: ViewStyle = {
height: "100%",
@@ -146,6 +147,10 @@ const Editor = React.memo(
nestedScrollEnabled
onError={onError}
injectedJavaScriptBeforeContentLoaded={`
globalThis.LINGUI_LOCALE = "${i18n.locale}";
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
[i18n.locale]: i18n.messages
})};
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar};
@@ -269,7 +274,7 @@ const useLockedNoteHandler = () => {
if (enrollBiometrics && note) {
try {
const unlocked = await db.vault.unlock(password);
if (!unlocked) throw new Error("Incorrect vault password");
if (!unlocked) throw new Error(strings.passwordIncorrect());
await BiometricService.storeCredentials(password);
eSendEvent("vaultUpdated");
ToastManager.show({

View File

@@ -52,12 +52,12 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<SelectionHeader id={route.name} items={notes} type="note" />
<Header
renderedInRoute={route.name}
title={strings.routes[route.name]()}
title={strings.routes[route.name as keyof typeof strings.routes]()}
canGoBack={false}
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name?.toLowerCase()}`,
placeholder: strings.searchInRoute(route.name),
type: "note",
title: route.name,
route: route.name
@@ -72,7 +72,9 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
dataType="note"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={strings.routes[route.name]?.()}
headerTitle={strings.routes[
route.name as keyof typeof strings.routes
]?.()}
placeholder={{
title: route.name?.toLowerCase(),
paragraph: strings.notesEmpty(),

View File

@@ -57,7 +57,7 @@ export const HomePicker = createSettingsPicker({
});
},
formatValue: (item) => {
return typeof item === "object" ? item.name : item;
return strings.routes[typeof item === "object" ? item.name : item]();
},
getItemKey: (item) => item.name,
options: MenuItemsList.slice(0, MenuItemsList.length - 1),
@@ -71,7 +71,11 @@ export const TrashIntervalPicker = createSettingsPicker({
db.settings.setTrashCleanupInterval(item);
},
formatValue: (item) => {
return item === -1 ? "Never" : item === 1 ? "Daily" : item + " days";
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: item + " " + strings.days();
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365],
@@ -109,7 +113,7 @@ export const TimeFormatPicker = createSettingsPicker({
});
},
formatValue: (item) => {
return `${item} (${dayjs().format(TimeFormats[item])})`;
return `${strings[item]()} (${dayjs().format(TimeFormats[item])})`;
},
getItemKey: (item) => item,
options: TIME_FORMATS,
@@ -122,9 +126,7 @@ export const BackupReminderPicker = createSettingsPicker({
SettingsService.set({ reminder: item });
},
formatValue: (item) => {
return item === "useroff" || item === "off" || item === "never"
? "Off"
: item.slice(0, 1).toUpperCase() + item.slice(1);
return item === "useroff" ? strings.off() : strings[item]?.();
},
getItemKey: (item) => item,
options: ["useroff", "daily", "weekly", "monthly"],
@@ -173,12 +175,12 @@ export const ApplockTimerPicker = createSettingsPicker({
},
formatValue: (item) => {
return item === -1
? "Never"
? strings.never()
: item === 0 || item === undefined
? "Immediately"
? strings.immediately()
: item === 1
? "1 minute"
: item + " minutes";
? strings.minutes(1)
: strings.minutes(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],

View File

@@ -812,7 +812,7 @@ export const settingsGroups: SettingSection[] = [
},
{
id: "privacy-security",
name: "Privacy and security",
name: strings.privacyAndSecurity(),
sections: [
{
id: "marketing-emails",
@@ -1460,7 +1460,7 @@ export const settingsGroups: SettingSection[] = [
},
{
id: "legal",
name: "legal",
name: strings.legal(),
sections: [
{
id: "tos",

View File

@@ -60,7 +60,7 @@ export const Tags = ({ navigation, route }: NavigationProps<"Tags">) => {
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
placeholder: strings.searchInRoute(route.name),
type: "tag",
title: route.name,
route: route.name

View File

@@ -93,7 +93,7 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
hasSearch={true}
onSearch={() => {
Navigation.push("Search", {
placeholder: `Type a keyword to search in ${route.name}`,
placeholder: strings.searchInRoute(route.name),
type: "trash",
title: route.name,
route: route.name

View File

@@ -171,7 +171,7 @@ async function run(
if (!androidBackupDirectory)
return {
error: new Error("Backup directory not selected"),
error: new Error(strings.backupDirectoryNotSelected()),
report: false
};

View File

@@ -13,11 +13,12 @@ global.Buffer = require('buffer').Buffer;
import '../app/common/logger/index';
import { DOMParser } from './worker.js';
global.DOMParser = DOMParser;
import { $en, setI18nGlobal } from "@notesnook/intl";
import { $en, setI18nGlobal,$de } from "@notesnook/intl";
import { i18n } from "@lingui/core";
i18n.load({
en: $en,
de: $de
});
i18n.activate("en");
i18n.activate("de");
setI18nGlobal(i18n);

View File

@@ -10,9 +10,12 @@
"hasInstallScript": true,
"dependencies": {
"@emotion/react": "11.11.1",
"@lingui/core": "4.11.2",
"@lingui/react": "4.11.2",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.0",
"@notesnook/editor": "file:../editor",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",
"@szhsin/react-menu": "^4.1.0",
"buffer": "^6.0.3",
@@ -126,6 +129,27 @@
"zustand": ">=4"
}
},
"../intl": {
"name": "@notesnook/intl",
"version": "1.0.0",
"devDependencies": {
"@babel/core": "^7.24.9",
"@babel/preset-env": "^7.20.2",
"@lingui/cli": "^4.11.2",
"@lingui/macro": "^4.11.2",
"@lingui/swc-plugin": "^4.0.7",
"@rspack/cli": "^0.6.2",
"@rspack/core": "^0.6.2",
"@types/react": "^18.2.39",
"babel-plugin-macros": "^3.1.0",
"react": "18.2.0",
"typescript": "5.5.3"
},
"peerDependencies": {
"@lingui/macro": "*",
"react": ">=18"
}
},
"../theme": {
"name": "@notesnook/theme",
"version": "2.1.3",
@@ -3613,6 +3637,46 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
"node_modules/@lingui/core": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.11.2.tgz",
"integrity": "sha512-5wFmpHeDbLXEqaEUwlayS4SoqrCbDI3/bVRlwhmdNCeUcUYWh+7dTDlQnp4tPek1x1dEppABIkdN/0qLDdKcBQ==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "4.11.2",
"unraw": "^3.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/message-utils": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.11.2.tgz",
"integrity": "sha512-3oJk7ZKExk4NVa4d3CM0z0iNqIokaFOWeu7lYVzu0oEX7DP6OxNjlCAtObIhJCB0FdIPz8sXxhDkyDHFj+eIvw==",
"dependencies": {
"@messageformat/parser": "^5.0.0",
"js-sha256": "^0.10.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/react": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@lingui/react/-/react-4.11.2.tgz",
"integrity": "sha512-OKHCg3yPW2xhYWoY2kOz+eP7qpdkab+4tERUvJ9QJ9bzQ6OnPLCagaRftB3nqdKuWzKoA5F2VG2QLUhF7DjpGA==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/core": "4.11.2"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@mdi/js": {
"version": "7.3.67",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz",
@@ -3626,6 +3690,14 @@
"prop-types": "^15.7.2"
}
},
"node_modules/@messageformat/parser": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz",
"integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==",
"dependencies": {
"moo": "^0.5.1"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3696,6 +3768,10 @@
"resolved": "../editor",
"link": true
},
"node_modules/@notesnook/intl": {
"resolved": "../intl",
"link": true
},
"node_modules/@notesnook/theme": {
"resolved": "../theme",
"link": true
@@ -4381,7 +4457,7 @@
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true
"devOptional": true
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4405,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==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4440,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==",
"dev": true
"devOptional": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
@@ -9725,7 +9801,7 @@
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true,
"devOptional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -12531,6 +12607,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-sha256": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz",
"integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -13200,6 +13281,11 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -17811,6 +17897,20 @@
"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",
@@ -17914,6 +18014,11 @@
"integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
"dev": true
},
"node_modules/unraw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
"integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg=="
},
"node_modules/upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",

View File

@@ -8,6 +8,7 @@
"@mdi/react": "^1.6.0",
"@notesnook/editor": "file:../editor",
"@notesnook/theme": "file:../theme",
"@notesnook/intl": "file:../intl",
"@szhsin/react-menu": "^4.1.0",
"buffer": "^6.0.3",
"framer-motion": "^10.16.8",
@@ -17,7 +18,9 @@
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
"zustand": "^4.4.7",
"@lingui/react": "4.11.2",
"@lingui/core": "4.11.2"
},
"devDependencies": {
"@playwright/test": "^1.37.1",

View File

@@ -51,6 +51,7 @@ import StatusBar from "./statusbar";
import Tags from "./tags";
import TiptapEditorWrapper from "./tiptap";
import Title from "./title";
import { strings } from "@notesnook/intl";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
@@ -196,6 +197,7 @@ const Tiptap = ({
copyToClipboard: (text) => {
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
},
placeholder: strings.startWritingNote(),
onSelectionUpdate: () => {
if (tabRef.current.noteId) {
const noteId = tabRef.current.noteId;
@@ -239,7 +241,7 @@ const Tiptap = ({
const update = useCallback(() => {
setTick((tick) => tick + 1);
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
"Note title"
strings.noteTitle()
);
setTimeout(() => {
editorControllers[tabRef.current.id]?.setLoading(false);
@@ -459,7 +461,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
Your changes could not be saved.
{strings.changesNotSaved()}
</p>
<p
style={{
@@ -472,7 +474,7 @@ const Tiptap = ({
fontSize: "0.9rem"
}}
>
It seems that your changes could not be saved. What to do next:
{strings.changesNotSavedDesc()}
</p>
<p
@@ -484,13 +486,10 @@ const Tiptap = ({
>
<ol>
<li>
<p>
Tap on "Dismiss" and copy the contents of your note so they
are not lost.
</p>
<p>{strings.changesNotSavedStep1()}</p>
</li>
<li>
<p>Restart the app.</p>
<p>{strings.changesNotSavedStep2()}</p>
</li>
</ol>
</p>
@@ -528,7 +527,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
Dismiss
{strings.dismiss()}
</p>
</button>
</div>
@@ -605,7 +604,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
This note is locked.
{strings.thisNoteLocked()}
</p>
<form
@@ -626,7 +625,7 @@ const Tiptap = ({
}}
>
<input
placeholder="Enter password"
placeholder={strings.enterPassword()}
ref={controller.passwordInputRef}
name="password"
type="password"
@@ -671,7 +670,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
Unlock note
{strings.unlockNote()}
</p>
</button>
@@ -705,7 +704,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
Enable biometric unlocking
{strings.vaultEnableBiometrics()}
</p>
</div>
) : null}
@@ -743,7 +742,7 @@ const Tiptap = ({
userSelect: "none"
}}
>
Unlock with biometrics
{strings.unlockWithBiometrics()}
</p>
</button>
) : null}

View File

@@ -21,7 +21,6 @@ import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
import CrownIcon from "mdi-react/CrownIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import DotsVerticalIcon from "mdi-react/DotsVerticalIcon";
import FullscreenIcon from "mdi-react/FullscreenIcon";

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { RefObject, useEffect, useRef, useState } from "react";
import { getTotalWords, Editor } from "@notesnook/editor";
import { useTabContext } from "../hooks/useTabStore";
import { strings } from "@notesnook/intl";
function StatusBar({
container,
@@ -37,20 +38,20 @@ function StatusBar({
const stickyRef = useRef(false);
const prevScroll = useRef(0);
const lastStickyChangeTime = useRef(0);
const [words, setWords] = useState("0 words");
const [words, setWords] = useState(`0 ${strings.words()}`);
const currentWords = useRef(words);
const statusBar = useRef({
set: setStatus,
updateWords: () => {
const editor = editors[tab.id];
if (!editor) return;
const words = getTotalWords(editor as Editor) + " words";
const words = getTotalWords(editor as Editor) + ` ${strings.words()}`;
if (currentWords.current === words) return;
setWords(words);
},
resetWords: () => {
currentWords.current = `0 words`;
setWords(`0 words`);
currentWords.current = `0 ${strings.words()}`;
setWords(`0 ${strings.words()}`);
}
});

View File

@@ -21,6 +21,7 @@ import React, { useEffect, useRef, useState } from "react";
import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css";
import { useTabContext } from "../hooks/useTabStore";
import { strings } from "@notesnook/intl";
function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element {
const [tags, setTags] = useState<
@@ -89,7 +90,7 @@ function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element {
userSelect: "none"
}}
>
Add a tag
{strings.addATag()}
</p>
) : null}
<svg

View File

@@ -17,14 +17,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
global.Buffer = require("buffer").Buffer;
import { i18n } from "@lingui/core";
import "@notesnook/editor/styles/fonts.mobile.css";
import "@notesnook/editor/styles/katex-fonts.mobile.css";
import "@notesnook/editor/styles/katex.min.css";
import "@notesnook/editor/styles/styles.css";
import { $en, setI18nGlobal } from "@notesnook/intl";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
i18n.load({
en: $en,
...globalThis.LINGUI_LOCALE_DATA
});
i18n.activate(globalThis.LINGUI_LOCALE || "en");
setI18nGlobal(i18n);
const rootElement = document.getElementById("root");
if (rootElement) {
const root = createRoot(rootElement);

View File

@@ -54,6 +54,8 @@ export type Settings = {
/* eslint-disable no-var */
declare global {
var LINGUI_LOCALE: string;
var LINGUI_LOCALE_DATA: { [name: string]: any };
var pendingResolvers: {
[key: string]: (value: any) => void;
};

View File

@@ -126,6 +126,8 @@ export type TiptapOptions = EditorOptions &
downloadOptions?: DownloadOptions;
isMobile?: boolean;
doubleSpacedLines?: boolean;
} & {
placeholder: string;
};
const useTiptap = (
@@ -289,7 +291,7 @@ const useTiptap = (
defaultAlignment: "left"
}),
Placeholder.configure({
placeholder: "Start writing your note..."
placeholder: options.placeholder || "Start writing your note..."
}),
ImageNode.configure({ allowBase64: true }),
EmbedNode,

View File

@@ -18,5 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export { messages as $en } from "./locales/$en.json";
export { messages as $de } from "./locales/$de.json";
export { messages as $fr } from "./locales/$fr.json";
export { strings } from "./src/strings";
export { setI18nGlobal } from "./src/setup";

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ["en"],
locales: ["en", "de", "fr"],
sourceLocale: "en",
catalogs: [
{

3826
packages/intl/locale/de.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3826
packages/intl/locale/fr.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -27,12 +27,13 @@ export const setI18nGlobal = (i18n: I18n) => {
export function getI18nGlobal() {
return i18nGlobal;
}
export const i18n = i18nn;
Object.keys(i18nGlobal || i18nn).forEach((key) => {
Object.defineProperty(i18n, key, {
get() {
return i18nGlobal?.[key as keyof I18n] || i18nn[key as keyof I18n];
export const i18n = new Proxy(
{},
{
get: (target, property) => {
return (
i18nGlobal?.[property as keyof I18n] || i18nn[property as keyof I18n]
);
}
});
});
}
);

View File

@@ -228,14 +228,12 @@ export const strings = {
monographPassDesc: () =>
t`Published note can only be viewed by someone with the password.`,
monographSelfDestructHeading: () => t`Self destruct`,
monographSelfDestructDesc:
() => t`Published note link will be automatically deleted once it is
viewed by someone.`,
monographSelfDestructDesc: () =>
t`Published note link will be automatically deleted once it is viewed by someone.`,
monographLearnMore: () => t`Learn more about Notesnook Monograph`,
rateAppHeading: () => t`Do you enjoy using Notesnook?`,
rateAppDesc:
() => t`It took us a year to bring Notesnook to life. Share your experience
and suggestions to help us improve it.`,
rateAppDesc: () =>
t`It took us a year to bring Notesnook to life. Share your experience and suggestions to help us improve it.`,
recoveryKeySavedConfirmation: () =>
t`Tap twice to confirm you have saved the recovery key.`,
noBlocksLinked: () => t`No blocks linked`,
@@ -1358,25 +1356,74 @@ For example:
errorGettingCodes: () => t`Error getting codes`,
noResultsFound: () => t`No results found for`,
routes: {
Notes: () => `Notes`,
Notebooks: () => `Notebooks`,
Notebook: () => `Notebook`,
Favorites: () => `Favorites`,
Reminders: () => `Reminders`,
Trash: () => `Trash`,
Settings: () => `Settings`,
Tags: () => `Tags`,
Editor: () => `Editor`,
Home: () => `Home`,
Search: () => `Search`
Notes: () => t`Notes`,
Notebooks: () => t`Notebooks`,
Notebook: () => t`Notebook`,
Favorites: () => t`Favorites`,
Reminders: () => t`Reminders`,
Trash: () => t`Trash`,
Settings: () => t`Settings`,
Tags: () => t`Tags`,
Editor: () => t`Editor`,
Home: () => t`Home`,
Search: () => t`Search`,
Monographs: () => t`Monographs`
},
searchInRoute: (routeName: string) =>
`Type a keyword to search in ${
t`Type a keyword to search in ${
strings.routes[routeName as keyof typeof strings.routes]?.() || routeName
}`,
logoutConfirmation: () =>
t`Are you sure you want to logout and clear all data stored on this device?`,
backupDataBeforeLogout: () => t`Take a backup before logging out`,
unsyncedChangesWarning: () =>
t`You have unsynced notes. Take a backup or sync your notes to avoid losing your critical data.`
t`You have unsynced notes. Take a backup or sync your notes to avoid losing your critical data.`,
databaseSetupFailed: () =>
t`Database setup failed, could not get database key`,
streamingNotSupported: () => t`Streaming not supported`,
unableToResolveDownloadUrl: () => t`Unable to resolve download url`,
pleaseWaitBeforeSendEmail: () =>
t`Please wait before requesting another email`,
unableToSend2faCode: () => t`Unable to send 2FA code`,
emailOrPasswordIncorrect: () => t`Email or password incorrect`,
errorApplyingPromoCode: () => t`Error applying promo code`,
noNotificationPermission: () =>
t`"App does not have permission to schedule notifications"`,
selectDayError: () => t`Please select the day to repeat the reminder on`,
setTitleError: () => t`Please set title of the reminder`,
dateError: () => t`Reminder date must be set in future`,
failedToDecryptBackup: () => t`Failed to decrypt backup`,
backupDirectoryNotSelected: () => t`"Backup directory not selected"`,
legal: () => t`legal`,
days: () => t`days`,
daily: () => t`Daily`,
weekly: () => t`Weekly`,
monthly: () => t`Monthly`,
yearly: () => t`Yearly`,
minutes: (count: number) =>
plural(count, {
one: `1 minute`,
other: `# minutes`
}),
hours: (count: number) =>
plural(count, {
one: `1 hour`,
other: `# hours`
}),
immediately: () => t`Immediately`,
"12-hour": () => t`12-hour`,
"24-hour": () => t`24-hour`,
noteTitle: () => t`Note title`,
changesNotSaved: () => t`Your changes could not be saved`,
changesNotSavedDesc: () =>
t`It seems that your changes could not be saved. What to do next:`,
changesNotSavedStep1: () =>
t`Tap on "Dismiss" and copy the contents of your note so they are not lost.`,
changesNotSavedStep2: () => t`Restart the app.`,
thisNoteLocked: () => `This note is locked`,
dismiss: () => t`Dismiss`,
words: () => t`words`,
addATag: () => t`Add a tag`,
startWritingNote: () => t`Start writing your note...`,
off: () => t`Off`
};

File diff suppressed because it is too large Load Diff