1
Fork 0

More translations, fix tests, modify build process

This commit is contained in:
viktorstrate 2021-04-05 23:11:48 +02:00
parent e530ce5555
commit b05f5b8eb7
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
20 changed files with 228 additions and 90 deletions

32
ui/babel.config.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = function (api) {
const isTest = api.env('test')
const isProduction = api.env('NODE_ENV') == 'production'
let presets = ['@babel/preset-react']
let plugins = []
if (isTest) {
presets.push('@babel/preset-env')
plugins.push('@babel/plugin-transform-runtime')
plugins.push('@babel/plugin-transform-modules-commonjs')
} else {
plugins.push(['styled-components', { pure: true }])
plugins.push('graphql-tag')
if (!isProduction) {
plugins.push([
'i18next-extract',
{
locales: ['en', 'da'],
discardOldKeys: true,
defaultValue: null,
},
])
}
}
return {
presets: presets,
plugins: plugins,
}
}

View File

@ -1,15 +0,0 @@
{
"presets": ["@babel/preset-react"],
"plugins": [
"styled-components",
"graphql-tag",
[
"i18next-extract",
{
"locales": ["en", "da"],
"discardOldKeys": true,
"defaultValue": null
}
]
]
}

View File

@ -72,11 +72,19 @@ if (watchMode) {
bs.reload(args) bs.reload(args)
}) })
} else { } else {
esbuild.build(esbuildOptions).then(() => console.log('esbuild done')) const esbuildPromise = esbuild
.build(esbuildOptions)
.then(() => console.log('esbuild done'))
workboxBuild.generateSW({ const workboxPromise = workboxBuild
globDirectory: 'dist/', .generateSW({
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'], globDirectory: 'dist/',
swDest: 'dist/service-worker.js', globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
}) swDest: 'dist/service-worker.js',
})
.then(() => console.log('workbox done'))
Promise.all([esbuildPromise, workboxPromise]).then(() =>
console.log('build complete')
)
} }

View File

@ -21,8 +21,16 @@
"save": "Gem" "save": "Gem"
}, },
"loading": { "loading": {
"album": "Loader album",
"default": "Loader...", "default": "Loader...",
"shares": null "media": "Loader medier",
"page": "Loader side",
"paginate": {
"faces": "Loader flere personer",
"media": "Loader flere medier"
},
"shares": "Loader delinger...",
"timeline": "Loader tidslinje"
} }
}, },
"header": { "header": {
@ -36,11 +44,6 @@
} }
} }
}, },
"loading": {
"paginate": {
"media": "Loader flere medier"
}
},
"login_page": { "login_page": {
"field": { "field": {
"password": "Adgangskode", "password": "Adgangskode",
@ -59,6 +62,16 @@
}, },
"welcome": "Velkommen til Photoview" "welcome": "Velkommen til Photoview"
}, },
"people_page": {
"face_group": {
"label_placeholder": "Navn",
"unlabeled": "Ikke navngivet"
},
"recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter"
},
"routes": {
"page_not_found": "Side ikke fundet"
},
"settings": { "settings": {
"concurrent_workers": { "concurrent_workers": {
"description": "Det maksimale antal medier som må skannes samtidig", "description": "Det maksimale antal medier som må skannes samtidig",
@ -122,6 +135,15 @@
"title": "Brugere" "title": "Brugere"
} }
}, },
"share_page": {
"protected_share": {
"description": "Denne deling er låst med en adgangskode.",
"title": "Beskyttet deling"
},
"share_not_found": "Deling blev ikke fundet",
"share_not_found_description": "Måske er delingen udløbet eller blevet slettet.",
"wrong_password": "Forkert adgangskode, prøv venligst igen."
},
"sidebar": { "sidebar": {
"album": { "album": {
"title": "Album indstillinger" "title": "Album indstillinger"
@ -202,6 +224,7 @@
}, },
"title": { "title": {
"loading_album": "Loader album", "loading_album": "Loader album",
"people": "Personer",
"settings": "Indstillinger" "settings": "Indstillinger"
} }
} }

View File

