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;