🌐(frontend) init i18next
- dynamically load locale files for smaller footprint - have a namespace for each feature. At first I'd figured I'd put each namespace in its correct feature folder but it's kinda cumbersome to manage if we want to link that to i18n management services like crowdin…
This commit is contained in:
7
src/frontend/i18next-parser.config.json
Normal file
7
src/frontend/i18next-parser.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"defaultNamespace": "global",
|
||||||
|
"input": ["src/**/*.{ts,tsx}", "!src/styled-system/**/*", "!src/**/*.d.ts"],
|
||||||
|
"output": "src/locales/$LOCALE/$NAMESPACE.json",
|
||||||
|
"createOldCatalogs": false,
|
||||||
|
"locales": ["en", "fr", "de"]
|
||||||
|
}
|
||||||
1547
src/frontend/package-lock.json
generated
1547
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,17 +7,24 @@
|
|||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
"build": "panda codegen && tsc -b && vite build",
|
"build": "panda codegen && tsc -b && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"i18n:extract": "i18next -c i18next-parser.config.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@livekit/components-react": "2.3.3",
|
"@livekit/components-react": "2.3.3",
|
||||||
"@livekit/components-styles": "1.0.12",
|
"@livekit/components-styles": "1.0.12",
|
||||||
"@pandacss/preset-panda": "0.41.0",
|
"@pandacss/preset-panda": "0.41.0",
|
||||||
"@tanstack/react-query": "5.49.2",
|
"@tanstack/react-query": "5.49.2",
|
||||||
|
"hoofd": "1.7.1",
|
||||||
|
"i18next": "23.12.1",
|
||||||
|
"i18next-browser-languagedetector": "8.0.0",
|
||||||
|
"i18next-parser": "9.0.0",
|
||||||
|
"i18next-resources-to-backend": "1.2.1",
|
||||||
"livekit-client": "2.3.1",
|
"livekit-client": "2.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-aria-components": "1.2.1",
|
"react-aria-components": "1.2.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-i18next": "14.1.3",
|
||||||
"wouter": "3.3.0"
|
"wouter": "3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import '@livekit/components-styles'
|
|||||||
import '@/styles/index.css'
|
import '@/styles/index.css'
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useLang } from 'hoofd'
|
||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
import { Home } from './routes/Home'
|
import { Home } from './routes/Home'
|
||||||
import { NotFound } from './routes/NotFound'
|
import { NotFound } from './routes/NotFound'
|
||||||
import { RoomRoute } from '@/features/rooms'
|
import { RoomRoute } from '@/features/rooms'
|
||||||
|
import './i18n/init'
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
useLang(i18n.language)
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Box } from '@/layout/Box'
|
import { Box } from '@/layout/Box'
|
||||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||||
|
|
||||||
@@ -6,12 +7,11 @@ export const Join = ({
|
|||||||
}: {
|
}: {
|
||||||
onSubmit: (choices: LocalUserChoices) => void
|
onSubmit: (choices: LocalUserChoices) => void
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('rooms')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box title="Verify your settings before joining" withBackButton>
|
<Box title={t('join.heading')} withBackButton>
|
||||||
<PreJoin
|
<PreJoin persistUserChoices onSubmit={onSubmit} />
|
||||||
persistUserChoices
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/frontend/src/i18n/init.ts
Normal file
23
src/frontend/src/i18n/init.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
const i18nDefaultNamespace = 'global'
|
||||||
|
|
||||||
|
i18n.setDefaultNamespace(i18nDefaultNamespace)
|
||||||
|
i18n
|
||||||
|
.use(
|
||||||
|
resourcesToBackend((language: string, namespace: string) => {
|
||||||
|
return import(`../locales/${language}/${namespace}.json`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
i18n.init({
|
||||||
|
supportedLngs: ['en', 'fr'],
|
||||||
|
fallbackLng: 'en',
|
||||||
|
ns: i18nDefaultNamespace,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Box as BoxDiv, H, Link } from '@/primitives'
|
import { Box as BoxDiv, H, Link } from '@/primitives'
|
||||||
|
|
||||||
export type BoxProps = {
|
export type BoxProps = {
|
||||||
@@ -12,6 +13,7 @@ export const Box = ({
|
|||||||
title = '',
|
title = '',
|
||||||
withBackButton = false,
|
withBackButton = false,
|
||||||
}: BoxProps) => {
|
}: BoxProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<BoxDiv asScreen>
|
<BoxDiv asScreen>
|
||||||
{!!title && <H lvl={1}>{title}</H>}
|
{!!title && <H lvl={1}>{title}</H>}
|
||||||
@@ -19,7 +21,7 @@ export const Box = ({
|
|||||||
{!!withBackButton && (
|
{!!withBackButton && (
|
||||||
<p>
|
<p>
|
||||||
<Link to="/" size="small">
|
<Link to="/" size="small">
|
||||||
Back to homescreen
|
{t('backToHome')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export const ErrorScreen = () => {
|
export const ErrorScreen = () => {
|
||||||
return (
|
const { t } = useTranslation()
|
||||||
<BoxScreen title="An error occured while loading the page" withBackButton />
|
return <BoxScreen title={t('error.heading')} withBackButton />
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export const ForbiddenScreen = () => {
|
export const ForbiddenScreen = () => {
|
||||||
return (
|
const { t } = useTranslation()
|
||||||
<BoxScreen
|
return <BoxScreen title={t('forbidden.heading')} withBackButton />
|
||||||
title="You don't have the permission to view this page"
|
|
||||||
withBackButton
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { flex } from '@/styled-system/patterns'
|
import { flex } from '@/styled-system/patterns'
|
||||||
import { A, Badge, Text } from '@/primitives'
|
import { A, Badge, Text } from '@/primitives'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const { user, isLoggedIn } = useUser()
|
const { user, isLoggedIn } = useUser()
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -26,16 +29,16 @@ export const Header = () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Text bold variant="h1" margin={false}>
|
<Text bold variant="h1" margin={false}>
|
||||||
Meet
|
<Link to="/">{t('app')}</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isLoggedIn === false && <A href={authUrl()}>Login</A>}
|
{isLoggedIn === false && <A href={authUrl()}>{t('login')}</A>}
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<p className={flex({ gap: 1, align: 'center' })}>
|
<p className={flex({ gap: 1, align: 'center' })}>
|
||||||
<Badge>{user.email}</Badge>
|
<Badge>{user.email}</Badge>
|
||||||
<A href={logoutUrl()} size="small">
|
<A href={logoutUrl()} size="small">
|
||||||
Logout
|
{t('logout')}
|
||||||
</A>
|
</A>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
import { Screen } from './Screen'
|
import { Screen } from './Screen'
|
||||||
|
|
||||||
export const LoadingScreen = ({ asBox = false }: { asBox?: boolean }) => {
|
export const LoadingScreen = ({ asBox = false }: { asBox?: boolean }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
// show the loading screen only after a little while to prevent flash of texts
|
// show the loading screen only after a little while to prevent flash of texts
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -15,7 +17,7 @@ export const LoadingScreen = ({ asBox = false }: { asBox?: boolean }) => {
|
|||||||
const Container = asBox ? BoxScreen : Screen
|
const Container = asBox ? BoxScreen : Screen
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<p>Loading…</p>
|
<p>{t('loading')}</p>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
|
||||||
export const NotFoundScreen = () => {
|
export const NotFoundScreen = () => {
|
||||||
return <BoxScreen title="Page not found" withBackButton />
|
const { t } = useTranslation()
|
||||||
|
return <BoxScreen title={t('notFound.heading')} withBackButton />
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/frontend/src/locales/de/global.json
Normal file
24
src/frontend/src/locales/de/global.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"backToHome": "",
|
||||||
|
"error": {
|
||||||
|
"heading": ""
|
||||||
|
},
|
||||||
|
"forbidden": {
|
||||||
|
"heading": ""
|
||||||
|
},
|
||||||
|
"app": "Meet",
|
||||||
|
"login": "Anmelden",
|
||||||
|
"logout": "",
|
||||||
|
"loading": "",
|
||||||
|
"notFound": {
|
||||||
|
"heading": ""
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"heading": "",
|
||||||
|
"intro": "",
|
||||||
|
"createMeeting": "",
|
||||||
|
"login": "",
|
||||||
|
"or": "",
|
||||||
|
"copyMeetingUrl": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/frontend/src/locales/de/rooms.json
Normal file
5
src/frontend/src/locales/de/rooms.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"join": {
|
||||||
|
"heading": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/frontend/src/locales/en/global.json
Normal file
24
src/frontend/src/locales/en/global.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"backToHome": "Back to homescreen",
|
||||||
|
"error": {
|
||||||
|
"heading": "An error occured while loading the page"
|
||||||
|
},
|
||||||
|
"forbidden": {
|
||||||
|
"heading": "You don't have the permission to view this page"
|
||||||
|
},
|
||||||
|
"app": "Meet",
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Logout",
|
||||||
|
"loading": "Loading…",
|
||||||
|
"notFound": {
|
||||||
|
"heading": ""
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"heading": "Welcome in Meet",
|
||||||
|
"intro": "What do you want to do? You can either:",
|
||||||
|
"createMeeting": "Create a conference call",
|
||||||
|
"login": "Login to create a conference call",
|
||||||
|
"or": "Or",
|
||||||
|
"copyMeetingUrl": "copy a meeting URL in your browser address bar to join an existing conference call"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/frontend/src/locales/en/rooms.json
Normal file
5
src/frontend/src/locales/en/rooms.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"join": {
|
||||||
|
"heading": "Verify your settings before joining"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/frontend/src/locales/fr/global.json
Normal file
24
src/frontend/src/locales/fr/global.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"backToHome": "Retour à l'accueil",
|
||||||
|
"error": {
|
||||||
|
"heading": "Une erreur est survenue lors du chargement de la page"
|
||||||
|
},
|
||||||
|
"forbidden": {
|
||||||
|
"heading": "Accès interdit"
|
||||||
|
},
|
||||||
|
"app": "Meet",
|
||||||
|
"login": "Se connecter",
|
||||||
|
"logout": "Se déconnecter",
|
||||||
|
"loading": "Chargement…",
|
||||||
|
"notFound": {
|
||||||
|
"heading": "Page introuvable"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"heading": "Bienvenue dans Meet",
|
||||||
|
"intro": "Que voulez vous faire ? Vous pouvez :",
|
||||||
|
"createMeeting": "Créer une conférence",
|
||||||
|
"login": "Vous connecter pour créer une conférence",
|
||||||
|
"or": "Ou",
|
||||||
|
"copyMeetingUrl": "copier une URL de conférence dans votre barre d'adresse pour rejoindre une conférence existante"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/frontend/src/locales/fr/rooms.json
Normal file
5
src/frontend/src/locales/fr/rooms.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"join": {
|
||||||
|
"heading": "Vérifiez vos paramètres"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { A, Button, Italic, P, Div, H, Box } from '@/primitives'
|
import { A, Button, Italic, P, Div, H, Box } from '@/primitives'
|
||||||
import { authUrl, useUser } from '@/features/auth'
|
import { authUrl, useUser } from '@/features/auth'
|
||||||
import { navigateToNewRoom } from '@/features/rooms'
|
import { navigateToNewRoom } from '@/features/rooms'
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'homepage' })
|
||||||
const { isLoggedIn } = useUser()
|
const { isLoggedIn } = useUser()
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<Box asScreen>
|
<Box asScreen>
|
||||||
<H lvl={1}>Welcome in Meet</H>
|
<H lvl={1}>{t('heading')}</H>
|
||||||
<P>What do you want to do? You can either:</P>
|
<P>{t('intro')}</P>
|
||||||
<Div marginBottom="gutter">
|
<Div marginBottom="gutter">
|
||||||
<Box variant="subtle" size="sm">
|
<Box variant="subtle" size="sm">
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<Button variant="primary" onPress={() => navigateToNewRoom()}>
|
<Button variant="primary" onPress={() => navigateToNewRoom()}>
|
||||||
Create a conference call
|
{t('createMeeting')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
<A href={authUrl()}>
|
<A href={authUrl()}>{t('login')}</A>
|
||||||
Login to create a conference call
|
|
||||||
</A>
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Div>
|
</Div>
|
||||||
<P>
|
<P>
|
||||||
<Italic>Or</Italic>
|
<Italic>{t('or')}</Italic>
|
||||||
</P>
|
</P>
|
||||||
<Box variant="subtle" size="sm">
|
<Box variant="subtle" size="sm">
|
||||||
<p>
|
<p>{t('copyMeetingUrl')}</p>
|
||||||
copy a meeting URL in your browser address bar to join an existing
|
|
||||||
conference call
|
|
||||||
</p>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Screen>
|
</Screen>
|
||||||
|
|||||||
Reference in New Issue
Block a user