@ -21,8 +21,16 @@
"save": "Save" "save": "Save"
}, },
"loading": { "loading": {
"album": "Loading album",
"default": "Loading...", "default": "Loading...",
"shares": "Loading shares..." "media": "Loading media",
"page": "Loading page",
"paginate": {
"faces": "Loading more people",
"media": "Loading more media"
},
"shares": "Loading shares...",
"timeline": "Loading timeline"
} }
}, },
"header": { "header": {
@ -36,11 +44,6 @@
} }
} }
}, },
"loading": {
"paginate": {
"media": "Loading more media"
}
},
"login_page": { "login_page": {
"field": { "field": {
"password": "Password", "password": "Password",
@ -59,6 +62,16 @@
}, },
"welcome": "Welcome to Photoview" "welcome": "Welcome to Photoview"
}, },
"people_page": {
"face_group": {
"label_placeholder": "Label",
"unlabeled": "Unlabeled"
},
"recognize_unlabeled_faces_button": "Recognize unlabeled faces"
},
"routes": {
"page_not_found": "Page not found"
},
"settings": { "settings": {
"concurrent_workers": { "concurrent_workers": {
"description": "The maximum amount of scanner jobs that is allowed to run at once", "description": "The maximum amount of scanner jobs that is allowed to run at once",
@ -89,7 +102,7 @@
"submit": "Add user" "submit": "Add user"
}, },
"confirm_delete_user": { "confirm_delete_user": {
"action": "Delete {user}", "action": "Delete {{user}}",
"description": "<0>Are you sure, you want to delete <1></1>?</0><p>This action cannot be undone</p>", "description": "<0>Are you sure, you want to delete <1></1>?</0><p>This action cannot be undone</p>",
"title": "Delete user" "title": "Delete user"
}, },
@ -122,6 +135,15 @@
"title": "Users" "title": "Users"
} }
}, },
"share_page": {
"protected_share": {
"description": "This share is protected with a password.",
"title": "Protected share"
},
"share_not_found": "Share not found",
"share_not_found_description": "Maybe the share has expired or has been deleted.",
"wrong_password": "Wrong password, please try again."
},
"sidebar": { "sidebar": {
"album": { "album": {
"title": "Album options" "title": "Album options"
@ -202,6 +224,7 @@
}, },
"title": { "title": {
"loading_album": "Loading album", "loading_album": "Loading album",
"people": "People",
"settings": "Settings" "settings": "Settings"
} }
} }

View File

