);
diff --git a/components/layout/Footer.module.css b/components/layout/Footer.module.css
index 0a46621d..ff5a26bb 100644
--- a/components/layout/Footer.module.css
+++ b/components/layout/Footer.module.css
@@ -1,14 +1,21 @@
.footer {
display: flex;
+ justify-content: space-between;
align-items: center;
font-size: var(--font-size-small);
min-height: 100px;
}
-.footer button {
+.footer .center button {
margin-left: 10px;
}
.footer a {
text-decoration: none;
}
+
+.center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js
index 78d120eb..83ae00b8 100644
--- a/components/metrics/ActiveUsers.js
+++ b/components/metrics/ActiveUsers.js
@@ -1,5 +1,4 @@
import React, { useMemo } from 'react';
-import { useSpring, animated } from 'react-spring';
import classNames from 'classnames';
import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
@@ -11,11 +10,6 @@ export default function ActiveUsers({ websiteId, className }) {
return data?.[0]?.x || 0;
}, [data]);
- const props = useSpring({
- x: count,
- from: { x: 0 },
- });
-
if (count === 0) {
return null;
}
@@ -24,14 +18,11 @@ export default function ActiveUsers({ websiteId, className }) {
-
- {props.x.interpolate(x => x.toFixed(0))}
-
diff --git a/lang/en.json b/lang/en.json
index e559a846..769bd85f 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1,6 +1,6 @@
{
"active-users.message": {
- "defaultMessage": "current {count, plural, one {visitor} other {visitors}}"
+ "defaultMessage": "{x} current {x, plural, one {visitor} other {visitors}}"
},
"button.add-account": {
"defaultMessage": "Add account"
@@ -74,6 +74,12 @@
"label.last-hours": {
"defaultMessage": "Last {x} hours"
},
+ "label.logged-in-as": {
+ "defaultMessage": "Logged in as {username}"
+ },
+ "label.logout": {
+ "defaultMessage": "Logout"
+ },
"label.name": {
"defaultMessage": "Name"
},
diff --git a/lang/zh-CN.json b/lang/zh-CN.json
new file mode 100644
index 00000000..922db26a
--- /dev/null
+++ b/lang/zh-CN.json
@@ -0,0 +1,233 @@
+{
+ "active-users.message": {
+ "defaultMessage": "xx {x} current {x, plural, one {visitor} other {visitors}}"
+ },
+ "button.add-account": {
+ "defaultMessage": "xx Add account"
+ },
+ "button.add-website": {
+ "defaultMessage": "xx Add website"
+ },
+ "button.back": {
+ "defaultMessage": "xx Back"
+ },
+ "button.cancel": {
+ "defaultMessage": "xx Cancel"
+ },
+ "button.change-password": {
+ "defaultMessage": "xx Change password"
+ },
+ "button.copy-to-clipboard": {
+ "defaultMessage": "xx Copy to clipboard"
+ },
+ "button.delete": {
+ "defaultMessage": "xx Delete"
+ },
+ "button.edit": {
+ "defaultMessage": "xx Edit"
+ },
+ "button.login": {
+ "defaultMessage": "xx Login"
+ },
+ "button.more": {
+ "defaultMessage": "xx More"
+ },
+ "button.save": {
+ "defaultMessage": "xx Save"
+ },
+ "button.view-details": {
+ "defaultMessage": "xx View details"
+ },
+ "footer.powered-by": {
+ "defaultMessage": "xx powered by"
+ },
+ "header.nav.dashboard": {
+ "defaultMessage": "xx Dashboard"
+ },
+ "header.nav.settings": {
+ "defaultMessage": "xx Settings"
+ },
+ "label.adminsitrator": {
+ "defaultMessage": "xx Administrator"
+ },
+ "label.confirm-password": {
+ "defaultMessage": "xx Confirm password"
+ },
+ "label.current-password": {
+ "defaultMessage": "xx Current password"
+ },
+ "label.domain": {
+ "defaultMessage": "xx Domain"
+ },
+ "label.enable-share-url": {
+ "defaultMessage": "xx Enable share URL"
+ },
+ "label.invalid": {
+ "defaultMessage": "xx Invalid"
+ },
+ "label.invalid-domain": {
+ "defaultMessage": "xx Invalid domain"
+ },
+ "label.last-days": {
+ "defaultMessage": "xx Last {x} days"
+ },
+ "label.last-hours": {
+ "defaultMessage": "xx Last {x} hours"
+ },
+ "label.logged-in-as": {
+ "defaultMessage": "xx Logged in as {username}"
+ },
+ "label.logout": {
+ "defaultMessage": "xx Logout"
+ },
+ "label.name": {
+ "defaultMessage": "xx Name"
+ },
+ "label.new-password": {
+ "defaultMessage": "xx New password"
+ },
+ "label.password": {
+ "defaultMessage": "xx Password"
+ },
+ "label.passwords-dont-match": {
+ "defaultMessage": "xx Passwords don't match"
+ },
+ "label.required": {
+ "defaultMessage": "xx Required"
+ },
+ "label.this-month": {
+ "defaultMessage": "xx This month"
+ },
+ "label.this-week": {
+ "defaultMessage": "xx This week"
+ },
+ "label.this-year": {
+ "defaultMessage": "xx This year"
+ },
+ "label.today": {
+ "defaultMessage": "xx Today"
+ },
+ "label.username": {
+ "defaultMessage": "xx Username"
+ },
+ "message.confirm-delete": {
+ "defaultMessage": "xx Are your sure you want to delete {target}?"
+ },
+ "message.copied": {
+ "defaultMessage": "xx Copied!"
+ },
+ "message.delete-warning": {
+ "defaultMessage": "xx All associated data will be deleted as well."
+ },
+ "message.failure": {
+ "defaultMessage": "xx Something went wrong."
+ },
+ "message.save-success": {
+ "defaultMessage": "xx Saved successfully."
+ },
+ "message.share-url": {
+ "defaultMessage": "xx This is the publicly shared URL for {target}."
+ },
+ "message.track-stats": {
+ "defaultMessage": "xx To track stats for {target}, place the following code in the {head} section of your website."
+ },
+ "message.type-delete": {
+ "defaultMessage": "xx Type {delete} in the box below to confirm."
+ },
+ "metrics.actions": {
+ "defaultMessage": "xx Actions"
+ },
+ "metrics.average-visit-time": {
+ "defaultMessage": "xx Average visit time"
+ },
+ "metrics.bounce-rate": {
+ "defaultMessage": "xx Bounce rate"
+ },
+ "metrics.browsers": {
+ "defaultMessage": "xx Browsers"
+ },
+ "metrics.countries": {
+ "defaultMessage": "xx Countries"
+ },
+ "metrics.devices": {
+ "defaultMessage": "xx Devices"
+ },
+ "metrics.events": {
+ "defaultMessage": "xx Events"
+ },
+ "metrics.filter.combined": {
+ "defaultMessage": "xx Combined"
+ },
+ "metrics.filter.domain-only": {
+ "defaultMessage": "xx Domain only"
+ },
+ "metrics.filter.raw": {
+ "defaultMessage": "xx Raw"
+ },
+ "metrics.operating-system": {
+ "defaultMessage": "xx Operating System"
+ },
+ "metrics.page-views": {
+ "defaultMessage": "xx Page views"
+ },
+ "metrics.pages": {
+ "defaultMessage": "xx Pages"
+ },
+ "metrics.referrers": {
+ "defaultMessage": "xx Referrers"
+ },
+ "metrics.unique-visitors": {
+ "defaultMessage": "xx Unique visitors"
+ },
+ "metrics.views": {
+ "defaultMessage": "xx Views"
+ },
+ "metrics.visitors": {
+ "defaultMessage": "xx Visitors"
+ },
+ "placeholder.message.go-to-settings": {
+ "defaultMessage": "xx Go to settings"
+ },
+ "placeholder.message.no-websites-configured": {
+ "defaultMessage": "xx You don't have any websites configured."
+ },
+ "settings.accounts": {
+ "defaultMessage": "xx Accounts"
+ },
+ "settings.profile": {
+ "defaultMessage": "xx Profile"
+ },
+ "settings.websites": {
+ "defaultMessage": "xx Websites"
+ },
+ "title.add-account": {
+ "defaultMessage": "xx Add account"
+ },
+ "title.add-website": {
+ "defaultMessage": "xx Add website"
+ },
+ "title.delete-account": {
+ "defaultMessage": "xx Delete account"
+ },
+ "title.delete-website": {
+ "defaultMessage": "xx Delete website"
+ },
+ "title.edit-account": {
+ "defaultMessage": "xx Edit account"
+ },
+ "title.edit-website": {
+ "defaultMessage": "xx Edit website"
+ },
+ "title.share-url": {
+ "defaultMessage": "xx Share URL"
+ },
+ "title.tracking-code": {
+ "defaultMessage": "xx Tracking code"
+ },
+ "tooltip.get-share-url": {
+ "defaultMessage": "xx Get share URL"
+ },
+ "tooltip.get-tracking-code": {
+ "defaultMessage": "xx Get tracking code"
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 20f99bd7..bf0988be 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "umami",
- "version": "0.23.0",
+ "version": "0.24.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao
",
"license": "MIT",
@@ -11,7 +11,7 @@
},
"scripts": {
"dev": "next dev",
- "build": "npm-run-all build-tracker copy-db-schema build-db-client build-app",
+ "build": "npm-run-all build-tracker compile-lang copy-db-schema build-db-client build-app",
"start": "next start",
"build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js",
@@ -22,7 +22,8 @@
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma",
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma",
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
- "extract": "formatjs extract --out-file lang/en.json components/**/*.js"
+ "extract-lang": "formatjs extract {pages,components}/**/*.js --out-file lang/en.json",
+ "compile-lang": "formatjs compile-folder --ast lang lang-compiled"
},
"lint-staged": {
"**/*.js": [
diff --git a/pages/_app.js b/pages/_app.js
index 56f17385..710a7aa5 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,19 +1,45 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { IntlProvider } from 'react-intl';
-import { Provider } from 'react-redux';
+import { Provider, useDispatch, useSelector } from 'react-redux';
import { useStore } from 'redux/store';
import 'styles/variables.css';
import 'styles/bootstrap-grid.css';
import 'styles/index.css';
+import en from 'lang-compiled/en.json';
+import cn from 'lang-compiled/zh-CN.json';
+import { updateApp } from '../redux/actions/app';
+
+const messages = {
+ en,
+ 'zh-CN': cn,
+};
+
+const Intl = ({ children }) => {
+ const dispatch = useDispatch();
+ const locale = useSelector(state => state.app.locale);
+
+ useEffect(() => {
+ const saved = localStorage.getItem('locale');
+ if (saved) {
+ dispatch(updateApp({ locale: saved }));
+ }
+ });
+
+ return (
+
+ {children}
+
+ );
+};
export default function App({ Component, pageProps }) {
const store = useStore();
return (
-
-
+
+
-
-
+
+
);
}
diff --git a/redux/actions/app.js b/redux/actions/app.js
new file mode 100644
index 00000000..4cc28b3b
--- /dev/null
+++ b/redux/actions/app.js
@@ -0,0 +1,16 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const app = createSlice({
+ name: 'app',
+ initialState: { locale: 'en' },
+ reducers: {
+ updateApp(state, action) {
+ state = action.payload;
+ return state;
+ },
+ },
+});
+
+export const { updateApp } = app.actions;
+
+export default app.reducer;
diff --git a/redux/reducers.js b/redux/reducers.js
index 7a4b6d7b..161bf134 100644
--- a/redux/reducers.js
+++ b/redux/reducers.js
@@ -1,6 +1,7 @@
import { combineReducers } from 'redux';
+import app from './actions/app';
import user from './actions/user';
import websites from './actions/websites';
import queries from './actions/queries';
-export default combineReducers({ user, websites, queries });
+export default combineReducers({ app, user, websites, queries });