diff --git a/package.json b/package.json index d416da6..e85faea 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,12 @@ "classnames": "^2.3.2", "dockview": "^1.7.1", "formik": "^2.2.9", + "i18next": "^22.4.15", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.2.2", "react-markdown": "^8.0.7", "remirror": "^2.0.26", "sass": "^1.62.0", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..09a3e7b --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,10 @@ +{ + "Edit": "Edit", + "Save": "Save", + "Delete": "Delete", + "Nothing's here yet": "Nothing's here yet", + "Background": "Background", + "Text": "Text", + "Links": "Links", + "Colors": "Colors" +} diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json new file mode 100644 index 0000000..73d416d --- /dev/null +++ b/public/locales/ru/translation.json @@ -0,0 +1,10 @@ +{ + "Edit": "Изменить", + "Save": "Сохранить", + "Delete": "Удалить", + "Nothing's here yet": "Здесь ничего нет", + "Background": "Фон", + "Text": "Текст", + "Links": "Ссылки", + "Colors": "Цвета" +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..c0f2a60 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,16 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .use(Backend) + .use(LanguageDetector) + .init({ + fallbackLng: "en", + debug: true, + interpolation: { + escapeValue: false, + }, + }); diff --git a/src/i18next.d.ts b/src/i18next.d.ts new file mode 100644 index 0000000..aa74b0b --- /dev/null +++ b/src/i18next.d.ts @@ -0,0 +1,11 @@ +import "i18next"; +import en from "../public/locales/en/translation.json"; + +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: "en"; + resources: { + en: typeof en; + }; + } +} diff --git a/src/main.tsx b/src/main.tsx index e09f381..dcadee9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,10 +2,12 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { Editor } from "~/pages/editor"; -import "./styles/main.scss"; import { ThemeProvider } from "./modules/theme/containers/ThemeProvider"; import { SettingsProvider } from "./modules/settings/providers/SettingsProvider"; +import "./i18n"; +import "./styles/main.scss"; + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <React.StrictMode> <SettingsProvider> diff --git a/src/modules/editor/components/EditorWrapper/index.tsx b/src/modules/editor/components/EditorWrapper/index.tsx index e4336fa..566bf19 100644 --- a/src/modules/editor/components/EditorWrapper/index.tsx +++ b/src/modules/editor/components/EditorWrapper/index.tsx @@ -1,6 +1,7 @@ import { FC, PropsWithChildren } from "react"; -import styles from "./styles.module.scss"; +import { useTranslation } from "react-i18next"; import { Button } from "~/components/buttons/Button"; +import styles from "./styles.module.scss"; interface EditorWrapperProps extends PropsWithChildren { save: () => void; @@ -8,19 +9,21 @@ interface EditorWrapperProps extends PropsWithChildren { } const EditorWrapper: FC<EditorWrapperProps> = ({ children, save, remove }) => { + const { t } = useTranslation(); + return ( <div className={styles.wrapper}> <div className={styles.content}>{children}</div> <div className={styles.panel}> <Button onClick={remove} role="button" size="small" variant="outline"> - Delete + {t("Delete")} </Button> <div className={styles.filler} /> <Button onClick={save} role="button" size="small"> - Save + {t("Save")} </Button> </div> </div> diff --git a/src/modules/editor/components/EmptyViewer/index.tsx b/src/modules/editor/components/EmptyViewer/index.tsx index c088c90..94387d2 100644 --- a/src/modules/editor/components/EmptyViewer/index.tsx +++ b/src/modules/editor/components/EmptyViewer/index.tsx @@ -1,7 +1,8 @@ import { FC } from "react"; -import styles from "./styles.module.scss"; -import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings"; +import { useTranslation } from "react-i18next"; import { Button } from "~/components/buttons/Button"; +import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings"; +import styles from "./styles.module.scss"; interface EmptyViewerProps { startEditing?: () => void; @@ -9,10 +10,11 @@ interface EmptyViewerProps { const EmptyViewer: FC<EmptyViewerProps> = ({ startEditing }) => { const style = useContainerPaddings(); + const { t } = useTranslation(); return ( <div className={styles.empty} style={style}> - <div className={styles.title}>Nothing's here</div> + <div className={styles.title}>{t(`Nothing's here yet`)}</div> <div> <Button onClick={startEditing} @@ -20,7 +22,7 @@ const EmptyViewer: FC<EmptyViewerProps> = ({ startEditing }) => { variant="outline" size="small" > - Edit it + {t("Edit")} </Button> </div> </div> diff --git a/src/modules/editor/components/ReactMarkdownViewer/index.tsx b/src/modules/editor/components/ReactMarkdownViewer/index.tsx index 84ef1d6..2c0b254 100644 --- a/src/modules/editor/components/ReactMarkdownViewer/index.tsx +++ b/src/modules/editor/components/ReactMarkdownViewer/index.tsx @@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown"; import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings"; import styles from "./styles.module.scss"; import { Button } from "~/components/buttons/Button"; +import { useTranslation } from "react-i18next"; interface ReactMarkdownViewerProps { value: string; @@ -13,6 +14,7 @@ const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({ value, startEditing, }) => { + const { t } = useTranslation(); const style = useContainerPaddings(); return ( @@ -24,7 +26,7 @@ const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({ role="button" onClick={startEditing} > - Edit + {t("Edit")} </Button> </div> diff --git a/src/modules/settings/containers/SettingsContainer/index.tsx b/src/modules/settings/containers/SettingsContainer/index.tsx index 0ea13c5..9e3c440 100644 --- a/src/modules/settings/containers/SettingsContainer/index.tsx +++ b/src/modules/settings/containers/SettingsContainer/index.tsx @@ -1,8 +1,10 @@ import { ChangeEvent, FC, useCallback } from "react"; import { useSettings } from "../../context/SettingsContext"; +import { useTranslation } from "react-i18next"; const SettingsContainer: FC = () => { const { update, settings } = useSettings(); + const { t } = useTranslation(); const updateBackgroundColor = useCallback( (event: ChangeEvent<HTMLInputElement>) => { @@ -26,8 +28,10 @@ const SettingsContainer: FC = () => { return ( <div> + <h2>{t("Colors")}</h2> + <label htmlFor="color"> - Background + {t("Background")} <input type="color" id="color" @@ -37,7 +41,8 @@ const SettingsContainer: FC = () => { </label> <label htmlFor="color"> - Text + {t("Text")} + <input type="color" id="color" @@ -47,7 +52,8 @@ const SettingsContainer: FC = () => { </label> <label htmlFor="color"> - Link + {t("Links")} + <input type="color" id="color" diff --git a/src/modules/settings/context/SettingsContext.ts b/src/modules/settings/context/SettingsContext.ts index 0470825..c7a1555 100644 --- a/src/modules/settings/context/SettingsContext.ts +++ b/src/modules/settings/context/SettingsContext.ts @@ -21,4 +21,5 @@ export const SettingsContext = createContext({ show: () => {}, hide: () => {}, }); + export const useSettings = () => useContext(SettingsContext); diff --git a/tsconfig.json b/tsconfig.json index 23c90ee..f01ec39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,9 @@ "baseUrl": "./", "paths": { "~/*": ["./src/*"] - } + }, + "types": ["./src/vite-env.d.ts", "./src/i18next.d.ts"] }, - "include": ["src"], + "include": ["src", "public/locales"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/yarn.lock b/yarn.lock index a687879..b9039ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -157,7 +157,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== -"@babel/runtime@7.21.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@7.21.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -3028,6 +3028,13 @@ create-context-state@^2.0.0: dependencies: "@babel/runtime" "^7.13.10" +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -4255,6 +4262,13 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: dependencies: react-is "^16.7.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + htmlparser2@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" @@ -4297,6 +4311,27 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== +i18next-browser-languagedetector@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87" + integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g== + dependencies: + "@babel/runtime" "^7.19.4" + +i18next-http-backend@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.2.0.tgz#f77c06dc8e766c7588bb38da2f5fa0614ba67b3f" + integrity sha512-Z4sM7R6tzdLknSPER9GisEBxKPg5FkI07UrQniuroZmS15PHQrcCPLyuGKj8SS68tf+O2aEDYSUnmy1TZqZSbw== + dependencies: + cross-fetch "3.1.5" + +i18next@^22.4.15: + version "22.4.15" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.15.tgz#951882b751872994f8502b5a6ef6f796e6a7d7f8" + integrity sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg== + dependencies: + "@babel/runtime" "^7.20.6" + idb-keyval@^5.0.2: version "5.1.5" resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.5.tgz#be11174bac0cb756dba4cc86fb36b6cd63f5ce6d" @@ -5530,6 +5565,13 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e" @@ -6222,6 +6264,14 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== +react-i18next@^12.2.2: + version "12.2.2" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.2.2.tgz#38a6fad11acf4f2abfc5611bdb6b1918d0f47578" + integrity sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg== + dependencies: + "@babel/runtime" "^7.20.6" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7197,6 +7247,11 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -7517,6 +7572,11 @@ vite@^4.3.0: optionalDependencies: fsevents "~2.3.2" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-keyname@^2.2.0, w3c-keyname@^2.2.4: version "2.2.6" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f" @@ -7580,6 +7640,19 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + when@3.7.7: version "3.7.7" resolved "https://registry.yarnpkg.com/when/-/when-3.7.7.tgz#aba03fc3bb736d6c88b091d013d8a8e590d84718"