🌐(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",
"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": {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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