diff --git a/ui/babel.config.json b/ui/babel.config.json index d2f9ebf4..91cd094a 100644 --- a/ui/babel.config.json +++ b/ui/babel.config.json @@ -1,4 +1,15 @@ { "presets": ["@babel/preset-react"], - "plugins": ["styled-components", "graphql-tag"] + "plugins": [ + "styled-components", + "graphql-tag", + [ + "i18next-extract", + { + "locales": ["en", "da"], + "discardOldKeys": true, + "defaultValue": null + } + ] + ] } diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json new file mode 100644 index 00000000..936bb0cd --- /dev/null +++ b/ui/extractedTranslations/da/translation.json @@ -0,0 +1,66 @@ +{ + "albums_page": { + "title": "Album" + }, + "loading": { + "paginate": { + "media": "Loader flere medier" + } + }, + "login_page": { + "field": { + "password": "Adgangskode", + "submit": "Log ind", + "username": "Brugernavn" + }, + "initial_setup": { + "field": { + "photo_path": "Billedesti" + }, + "field-photo_path": { + "placeholder": "/sti/til/billeder" + }, + "field-submit": "Opsæt Photoview", + "title": "Førstegangsopsætning" + }, + "welcome": "Velkommen til Photoview" + }, + "settings": { + "concurrent_workers": { + "description": "Det maksimale antal medier som må skannes samtidig", + "title": "Samtidige scanner-arbejdere" + }, + "periodic_scanner": { + "checkbox_label": "Aktiver periodiske scanner", + "field": { + "description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere", + "label": "Periodiske scanningsintervaller" + }, + "interval_unit": { + "days": "Dage", + "hour": "Timer", + "minutes": "Minutter", + "months": "Måneder", + "seconds": "Sekunder" + }, + "title": "Periodisk scanner" + }, + "scanner": { + "description": "Vil scanne alle brugere for nye eller opdaterede medier", + "scan_all_users": "Scan alle brugere", + "title": "Scanner" + } + }, + "sidemenu": { + "albums": null, + "logout": "Log af", + "people": "Personer", + "photos": "Billeder", + "places": "Kort", + "settings": "Indstillinger" + }, + "title": { + "loading_album": "Loader album", + "settings": "Indstillinger" + } +} diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json new file mode 100644 index 00000000..02cd8b10 --- /dev/null +++ b/ui/extractedTranslations/en/translation.json @@ -0,0 +1,66 @@ +{ + "albums_page": { + "title": "Albums" + }, + "loading": { + "paginate": { + "media": "Loading more media" + } + }, + "login_page": { + "field": { + "password": "Password", + "submit": "Sign in", + "username": "Username" + }, + "initial_setup": { + "field": { + "photo_path": "Photo path" + }, + "field-photo_path": { + "placeholder": "/path/to/photos" + }, + "field-submit": "Setup Photoview", + "title": "Initial Setup" + }, + "welcome": "Welcome to Photoview" + }, + "settings": { + "concurrent_workers": { + "description": "The maximum amount of scanner jobs that is allowed to run at once", + "title": "Scanner concurrent workers" + }, + "periodic_scanner": { + "checkbox_label": "Enable periodic scanner", + "field": { + "description": "How often the scanner should perform automatic scans of all users", + "label": "Periodic scan interval" + }, + "interval_unit": { + "days": "Days", + "hour": "Hour", + "minutes": "Minutes", + "months": "Months", + "seconds": "Seconds" + }, + "title": "Periodic scanner" + }, + "scanner": { + "description": "Will scan all users for new or updated media", + "scan_all_users": "Scan all users", + "title": "Scanner" + } + }, + "sidemenu": { + "albums": "Albums", + "logout": "Log out", + "people": "People", + "photos": "Photos", + "places": "Places", + "settings": "Settings" + }, + "title": { + "loading_album": "Loading album", + "settings": "Settings" + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 37f2286d..67884c0e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,6 +18,7 @@ "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", + "babel-plugin-i18next-extract": "^0.8.3", "babel-plugin-styled-components": "^1.12.0", "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1", "browser-sync": "^2.26.14", @@ -3413,6 +3414,39 @@ "graphql-tag": "^2.10.1" } }, + "node_modules/babel-plugin-i18next-extract": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-i18next-extract/-/babel-plugin-i18next-extract-0.8.3.tgz", + "integrity": "sha512-ZBhGjP2nLF3pGJO/X6s8DlyUo8zkuPQ09sGZK4XGqtJit/ccj8zocO5JI/F+oFZgKVH1tN8pAQT4fm0JWk2SIQ==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/types": "7.9.6", + "deepmerge": "^4.2.2", + "i18next": "^19.8.3", + "json-stable-stringify": "^1.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/babel-plugin-i18next-extract/node_modules/@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/babel-plugin-i18next-extract/node_modules/i18next": { + "version": "19.9.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz", + "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, "node_modules/babel-plugin-istanbul": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", @@ -9161,6 +9195,14 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dependencies": { + "jsonify": "~0.0.0" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9191,6 +9233,14 @@ "node": ">= 10.0.0" } }, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "engines": { + "node": "*" + } + }, "node_modules/jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -16576,6 +16626,38 @@ "debug": "^4.1.1" } }, + "babel-plugin-i18next-extract": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-i18next-extract/-/babel-plugin-i18next-extract-0.8.3.tgz", + "integrity": "sha512-ZBhGjP2nLF3pGJO/X6s8DlyUo8zkuPQ09sGZK4XGqtJit/ccj8zocO5JI/F+oFZgKVH1tN8pAQT4fm0JWk2SIQ==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/types": "7.9.6", + "deepmerge": "^4.2.2", + "i18next": "^19.8.3", + "json-stable-stringify": "^1.0.1" + }, + "dependencies": { + "@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "requires": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "i18next": { + "version": "19.9.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz", + "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==", + "requires": { + "@babel/runtime": "^7.12.0" + } + } + } + }, "babel-plugin-istanbul": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", @@ -21096,6 +21178,14 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -21123,6 +21213,11 @@ } } }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", diff --git a/ui/package.json b/ui/package.json index f61677e4..68d1e70d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", + "babel-plugin-i18next-extract": "^0.8.3", "babel-plugin-styled-components": "^1.12.0", "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1", "browser-sync": "^2.26.14", diff --git a/ui/src/Layout.js b/ui/src/Layout.js index d64dfb02..1a1799c3 100644 --- a/ui/src/Layout.js +++ b/ui/src/Layout.js @@ -108,31 +108,33 @@ export const SideMenu = () => { - {t('Photos')} + {t('sidemenu.photos', 'Photos')} - {t('Albums')} + {t('sidemenu.albums', 'Albums')} {mapboxEnabled ? ( - {t('Places')} + {t('sidemenu.places', 'Places')} ) : null} - {t('People')} + {t('sidemenu.people', 'People')} {isAdmin ? ( - {t('Settings')} + + {t('sidemenu.settings', 'Settings')} + ) : null} - {t('Log out')} + {t('sidemenu.logout', 'Log out')} ) diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index bd36767a..f9b51807 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -8,6 +8,7 @@ import useURLParameters from '../../hooks/useURLParameters' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import LazyLoad from '../../helpers/LazyLoad' +import { useTranslation } from 'react-i18next' const albumQuery = gql` query albumQuery( @@ -63,6 +64,8 @@ let refetchNeededFavorites = false function AlbumPage({ match }) { const albumId = match.params.id + const { t } = useTranslation() + const { getParam, setParam, setParams } = useURLParameters() const onlyFavorites = getParam('favorites') == '1' ? true : false @@ -139,7 +142,11 @@ function AlbumPage({ match }) { if (error) return
Error
return ( - + ) diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js b/ui/src/Pages/AllAlbumsPage/AlbumsPage.js index 0751e06a..ae805b11 100644 --- a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js +++ b/ui/src/Pages/AllAlbumsPage/AlbumsPage.js @@ -3,6 +3,7 @@ import AlbumBoxes from '../../components/albumGallery/AlbumBoxes' import Layout from '../../Layout' import { useQuery, gql } from '@apollo/client' import LazyLoad from '../../helpers/LazyLoad' +import { useTranslation } from 'react-i18next' const getAlbumsQuery = gql` query getMyAlbums { @@ -19,6 +20,7 @@ const getAlbumsQuery = gql` ` const AlbumsPage = () => { + const { t } = useTranslation() const { loading, error, data } = useQuery(getAlbumsQuery) useEffect(() => { @@ -31,7 +33,7 @@ const AlbumsPage = () => { return ( -

Albums

+

{t('albums_page.title', 'Albums')}

{!loading && ( { + const { t } = useTranslation() + const [state, setState] = useState({ username: '', password: '', @@ -85,7 +88,7 @@ const InitialSetupPage = () => { {initialSetupRedirect}
- Initial Setup + {t('login_page.initial_setup.title', 'Initial Setup')}
{ } > - + handleChange(e, 'username')} /> - + handleChange(e, 'password')} /> - + handleChange(e, 'rootPath')} /> - +
diff --git a/ui/src/Pages/LoginPage/LoginPage.js b/ui/src/Pages/LoginPage/LoginPage.js index ff4a984c..753fd74f 100644 --- a/ui/src/Pages/LoginPage/LoginPage.js +++ b/ui/src/Pages/LoginPage/LoginPage.js @@ -7,6 +7,7 @@ import { checkInitialSetupQuery, login, Container } from './loginUtilities' import { authToken } from '../../helpers/authentication' import logoPath from '../../assets/photoview-logo.svg' +import { useTranslation } from 'react-i18next' const authorizeMutation = gql` mutation Authorize($username: String!, $password: String!) { @@ -22,18 +23,26 @@ const StyledLogo = styled.img` max-height: 128px; ` -const LogoHeader = props => ( -
- -

Welcome to Photoview

-
-) +const LogoHeader = props => { + const { t } = useTranslation() + + return ( +
+ +

+ {t('login_page.welcome', 'Welcome to Photoview')} +

+
+ ) +} const LogoHeaderStyled = styled(LogoHeader)` margin-bottom: 72px !important; ` const LoginPage = () => { + const { t } = useTranslation() + const [credentials, setCredentials] = useState({ username: '', password: '', @@ -99,14 +108,18 @@ const LoginPage = () => { loading={loading || (data && data.authorizeUser.success)} > - + handleChange(e, 'username')} /> - + { /> - + diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.js b/ui/src/Pages/SettingsPage/PeriodicScanner.js index 2235eb9b..d6228907 100644 --- a/ui/src/Pages/SettingsPage/PeriodicScanner.js +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.js @@ -3,6 +3,7 @@ import React, { useRef, useState } from 'react' import { useMutation, useQuery } from '@apollo/client' import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react' import { InputLabelDescription, InputLabelTitle } from './SettingsPage' +import { useTranslation } from 'react-i18next' const SCAN_INTERVAL_QUERY = gql` query scanIntervalQuery { @@ -74,6 +75,8 @@ const convertToAppropriateUnit = ({ value, unit }) => { } const PeriodicScanner = () => { + const { t } = useTranslation() + const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false) const [scanInterval, setScanInterval] = useState({ value: 4, @@ -131,38 +134,41 @@ const PeriodicScanner = () => { const scanIntervalUnits = [ { key: 'second', - text: 'Seconds', + text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'), value: 'second', }, { key: 'minute', - text: 'Minutes', + text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'), value: 'minute', }, { key: 'hour', - text: 'Hours', + text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'), value: 'hour', }, { key: 'day', - text: 'Days', + text: t('settings.periodic_scanner.interval_unit.days', 'Days'), value: 'day', }, { key: 'month', - text: 'Months', + text: t('settings.periodic_scanner.interval_unit.months', 'Months'), value: 'month', }, ] return ( <> -

Periodic scanner

+

{t('settings.periodic_scanner.title', 'Periodic scanner')}

onScanIntervalCheckboxChange(checked)} @@ -172,9 +178,17 @@ const PeriodicScanner = () => { {enablePeriodicScanner && ( <> { + const { t } = useTranslation() + const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, { onCompleted(data) { setWorkerAmount(data.siteInfo.concurrentWorkers) @@ -46,9 +49,14 @@ const ScannerConcurrentWorkers = () => { return (
{ + const { t } = useTranslation() const [startScanner, { called }] = useMutation(SCAN_MUTATION) return (
- Scanner + + {t('settings.scanner.title', 'Scanner')} + - Will scan all users for new or updated media + {t( + 'settings.scanner.description', + 'Will scan all users for new or updated media' + )} diff --git a/ui/src/Pages/SettingsPage/SettingsPage.js b/ui/src/Pages/SettingsPage/SettingsPage.js index 04e43344..4d832aaf 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.js +++ b/ui/src/Pages/SettingsPage/SettingsPage.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' import Layout from '../../Layout' @@ -24,8 +25,10 @@ export const InputLabelDescription = styled.p` ` const SettingsPage = () => { + const { t } = useTranslation() + return ( - + diff --git a/ui/src/index.js b/ui/src/index.js index 59682844..eef8dddd 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -20,12 +20,19 @@ i18n.use(initReactI18next).init({ }, lng: 'en', fallbackLng: 'en', + returnNull: false, interpolation: { escapeValue: false, }, }) +import('../extractedTranslations/da/translation.json').then(danish => { + i18n.addResourceBundle('da', 'translation', danish) + console.log('loaded danish') + i18n.changeLanguage('da') +}) + const Main = () => (