🌐(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",
|
||||
"build": "panda codegen && tsc -b && vite build",
|
||||
"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": {
|
||||
"@livekit/components-react": "2.3.3",
|
||||
"@livekit/components-styles": "1.0.12",
|
||||
"@pandacss/preset-panda": "0.41.0",
|
||||
"@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",
|
||||
"react": "18.2.0",
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "14.1.3",
|
||||
"wouter": "3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,14 +2,19 @@ import '@livekit/components-styles'
|
||||
import '@/styles/index.css'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLang } from 'hoofd'
|
||||
import { Route, Switch } from 'wouter'
|
||||
import { Home } from './routes/Home'
|
||||
import { NotFound } from './routes/NotFound'
|
||||
import { RoomRoute } from '@/features/rooms'
|
||||
import './i18n/init'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
function App() {
|
||||
const { i18n } = useTranslation()
|
||||
useLang(i18n.language)
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Switch>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Box } from '@/layout/Box'
|
||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||
|
||||
@@ -6,12 +7,11 @@ export const Join = ({
|
||||
}: {
|
||||
onSubmit: (choices: LocalUserChoices) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
|
||||
return (
|
||||
<Box title="Verify your settings before joining" withBackButton>
|
||||
<PreJoin
|
||||
persistUserChoices
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<Box title={t('join.heading')} withBackButton>
|
||||
<PreJoin persistUserChoices onSubmit={onSubmit} />
|
||||
</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 { useTranslation } from 'react-i18next'
|
||||
import { Box as BoxDiv, H, Link } from '@/primitives'
|
||||
|
||||
export type BoxProps = {
|
||||
@@ -12,6 +13,7 @@ export const Box = ({
|
||||
title = '',
|
||||
withBackButton = false,
|
||||
}: BoxProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<BoxDiv asScreen>
|
||||
{!!title && <H lvl={1}>{title}</H>}
|
||||
@@ -19,7 +21,7 @@ export const Box = ({
|
||||
{!!withBackButton && (
|
||||
<p>
|
||||
<Link to="/" size="small">
|
||||
Back to homescreen
|
||||
{t('backToHome')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ErrorScreen = () => {
|
||||
return (
|
||||
<BoxScreen title="An error occured while loading the page" withBackButton />
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
return <BoxScreen title={t('error.heading')} withBackButton />
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ForbiddenScreen = () => {
|
||||
return (
|
||||
<BoxScreen
|
||||
title="You don't have the permission to view this page"
|
||||
withBackButton
|
||||
/>
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
return <BoxScreen title={t('forbidden.heading')} withBackButton />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { flex } from '@/styled-system/patterns'
|
||||
import { A, Badge, Text } from '@/primitives'
|
||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const { user, isLoggedIn } = useUser()
|
||||
return (
|
||||
<div
|
||||
@@ -26,16 +29,16 @@ export const Header = () => {
|
||||
>
|
||||
<div>
|
||||
<Text bold variant="h1" margin={false}>
|
||||
Meet
|
||||
<Link to="/">{t('app')}</Link>
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
{isLoggedIn === false && <A href={authUrl()}>Login</A>}
|
||||
{isLoggedIn === false && <A href={authUrl()}>{t('login')}</A>}
|
||||
{!!user && (
|
||||
<p className={flex({ gap: 1, align: 'center' })}>
|
||||
<Badge>{user.email}</Badge>
|
||||
<A href={logoutUrl()} size="small">
|
||||
Logout
|
||||
{t('logout')}
|
||||
</A>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { Screen } from './Screen'
|
||||
|
||||
export const LoadingScreen = ({ asBox = false }: { asBox?: boolean }) => {
|
||||
const { t } = useTranslation()
|
||||
// show the loading screen only after a little while to prevent flash of texts
|
||||
const [show, setShow] = useState(false)
|
||||
useEffect(() => {
|
||||
@@ -15,7 +17,7 @@ export const LoadingScreen = ({ asBox = false }: { asBox?: boolean }) => {
|
||||
const Container = asBox ? BoxScreen : Screen
|
||||
return (
|
||||
<Container>
|
||||
<p>Loading…</p>
|
||||
<p>{t('loading')}</p>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
|
||||
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 { authUrl, useUser } from '@/features/auth'
|
||||
import { navigateToNewRoom } from '@/features/rooms'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
|
||||
export const Home = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'homepage' })
|
||||
const { isLoggedIn } = useUser()
|
||||
return (
|
||||
<Screen>
|
||||
<Box asScreen>
|
||||
<H lvl={1}>Welcome in Meet</H>
|
||||
<P>What do you want to do? You can either:</P>
|
||||
<H lvl={1}>{t('heading')}</H>
|
||||
<P>{t('intro')}</P>
|
||||
<Div marginBottom="gutter">
|
||||
<Box variant="subtle" size="sm">
|
||||
{isLoggedIn ? (
|
||||
<Button variant="primary" onPress={() => navigateToNewRoom()}>
|
||||
Create a conference call
|
||||
{t('createMeeting')}
|
||||
</Button>
|
||||
) : (
|
||||
<p>
|
||||
<A href={authUrl()}>
|
||||
Login to create a conference call
|
||||
</A>
|
||||
<A href={authUrl()}>{t('login')}</A>
|
||||
</p>
|
||||
)}
|
||||
</Box>
|
||||
</Div>
|
||||
<P>
|
||||
<Italic>Or</Italic>
|
||||
<Italic>{t('or')}</Italic>
|
||||
</P>
|
||||
<Box variant="subtle" size="sm">
|
||||
<p>
|
||||
copy a meeting URL in your browser address bar to join an existing
|
||||
conference call
|
||||
</p>
|
||||
<p>{t('copyMeetingUrl')}</p>
|
||||
</Box>
|
||||
</Box>
|
||||
</Screen>
|
||||
|
||||
Reference in New Issue
Block a user