diff --git a/web/package-lock.json b/web/package-lock.json index 8ee2635d..ef9680be 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "ntfy", "version": "1.0.0", "dependencies": { + "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", @@ -25,7 +26,9 @@ "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.2.2", "stacktrace-gps": "^3.0.4", - "stacktrace-js": "^2.0.2" + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0", + "stylis-plugin-rtl": "^2.1.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.0.0", @@ -1765,6 +1768,11 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/cache": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", @@ -1777,6 +1785,11 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", @@ -3314,6 +3327,14 @@ "node": ">=8" } }, + "node_modules/cssjanus": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.1.0.tgz", + "integrity": "sha512-kAijbny3GmdOi9k+QT6DGIXqFvL96aksNlGr4Rhk9qXDZYWUojU4bRc3IHWxdaLNOqgEZHuXoe5Wl2l7dxLW5g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -6351,9 +6372,20 @@ } }, "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" + }, + "node_modules/stylis-plugin-rtl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz", + "integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==", + "dependencies": { + "cssjanus": "^2.0.1" + }, + "peerDependencies": { + "stylis": "4.x" + } }, "node_modules/supports-color": { "version": "5.5.0", diff --git a/web/package.json b/web/package.json index 2e52635a..400a090a 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "lint": "eslint --report-unused-disable-directives --ext .js,.jsx ./src/" }, "dependencies": { + "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", @@ -28,7 +29,9 @@ "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.2.2", "stacktrace-gps": "^3.0.4", - "stacktrace-js": "^2.0.2" + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0", + "stylis-plugin-rtl": "^2.1.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.0.0", diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 2ad7f419..8b60b3e8 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -3,6 +3,7 @@ import { createContext, Suspense, useContext, useEffect, useState, useMemo } fro import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material"; import { useLiveQuery } from "dexie-react-hooks"; import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { AllSubscriptions, SingleSubscription } from "./Notifications"; import { darkTheme, lightTheme } from "./theme"; import Navigation from "./Navigation"; @@ -21,6 +22,7 @@ import Signup from "./Signup"; import Account from "./Account"; import "../app/i18n"; // Translations! import prefs, { THEME } from "../app/Prefs"; +import RTLCacheProvider from "./RTLCacheProvider"; export const AccountContext = createContext(null); @@ -39,37 +41,47 @@ const darkModeEnabled = (prefersDarkMode, themePreference) => { }; const App = () => { + const { i18n } = useTranslation(); + const languageDir = i18n.dir(); + const [account, setAccount] = useState(null); const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); const themePreference = useLiveQuery(() => prefs.theme()); const theme = React.useMemo( - () => createTheme(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme), - [prefersDarkMode, themePreference] + () => createTheme({ ...(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme), direction: languageDir }), + [prefersDarkMode, themePreference, languageDir] ); + useEffect(() => { + document.documentElement.setAttribute("lang", i18n.language); + document.dir = languageDir; + }, [i18n.language, languageDir]); + return ( }> - - - - - - - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - - - - - - + + + + + + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + + + + + + + ); }; diff --git a/web/src/components/RTLCacheProvider.jsx b/web/src/components/RTLCacheProvider.jsx new file mode 100644 index 00000000..a85fced6 --- /dev/null +++ b/web/src/components/RTLCacheProvider.jsx @@ -0,0 +1,22 @@ +import React from "react"; + +import rtlPlugin from "stylis-plugin-rtl"; +import { CacheProvider } from "@emotion/react"; +import createCache from "@emotion/cache"; +import { prefixer } from "stylis"; +import { useTranslation } from "react-i18next"; + +// https://mui.com/material-ui/guides/right-to-left + +const cacheRtl = createCache({ + key: "muirtl", + stylisPlugins: [prefixer, rtlPlugin], +}); + +const RTLCacheProvider = ({ children }) => { + const { i18n } = useTranslation(); + + return i18n.dir() === "rtl" ? {children} : children; +}; + +export default RTLCacheProvider;