@ -53,8 +53,8 @@
"workbox-build": "^6.1.2" "workbox-build": "^6.1.2"
}, },
"scripts": { "scripts": {
"start": "node build.mjs watch", "start": "node --experimental-modules build.mjs watch",
"build": "NODE_ENV=production node build.mjs", "build": "NODE_ENV=production node --experimental-modules build.mjs",
"test": "npm run lint && npm run jest", "test": "npm run lint && npm run jest",
"lint": "eslint ./src --max-warnings 0 --cache", "lint": "eslint ./src --max-warnings 0 --cache",
"jest": "jest", "jest": "jest",

View File

@ -9,6 +9,8 @@ import { MemoryRouter } from 'react-router-dom'
import * as authentication from './helpers/authentication' import * as authentication from './helpers/authentication'
require('./localization').default()
jest.mock('./helpers/authentication.js') jest.mock('./helpers/authentication.js')
test('Layout component', async () => { test('Layout component', async () => {

View File

@ -161,7 +161,7 @@ function AlbumPage({ match }) {
/> />
<PaginateLoader <PaginateLoader
active={!finishedLoadingMore && !loading} active={!finishedLoadingMore && !loading}
text={t('loading.paginate.media', 'Loading more media')} text={t('general.loading.paginate.media', 'Loading more media')}
/> />
</Layout> </Layout>
) )

View File

@ -9,6 +9,7 @@ import { Button, Icon, Input } from 'semantic-ui-react'
import FaceCircleImage from './FaceCircleImage' import FaceCircleImage from './FaceCircleImage'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../../components/PaginateLoader' import PaginateLoader from '../../components/PaginateLoader'
import { useTranslation } from 'react-i18next'
export const MY_FACES_QUERY = gql` export const MY_FACES_QUERY = gql`
query myFaces($limit: Int, $offset: Int) { query myFaces($limit: Int, $offset: Int) {
@ -73,6 +74,7 @@ const FaceDetailsButton = styled.button`
const FaceLabel = styled.span`` const FaceLabel = styled.span``
const FaceDetails = ({ group }) => { const FaceDetails = ({ group }) => {
const { t } = useTranslation()
const [editLabel, setEditLabel] = useState(false) const [editLabel, setEditLabel] = useState(false)
const [inputValue, setInputValue] = useState(group.label ?? '') const [inputValue, setInputValue] = useState(group.label ?? '')
const inputRef = createRef() const inputRef = createRef()
@ -124,7 +126,9 @@ const FaceDetails = ({ group }) => {
onClick={() => setEditLabel(true)} onClick={() => setEditLabel(true)}
> >
<FaceImagesCount>{group.imageFaceCount}</FaceImagesCount> <FaceImagesCount>{group.imageFaceCount}</FaceImagesCount>
<FaceLabel>{group.label ?? 'Unlabeled'}</FaceLabel> <FaceLabel>
{group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')}
</FaceLabel>
<EditIcon name="pencil" /> <EditIcon name="pencil" />
</FaceDetailsButton> </FaceDetailsButton>
) )
@ -135,7 +139,7 @@ const FaceDetails = ({ group }) => {
loading={loading} loading={loading}
ref={inputRef} ref={inputRef}
size="mini" size="mini"
placeholder="Label" placeholder={t('people_page.face_group.label_placeholder', 'Label')}
icon="arrow right" icon="arrow right"
value={inputValue} value={inputValue}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
@ -199,6 +203,7 @@ const FaceGroupsWrapper = styled.div`
` `
const PeopleGallery = () => { const PeopleGallery = () => {
const { t } = useTranslation()
const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, { const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, {
variables: { variables: {
limit: 50, limit: 50,
@ -230,7 +235,7 @@ const PeopleGallery = () => {
} }
return ( return (
<Layout title={'People'}> <Layout title={t('title.people', 'People')}>
<Button <Button
loading={recognizeUnlabeledLoading} loading={recognizeUnlabeledLoading}
disabled={recognizeUnlabeledLoading} disabled={recognizeUnlabeledLoading}
@ -239,12 +244,15 @@ const PeopleGallery = () => {
}} }}
> >
<Icon name="sync" /> <Icon name="sync" />
Recognize unlabeled faces {t(
'people_page.recognize_unlabeled_faces_button',
'Recognize unlabeled faces'
)}
</Button> </Button>
<FaceGroupsWrapper ref={containerElem}>{faces}</FaceGroupsWrapper> <FaceGroupsWrapper ref={containerElem}>{faces}</FaceGroupsWrapper>
<PaginateLoader <PaginateLoader
active={!finishedLoadingMore && !loading} active={!finishedLoadingMore && !loading}
text="Loading more people" text={t('general.loading.paginate.faces', 'Loading more people')}
/> />
</Layout> </Layout>
) )

View File

@ -4,6 +4,7 @@ import Layout from '../../Layout'
import AlbumGallery from '../../components/albumGallery/AlbumGallery' import AlbumGallery from '../../components/albumGallery/AlbumGallery'
import styled from 'styled-components' import styled from 'styled-components'
import { gql, useQuery } from '@apollo/client' import { gql, useQuery } from '@apollo/client'
import { useTranslation } from 'react-i18next'
export const SHARE_ALBUM_QUERY = gql` export const SHARE_ALBUM_QUERY = gql`
query shareAlbumQuery( query shareAlbumQuery(
@ -73,6 +74,7 @@ const AlbumSharePageWrapper = styled.div`
` `
const AlbumSharePage = ({ albumID, token, password }) => { const AlbumSharePage = ({ albumID, token, password }) => {
const { t } = useTranslation()
const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, { const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, {
variables: { variables: {
id: albumID, id: albumID,
@ -88,14 +90,18 @@ const AlbumSharePage = ({ albumID, token, password }) => {
} }
if (loading) { if (loading) {
return 'Loading...' return t('general.loading.default', 'Loading...')
} }
const album = data.album const album = data.album
return ( return (
<AlbumSharePageWrapper data-testid="AlbumSharePage"> <AlbumSharePageWrapper data-testid="AlbumSharePage">
<Layout title={album ? album.title : 'Loading album'}> <Layout
title={
album ? album.title : t('general.loading.album', 'Loading album')
}
>
<AlbumGallery <AlbumGallery
album={album} album={album}
customAlbumLink={albumId => `/share/${token}/${albumId}`} customAlbumLink={albumId => `/share/${token}/${albumId}`}

View File

@ -11,6 +11,7 @@ import {
} from '../../helpers/authentication' } from '../../helpers/authentication'
import AlbumSharePage from './AlbumSharePage' import AlbumSharePage from './AlbumSharePage'
import MediaSharePage from './MediaSharePage' import MediaSharePage from './MediaSharePage'
import { useTranslation } from 'react-i18next'
export const SHARE_TOKEN_QUERY = gql` export const SHARE_TOKEN_QUERY = gql`
query SharePageToken($token: String!, $password: String) { query SharePageToken($token: String!, $password: String) {
@ -71,6 +72,8 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql`
` `
const AuthorizedTokenRoute = ({ match }) => { const AuthorizedTokenRoute = ({ match }) => {
const { t } = useTranslation()
const token = match.params.token const token = match.params.token
const password = getSharePassword(token) const password = getSharePassword(token)
@ -122,7 +125,7 @@ const AuthorizedTokenRoute = ({ match }) => {
return <MediaSharePage media={data.shareToken.media} /> return <MediaSharePage media={data.shareToken.media} />
} }
return <h1>Share not found</h1> return <h1>{t('share_page.share_not_found', 'Share not found')}</h1>
} }
AuthorizedTokenRoute.propTypes = { AuthorizedTokenRoute.propTypes = {
@ -138,6 +141,8 @@ const ProtectedTokenEnterPassword = ({
refetchWithPassword, refetchWithPassword,
loading = false, loading = false,
}) => { }) => {
const { t } = useTranslation()
const [passwordValue, setPasswordValue] = useState('') const [passwordValue, setPasswordValue] = useState('')
const [invalidPassword, setInvalidPassword] = useState(false) const [invalidPassword, setInvalidPassword] = useState(false)
@ -150,7 +155,9 @@ const ProtectedTokenEnterPassword = ({
if (invalidPassword && !loading) { if (invalidPassword && !loading) {
errorMessage = ( errorMessage = (
<Message negative> <Message negative>
<Message.Content>Wrong password, please try again.</Message.Content> <Message.Content>
{t('share_page.wrong_password', 'Wrong password, please try again.')}
</Message.Content>
</Message> </Message>
) )
} }
@ -158,18 +165,23 @@ const ProtectedTokenEnterPassword = ({
return ( return (
<MessageContainer> <MessageContainer>
<Header as="h1" style={{ fontWeight: 400 }}> <Header as="h1" style={{ fontWeight: 400 }}>
Protected share {t('share_page.protected_share.title', 'Protected share')}
</Header> </Header>
<p>This share is protected with a password.</p> <p>
{t(
'share_page.protected_share.description',
'This share is protected with a password.'
)}
</p>
<Form> <Form>
<Form.Field> <Form.Field>
<label>Password</label> <label>{t('login_page.field.password', 'Password')}</label>
<Input <Input
loading={loading} loading={loading}
disabled={loading} disabled={loading}
onKeyUp={event => event.key == 'Enter' && onSubmit()} onKeyUp={event => event.key == 'Enter' && onSubmit()}
onChange={e => setPasswordValue(e.target.value)} onChange={e => setPasswordValue(e.target.value)}
placeholder="Password" placeholder={t('login_page.field.password', 'Password')}
type="password" type="password"
icon={<Icon onClick={onSubmit} link name="arrow right" />} icon={<Icon onClick={onSubmit} link name="arrow right" />}
/> />
@ -186,6 +198,8 @@ ProtectedTokenEnterPassword.propTypes = {
} }
const TokenRoute = ({ match }) => { const TokenRoute = ({ match }) => {
const { t } = useTranslation()
const token = match.params.token const token = match.params.token
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery(
@ -203,8 +217,13 @@ const TokenRoute = ({ match }) => {
if (error.message == 'GraphQL error: share not found') { if (error.message == 'GraphQL error: share not found') {
return ( return (
<MessageContainer> <MessageContainer>
<h1>Share not found</h1> <h1>{t('share_page.share_not_found', 'Share not found')}</h1>
<p>Maybe the share has expired or has been deleted.</p> <p>
{t(
'share_page.share_not_found_description',
'Maybe the share has expired or has been deleted.'
)}
</p>
</MessageContainer> </MessageContainer>
) )
} }
@ -225,7 +244,7 @@ const TokenRoute = ({ match }) => {
) )
} }
if (loading) return 'Loading...' if (loading) return t('general.loading.default', 'Loading...')
return <AuthorizedTokenRoute match={match} /> return <AuthorizedTokenRoute match={match} />
} }
@ -234,16 +253,20 @@ TokenRoute.propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
} }
const SharePage = ({ match }) => ( const SharePage = ({ match }) => {
<Switch> const { t } = useTranslation()
<Route path={`${match.url}/:token`}>
{({ match }) => { return (
return <TokenRoute match={match} /> <Switch>
}} <Route path={`${match.url}/:token`}>
</Route> {({ match }) => {
<Route path="/">Route not found</Route> return <TokenRoute match={match} />
</Switch> }}
) </Route>
<Route path="/">{t('routes.page_not_found', 'Page not found')}</Route>
</Switch>
)
}
SharePage.propTypes = { SharePage.propTypes = {
...RouterProps, ...RouterProps,

View File

@ -18,6 +18,8 @@ import SharePage, {
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload' import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload'
import { SHARE_ALBUM_QUERY } from './AlbumSharePage' import { SHARE_ALBUM_QUERY } from './AlbumSharePage'
require('../../localization').default()
describe('load correct share page, based on graphql query', () => { describe('load correct share page, based on graphql query', () => {
const token = 'TOKEN123' const token = 'TOKEN123'

View File

@ -6,6 +6,7 @@ import PresentView from './presentView/PresentView'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { SidebarContext } from '../sidebar/Sidebar' import { SidebarContext } from '../sidebar/Sidebar'
import MediaSidebar from '../sidebar/MediaSidebar' import MediaSidebar from '../sidebar/MediaSidebar'
import { useTranslation } from 'react-i18next'
const Gallery = styled.div` const Gallery = styled.div`
display: flex; display: flex;
@ -41,6 +42,7 @@ const PhotoGallery = ({
previousImage, previousImage,
onFavorite, onFavorite,
}) => { }) => {
const { t } = useTranslation()
const { updateSidebar } = useContext(SidebarContext) const { updateSidebar } = useContext(SidebarContext)
const activeImage = media && activeIndex != -1 && media[activeIndex] const activeImage = media && activeIndex != -1 && media[activeIndex]
@ -80,7 +82,9 @@ const PhotoGallery = ({
return ( return (
<ClearWrap> <ClearWrap>
<Gallery> <Gallery>
<Loader active={loading}>Loading images</Loader> <Loader active={loading}>
{t('general.loading.media', 'Loading media')}
</Loader>
{getPhotoElements(updateSidebar)} {getPhotoElements(updateSidebar)}
<PhotoFiller /> <PhotoFiller />
</Gallery> </Gallery>

View File

@ -4,6 +4,7 @@ import { Route, Switch, Redirect } from 'react-router-dom'
import { Loader } from 'semantic-ui-react' import { Loader } from 'semantic-ui-react'
import Layout from '../../Layout' import Layout from '../../Layout'
import { clearTokenCookie } from '../../helpers/authentication' import { clearTokenCookie } from '../../helpers/authentication'
import { useTranslation } from 'react-i18next'
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute')) const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
@ -26,11 +27,13 @@ const SettingsPage = React.lazy(() =>
) )
const Routes = () => { const Routes = () => {
const { t } = useTranslation()
return ( return (
<React.Suspense <React.Suspense
fallback={ fallback={
<Layout> <Layout>
<Loader active>Loading page</Loader> <Loader active>{t('general.loading.page', 'Loading page')}</Loader>
</Layout> </Layout>
} }
> >
@ -51,7 +54,11 @@ const Routes = () => {
<AuthorizedRoute path="/people/:person?" component={PeoplePage} /> <AuthorizedRoute path="/people/:person?" component={PeoplePage} />
<AuthorizedRoute admin path="/settings" component={SettingsPage} /> <AuthorizedRoute admin path="/settings" component={SettingsPage} />
<Route path="/" exact render={() => <Redirect to="/photos" />} /> <Route path="/" exact render={() => <Redirect to="/photos" />} />
<Route render={() => <div>Page not found</div>} /> <Route
render={() => (
<div>{t('routes.page_not_found', 'Page not found')}</div>
)}
/>
</Switch> </Switch>
</React.Suspense> </React.Suspense>
) )

View File

@ -10,6 +10,8 @@ import {
} from '@testing-library/react' } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
require('../../localization').default()
describe('routes', () => { describe('routes', () => {
test('unauthorized root path should navigate to login page', async () => { test('unauthorized root path should navigate to login page', async () => {
jest.mock('../../Pages/LoginPage/LoginPage.js', () => () => ( jest.mock('../../Pages/LoginPage/LoginPage.js', () => () => (

View File

@ -169,6 +169,7 @@ export const MetadataInfo = ({ media }) => {
exif.focalLength = `${exif.focalLength}mm` exif.focalLength = `${exif.focalLength}mm`
} }
const flash = flashLookup(t)
if (!isNil(exif.flash) && flash[exif.flash]) { if (!isNil(exif.flash) && flash[exif.flash]) {
exif.flash = flash[exif.flash] exif.flash = flash[exif.flash]
} }
@ -249,7 +250,7 @@ const exposureProgramsLookup = t => ({
}) })
// From https://exiftool.org/TagNames/EXIF.html#Flash // From https://exiftool.org/TagNames/EXIF.html#Flash
const flash = t => { const flashLookup = t => {
const values = { const values = {
no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'), no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'),
fired: t('sidebar.media.exif.flash.fired', 'Fired'), fired: t('sidebar.media.exif.flash.fired', 'Fired'),

View File

@ -4,6 +4,8 @@ import React from 'react'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { MetadataInfo } from './MediaSidebar' import { MetadataInfo } from './MediaSidebar'
require('../../localization').default()
describe('MetadataInfo', () => { describe('MetadataInfo', () => {
test('without EXIF information', async () => { test('without EXIF information', async () => {
const media = { const media = {
@ -33,11 +35,11 @@ describe('MetadataInfo', () => {
expect(screen.queryByText('Maker')).not.toBeInTheDocument() expect(screen.queryByText('Maker')).not.toBeInTheDocument()
expect(screen.queryByText('Lens')).not.toBeInTheDocument() expect(screen.queryByText('Lens')).not.toBeInTheDocument()
expect(screen.queryByText('Program')).not.toBeInTheDocument() expect(screen.queryByText('Program')).not.toBeInTheDocument()
expect(screen.queryByText('Date Shot')).not.toBeInTheDocument() expect(screen.queryByText('Date shot')).not.toBeInTheDocument()
expect(screen.queryByText('Exposure')).not.toBeInTheDocument() expect(screen.queryByText('Exposure')).not.toBeInTheDocument()
expect(screen.queryByText('Aperture')).not.toBeInTheDocument() expect(screen.queryByText('Aperture')).not.toBeInTheDocument()
expect(screen.queryByText('ISO')).not.toBeInTheDocument() expect(screen.queryByText('ISO')).not.toBeInTheDocument()
expect(screen.queryByText('Focal Length')).not.toBeInTheDocument() expect(screen.queryByText('Focal length')).not.toBeInTheDocument()
expect(screen.queryByText('Flash')).not.toBeInTheDocument() expect(screen.queryByText('Flash')).not.toBeInTheDocument()
}) })
@ -77,7 +79,7 @@ describe('MetadataInfo', () => {
expect(screen.getByText('Program')).toBeInTheDocument() expect(screen.getByText('Program')).toBeInTheDocument()
expect(screen.getByText('Canon EOS R')).toBeInTheDocument() expect(screen.getByText('Canon EOS R')).toBeInTheDocument()
expect(screen.getByText('Date Shot')).toBeInTheDocument() expect(screen.getByText('Date shot')).toBeInTheDocument()
expect(screen.getByText('Exposure')).toBeInTheDocument() expect(screen.getByText('Exposure')).toBeInTheDocument()
expect(screen.getByText('1/60')).toBeInTheDocument() expect(screen.getByText('1/60')).toBeInTheDocument()
@ -91,7 +93,7 @@ describe('MetadataInfo', () => {
expect(screen.getByText('ISO')).toBeInTheDocument() expect(screen.getByText('ISO')).toBeInTheDocument()
expect(screen.getByText('100')).toBeInTheDocument() expect(screen.getByText('100')).toBeInTheDocument()
expect(screen.getByText('Focal Length')).toBeInTheDocument() expect(screen.getByText('Focal length')).toBeInTheDocument()
expect(screen.getByText('24mm')).toBeInTheDocument() expect(screen.getByText('24mm')).toBeInTheDocument()
expect(screen.getByText('Flash')).toBeInTheDocument() expect(screen.getByText('Flash')).toBeInTheDocument()

View File

@ -10,6 +10,7 @@ import { FavoritesCheckbox } from '../AlbumFilter'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../PaginateLoader' import PaginateLoader from '../PaginateLoader'
import LazyLoad from '../../helpers/LazyLoad' import LazyLoad from '../../helpers/LazyLoad'
import { useTranslation } from 'react-i18next'
const MY_TIMELINE_QUERY = gql` const MY_TIMELINE_QUERY = gql`
query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) { query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
@ -54,6 +55,7 @@ const GalleryWrapper = styled.div`
` `
const TimelineGallery = () => { const TimelineGallery = () => {
const { t } = useTranslation()
const [activeIndex, setActiveIndex] = useState({ const [activeIndex, setActiveIndex] = useState({
dateGroup: -1, dateGroup: -1,
albumGroup: -1, albumGroup: -1,
@ -212,7 +214,9 @@ const TimelineGallery = () => {
return ( return (
<> <>
<Loader active={loading}>Loading timeline</Loader> <Loader active={loading}>
{t('general.loading.timeline', 'Loading timeline')}
</Loader>
<FavoritesCheckbox <FavoritesCheckbox
onlyFavorites={onlyFavorites} onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites} setOnlyFavorites={setOnlyFavorites}
@ -220,7 +224,7 @@ const TimelineGallery = () => {
<GalleryWrapper ref={containerElem}>{timelineGroups}</GalleryWrapper> <GalleryWrapper ref={containerElem}>{timelineGroups}</GalleryWrapper>
<PaginateLoader <PaginateLoader
active={!finishedLoadingMore && !loading} active={!finishedLoadingMore && !loading}
text="Loading more media" text={t('general.loading.paginate.media', 'Loading more media')}
/> />
{presenting && ( {presenting && (
<PresentView <PresentView

View File

@ -8,24 +8,9 @@ import client from './apolloClient'
import { ApolloProvider } from '@apollo/client' import { ApolloProvider } from '@apollo/client'
import { BrowserRouter as Router } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom'
import i18n from 'i18next' import i18n from 'i18next'
import { initReactI18next } from 'react-i18next' import setupLocalization from './localization'
i18n.use(initReactI18next).init({ setupLocalization()
resources: {
en: {
translation: {
'Welcome to React': 'Welcome to React and react-i18next',
},
},
},
lng: 'en',
fallbackLng: 'en',
returnNull: false,
interpolation: {
escapeValue: false,
},
})
import('../extractedTranslations/da/translation.json').then(danish => { import('../extractedTranslations/da/translation.json').then(danish => {
i18n.addResourceBundle('da', 'translation', danish) i18n.addResourceBundle('da', 'translation', danish)

21
ui/src/localization.js Normal file
View File

@ -0,0 +1,21 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
export default function setupLocalization() {
i18n.use(initReactI18next).init({
resources: {
en: {
translation: {
'Welcome to React': 'Welcome to React and react-i18next',
},
},
},
lng: 'en',
fallbackLng: 'en',
returnNull: false,
interpolation: {
escapeValue: false,
},
})
}