feat: localisation

This commit is contained in:
Artemy 2023-04-04 15:15:15 +03:00
parent e2708f1985
commit 8fb91d7ecb
15 changed files with 208 additions and 64 deletions

View file

@ -12,6 +12,8 @@ import PubNoteSafe from "./pages/pubNoteSafe";
import RenderMarkdown from "./components/markdown"; import RenderMarkdown from "./components/markdown";
import socket from "./components/socket"; import socket from "./components/socket";
import Settings from "./pages/settings"; import Settings from "./pages/settings";
import Locales from "./localisation/main";
import { useState } from "react";
function App() { function App() {
Storage.prototype.setObj = function (key, obj) { Storage.prototype.setObj = function (key, obj) {
@ -21,10 +23,24 @@ function App() {
return JSON.parse(this.getItem(key)) || {}; return JSON.parse(this.getItem(key)) || {};
}; };
const [key, setKey] = useState(Math.random());
window.settings = localStorage.getObj("settings") || {}; window.settings = localStorage.getObj("settings") || {};
window.locals =
Locales[window.settings.language] ||
Locales[navigator.language] ||
Locales[navigator.userLanguage] ||
Locales.en;
window.addEventListener("reRenderPage", () => {
setKey(Math.random());
});
return ( return (
<div className="grid grid-cols-4 lg:grid-cols-5 gap-10 text-black dark:text-white"> <div
className="grid grid-cols-4 lg:grid-cols-5 gap-10 text-black dark:text-white"
key={key}
>
<Menu /> <Menu />
<div className="col-span-4 p-5 m-4 rounded-2xl"> <div className="col-span-4 p-5 m-4 rounded-2xl">
<Routes> <Routes>

View file

@ -11,14 +11,18 @@ function Menu() {
<div className="grid grid-cols-1 col-span-4 lg:col-span-1 gap-2 m-4 place-content-start justify-self-center justify-center"> <div className="grid grid-cols-1 col-span-4 lg:col-span-1 gap-2 m-4 place-content-start justify-self-center justify-center">
<ButtonWithIcon <ButtonWithIcon
icon={MagnifyingGlassCircleIcon} icon={MagnifyingGlassCircleIcon}
text="Заметки" text={locals.Notes}
href="/notes" href="/notes"
/> />
<ButtonWithIcon icon={PencilIcon} text="Написать" href="/" /> <ButtonWithIcon icon={PencilIcon} text={locals.Write} href="/" />
<ButtonWithIcon icon={Cog6ToothIcon} text="Настройки" href="/settings" /> <ButtonWithIcon
icon={Cog6ToothIcon}
text={locals.Settings}
href="/settings"
/>
<ButtonWithIcon <ButtonWithIcon
icon={ExclamationCircleIcon} icon={ExclamationCircleIcon}
text="Подробнее" text={locals.About}
href="/about" href="/about"
/> />
</div> </div>

View file

@ -1,5 +1,5 @@
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import printDate from "./utils"; import { printDate } from "./utils";
function Note({ note }) { function Note({ note }) {
return ( return (

View file

@ -13,4 +13,8 @@ function printDate(time) {
return dateStr; return dateStr;
} }
export default printDate; function reRenderPage() {
window.dispatchEvent(new Event("reRenderPage"));
}
export { printDate, reRenderPage };

46
src/localisation/en.js Normal file
View file

@ -0,0 +1,46 @@
let en = {
Notes: "Notes",
Write: "Write",
Chat: "Chat",
Settings: "Settings",
About: "About",
Name: "Name",
UserName: "Username",
User: "User",
PhotoUrl: "Photo URL",
Url: "URL",
Status: "Status",
UserStatus: "User status",
EditPreview: "Editing in preview",
EditPreviewWarn:
"Can cause irreversible text changes, such as breaking code tags",
PublicNote: "Public note",
PublicNoteTitle: "If enabled, note will be visible to all users",
Interface: "Interface",
Language: "Language",
ThirdPartyApi: "Third party API",
OpenAiKey: "OpenAI API key",
Key: "Key",
Preview: "Preview",
NotePlaceholder:
"Your note starts here. You can use markdown, MathJax and GFM.",
NoteName: "Note name",
Publish: "Publish",
Save: "Save",
WriteNote: "Write note",
PubError: "Error in publishing note",
PubErrorMsg: "Note was not published due to an unknown error",
PubErrorMsgNoName: "Note was not published, because it does not have a name.",
PubErrorMsgNoText: "Note was not published, because it does not have a text.",
Back: "Back",
PubNoteNotExist: "This note does not exist",
NoteNotExist: "This note does not exist",
Idontexists: "I don't exist",
PubUrlPlaceholder:
"The link to send a public note. When you click this link, the note will disappear from the server and be saved locally.",
Delete: "Delete",
Open: "Open",
NoNotesYet: "No notes yet",
};
export default en;

22
src/localisation/main.js Normal file
View file

@ -0,0 +1,22 @@
import ru from "./ru";
import en from "./en";
let Locales = {
ru,
en,
"en-US": en,
"en-AU": en,
"en-BZ": en,
"en-CA": en,
"en-IE": en,
"en-JM": en,
"en-NZ": en,
"en-ZA": en,
"en-TT": en,
"en-GB": en,
"en-US": en,
"ru-RU": ru,
};
export default Locales;

47
src/localisation/ru.js Normal file
View file

@ -0,0 +1,47 @@
let ru = {
Notes: "Заметки",
Write: "Написать",
Chat: "Чат",
Settings: "Настройки",
About: "Подробнее",
Name: "Имя",
UserName: "Имя пользователя",
User: "Пользователь",
PhotoUrl: "Ссылка на фото",
Url: "Ссылка",
Status: "Статус",
UserStatus: "Статус пользователя",
EditPreview: "Редактирование в предпросмотре",
EditPreviewWarn:
"Может вызывать необратимые изменения текста, например ломает теги кода",
PublicNote: "Публичная заметка",
PublicNoteTitle: "Если включено, то заметка будет видна всем пользователям",
Interface: "Интерфейс",
Language: "Язык",
ThirdPartyApi: "Сторонний API",
OpenAiKey: "OpenAI API ключ",
Key: "Ключ",
Preview: "Предпросмотр",
NotePlaceholder:
"Ваша заметка начинается здесь. Вы можете использовать markdown, MathJax и GFM.",
NoteName: "Название заметки",
Publish: "Опубликовать",
Save: "Сохранить",
WriteNote: "Написать заметку",
PubError: "Ошибка в публикации заметки",
PubErrorMsg: "Заметка не была опубликована из-за неизвестной ошибки",
PubErrorMsgNoName:
"Заметка не была опубликована, так как отсутствует название.",
PubErrorMsgNoText: "Заметка не была опубликована, так как отсутствует текст.",
Back: "Назад",
PubNoteNotExist: "Такой публичной заметки не существует",
NoteNotExist: "Заметки не существует",
Idontexists: "Меня не существует",
PubUrlPlaceholder:
"Ссылка для отправки публичной заметки. При переходе на эту ссылку, заметка исчезнет с сервера и будет сохранена локально.",
Delete: "Удалить",
Open: "Открыть",
NoNotesYet: "Заметок пока нет",
};
export default ru;

View file

@ -3,7 +3,7 @@ import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { CheckBox } from "../components/checkbox"; import { CheckBox } from "../components/checkbox";
import { useState } from "react"; import { useState } from "react";
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import printDate from "../components/utils"; import { printDate } from "../components/utils";
import rehypeRemark from "rehype-remark/lib"; import rehypeRemark from "rehype-remark/lib";
import ContentEditable from "react-contenteditable"; import ContentEditable from "react-contenteditable";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
@ -41,11 +41,11 @@ function CreateNote() {
<div> <div>
<div className="grid grid-cols-1 lg:grid-cols-2"> <div className="grid grid-cols-1 lg:grid-cols-2">
<h2 className="text-center lg:text-left leading-tight text-2xl font-bold"> <h2 className="text-center lg:text-left leading-tight text-2xl font-bold">
{`${preview ? "" : "Написать заметку"}`} {`${preview ? "" : locals.WriteNote}`}
</h2> </h2>
<CheckBox <CheckBox
className="justify-self-center lg:justify-self-end" className="justify-self-center lg:justify-self-end"
label="Предпросмотр" label={locals.Preview}
id="preview" id="preview"
onClick={(val) => { onClick={(val) => {
setText(localStorage.getItem("NoteText")); setText(localStorage.getItem("NoteText"));
@ -60,7 +60,7 @@ function CreateNote() {
className={`mb-2 md:w-1/6 w-full ${inputStyle} ${ className={`mb-2 md:w-1/6 w-full ${inputStyle} ${
preview ? "hidden" : "" preview ? "hidden" : ""
}`} }`}
placeholder="Название заметки..." placeholder={locals.NoteName}
maxLength={64} maxLength={64}
value={localStorage.getItem("NoteName") || ""} value={localStorage.getItem("NoteName") || ""}
onChange={(e) => { onChange={(e) => {
@ -75,7 +75,7 @@ function CreateNote() {
${preview ? "hidden" : ""} ${preview ? "hidden" : ""}
`} `}
rows="10" rows="10"
placeholder="Ваша заметка начинается здесь. Можно использовать markdown..." placeholder={locals.NotePlaceholder}
maxLength={5000} maxLength={5000}
onChange={(e) => { onChange={(e) => {
setText(e.target.value); setText(e.target.value);
@ -108,8 +108,8 @@ function CreateNote() {
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-center w-full"> <div className="grid grid-cols-1 lg:grid-cols-2 justify-items-center w-full">
<SettingsCheckBox <SettingsCheckBox
label="Публичная заметка" label={locals.PublicNote}
title="Если включено, то заметка будет видна всем пользователям" title={locals.PublicNoteTitle}
checked={settings.publicNote} checked={settings.publicNote}
settingName="publicNote" settingName="publicNote"
className="justify-self-center lg:justify-self-start" className="justify-self-center lg:justify-self-start"
@ -120,7 +120,7 @@ function CreateNote() {
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
<ButtonWithIcon <ButtonWithIcon
icon={ChevronDoubleRightIcon} icon={ChevronDoubleRightIcon}
text={publicState ? "Опубликовать" : "Сохранить"} text={publicState ? locals.Publish : locals.Save}
reverse={true} reverse={true}
href={publicState ? "/notes/publish" : "/notes/save-local"} href={publicState ? "/notes/publish" : "/notes/save-local"}
className="m-1" className="m-1"

View file

@ -14,17 +14,17 @@ function NotePage() {
icon={ChevronDoubleLeftIcon} icon={ChevronDoubleLeftIcon}
className="mb-4" className="mb-4"
href="/notes" href="/notes"
text="Заметки" text={locals.Notes}
/> />
{note ? <Note note={note} /> : <div>Заметки не существует.</div>} {note ? <Note note={note} /> : <div>{locals.NoteNotExists}</div>}
{note && ( {note && (
<div className="grid grid-cols-1"> <div className="grid grid-cols-1">
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
<ButtonWithIcon <ButtonWithIcon
className="mt-4" className="mt-4"
href="/notes" href="/notes"
text="Удалить" text={locals.Delete}
icon={TrashIcon} icon={TrashIcon}
onClick={() => { onClick={() => {
let notesObj = localStorage.getObj("Notes"); let notesObj = localStorage.getObj("Notes");

View file

@ -1,6 +1,6 @@
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import printDate from "../components/utils"; import { printDate } from "../components/utils";
function Notes() { function Notes() {
let notes = Object.entries(localStorage.getObj("Notes")) let notes = Object.entries(localStorage.getObj("Notes"))
@ -23,7 +23,7 @@ function Notes() {
href={`/notes/${val[0]}`} href={`/notes/${val[0]}`}
reverse={true} reverse={true}
icon={ChevronDoubleRightIcon} icon={ChevronDoubleRightIcon}
text="Перейти" text={locals.Open}
/> />
</div> </div>
</div> </div>
@ -34,7 +34,7 @@ function Notes() {
if (notes.length === 0) if (notes.length === 0)
return ( return (
<div className="md"> <div className="md">
<h3>Заметок пока нет</h3> <h3>{locals.NoNotesYet}</h3>
</div> </div>
); );
return notes; return notes;

View file

@ -1,4 +1,4 @@
import printDate from "../components/utils"; import { printDate } from "../components/utils";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline";
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
@ -12,22 +12,20 @@ function PubError() {
<ButtonWithIcon <ButtonWithIcon
className="mb-4" className="mb-4"
href="/" href="/"
text="Вернуться" text={locals.Back}
icon={ChevronDoubleLeftIcon} icon={ChevronDoubleLeftIcon}
/> />
<div className="border border-blue-300 rounded-lg p-4"> <div className="border border-blue-300 rounded-lg p-4">
<div className="grid grid-cols-1 lg:grid-cols-2"> <div className="grid grid-cols-1 lg:grid-cols-2">
<h2 className="font-medium text-center lg:text-left leading-tight text-4xl mt-0 mb-2"> <h2 className="font-medium text-center lg:text-left leading-tight text-4xl mt-0 mb-2">
Ошибка в публикации заметки {locals.PubError}
</h2> </h2>
<div className="justify-self-center lg:justify-self-end"> <div className="justify-self-center lg:justify-self-end">
{printDate(Date.now())} {printDate(Date.now())}
</div> </div>
</div> </div>
<div className="w-full md"> <div className="w-full md">{err ? err : locals.PubErrorMsg}</div>
{err ? err : "Заметка не была опубликована из-за неизвестной ошибки"}
</div>
</div> </div>
</div> </div>
); );

View file

@ -1,7 +1,7 @@
import RenderMarkdown from "../components/markdown"; import RenderMarkdown from "../components/markdown";
import { useState } from "react"; import { useState } from "react";
import { Navigate, useParams } from "react-router-dom"; import { Navigate, useParams } from "react-router-dom";
import printDate from "../components/utils"; import { printDate } from "../components/utils";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/outline";
import { ButtonWithIcon } from "../components/button"; import { ButtonWithIcon } from "../components/button";

View file

@ -9,6 +9,12 @@ function PubNoteSafe() {
let params = useParams(); let params = useParams();
let [note, setNote] = useState(false); let [note, setNote] = useState(false);
let nullNote = {
text: locals.PubNoteNotExist,
name: locals.Idontexists,
time: Date.now(),
code: 0,
};
if (note === false) if (note === false)
fetch(`/get-note/safe/${params.id}`) fetch(`/get-note/safe/${params.id}`)
@ -20,21 +26,11 @@ function PubNoteSafe() {
setNote(data); setNote(data);
}) })
.catch(() => { .catch(() => {
setNote({ setNote(nullNote);
text: "Такой публичной заметки не сущуествует",
name: "Меня не существует",
time: Date.now(),
code: 0,
});
}); });
}) })
.catch(() => { .catch(() => {
setNote({ setNote(nullNote);
text: "Такой публичной заметки не сущуествует",
name: "Меня не существует",
time: Date.now(),
code: 0,
});
}); });
return ( return (
@ -42,7 +38,7 @@ function PubNoteSafe() {
<ButtonWithIcon <ButtonWithIcon
className="mb-4" className="mb-4"
href="/" href="/"
text="Писать" text={locals.Write}
icon={ChevronDoubleLeftIcon} icon={ChevronDoubleLeftIcon}
/> />
@ -50,8 +46,7 @@ function PubNoteSafe() {
<div className="p-4 mb-2"> <div className="p-4 mb-2">
<div className="grid grid-cols-1 lg:grid-cols-2"> <div className="grid grid-cols-1 lg:grid-cols-2">
<h2 className="font-medium text-center lg:text-left p-2"> <h2 className="font-medium text-center lg:text-left p-2">
Ссылка для отправки публичной заметки. При переходе на эту ссылку, {locals.PubUrlPlaceholder}
заметка исчезнет с сервера и будет сохранена локально.
</h2> </h2>
<CopyToClipboard <CopyToClipboard
text={`${window.location.origin}/pubNotes/${params.id}`} text={`${window.location.origin}/pubNotes/${params.id}`}

View file

@ -16,10 +16,10 @@ function Publish() {
}; };
if (!note.name) { if (!note.name) {
err = "Заметка не была опубликована, так как отсутствует название."; err = locals.PubErrorMsgNoName;
} }
if (!note.text) { if (!note.text) {
err = "Заметка не была опубликована, так как отсутствует текст."; err = locals.PubErrorMsgNoText;
} }
fetch(`/publish`, { fetch(`/publish`, {

View file

@ -3,53 +3,56 @@ import {
SettingsTextInput, SettingsTextInput,
SettingsSelectInput, SettingsSelectInput,
} from "../components/settingsInputs"; } from "../components/settingsInputs";
import { reRenderPage } from "../components/utils";
import Locales from "../localisation/main";
function Settings() { function Settings() {
return ( return (
<div className=""> <div className="">
<h1 className="text-center lg:text-left leading-tight text-2xl font-bold"> <h1 className="text-center lg:text-left leading-tight text-2xl font-bold">
Настройки {locals.Settings}
</h1> </h1>
<SettingsPlaceholder text="Пользователь" /> <SettingsPlaceholder text={locals.User} />
<SettingsTextInput <SettingsTextInput
placeholder="Имя" placeholder={locals.Name}
label="Имя пользователя" label={locals.UserName}
settingName="userName" settingName="userName"
/> />
<SettingsTextInput <SettingsTextInput
placeholder="Ссылка" placeholder={locals.Url}
label="Ссылка на фото" label={locals.PhotoUrl}
settingName="userPhoto" settingName="userPhoto"
/> />
<SettingsTextInput <SettingsTextInput
placeholder="Статус" placeholder={locals.Status}
label="Статус пользователя" label={locals.UserStatus}
settingName="userStatus" settingName="userStatus"
/> />
<SettingsPlaceholder text="Заметки" /> <SettingsPlaceholder text={locals.Notes} />
<SettingsCheckBox <SettingsCheckBox
label="Редактирование в предпросмотре" label={locals.EditPreview}
title="Может вызывать необратимые изменения текста, например ломает теги кода" title={locals.EditPreviewWarn}
checked={settings.editPreview} checked={settings.editPreview}
settingName="editPreview" settingName="editPreview"
/> />
<SettingsCheckBox <SettingsCheckBox
label="Публичная заметка" label={locals.PublicNote}
title="Если включено, то заметка будет видна всем пользователям" title={locals.PublicNoteTitle}
checked={settings.publicNote} checked={settings.publicNote}
settingName="publicNote" settingName="publicNote"
/> />
<SettingsPlaceholder text="Интерфейс" /> <SettingsPlaceholder text={locals.Interface} />
<SettingsSelectInput <SettingsSelectInput
label="Язык" label={locals.Language}
settingName="language" settingName="language"
options={[ options={[
{ {
@ -58,16 +61,25 @@ function Settings() {
}, },
{ {
value: "en", value: "en",
label: "English", label: "English (US)",
}, },
]} ]}
onChange={(e) => {
window.locals =
Locales[window.settings.language] ||
Locales[navigator.language] ||
Locales[navigator.userLanguage] ||
Locales.en;
reRenderPage();
}}
/> />
<SettingsPlaceholder text="Стороннее API" /> <SettingsPlaceholder text={locals.ThirdPartyApi} />
<SettingsTextInput <SettingsTextInput
placeholder="Ключ" placeholder={locals.Key}
label="OpenAi API ключ" label={locals.OpenAiKey}
settingName="openAiKey" settingName="openAiKey"
secret secret
/> />