🌐(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:
Emmanuel Pelletier
2024-07-18 13:59:50 +02:00
parent 84c2986c01
commit d2dba511e2
19 changed files with 1670 additions and 73 deletions

View 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"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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>
) )
} }

View 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,
},
})

View File

@@ -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>
)} )}

View File

@@ -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 />
)
} }

View File

@@ -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
/>
)
} }

View File

@@ -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>
)} )}

View File

@@ -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>
) )
} }

View File

@@ -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 />
} }

View 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": ""
}
}

View File

@@ -0,0 +1,5 @@
{
"join": {
"heading": ""
}
}

View 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"
}
}

View File

@@ -0,0 +1,5 @@
{
"join": {
"heading": "Verify your settings before joining"
}
}

View 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"
}
}

View File

@@ -0,0 +1,5 @@
{
"join": {
"heading": "Vérifiez vos paramètres"
}
}

View File

@@ -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>