Skip to content

Commit d7084a3

Browse files
committed
feat(i18n): integrate internationalization support into dashboard
- Added i18next and react-i18next for localization support. - Created i18n configuration in `src/i18n.ts` and set up language detection. - Introduced `I18nProvider` to manage i18n context and resource updates. - Added localization files for English and Chinese (Simplified) with necessary translations. - Updated `ErrorElement` component to utilize translations for error messages. - Enhanced dashboard documentation with i18n usage guidelines. Signed-off-by: Innei <tukon479@gmail.com>
1 parent c8b7fcc commit d7084a3

File tree

16 files changed

+236
-24
lines changed

16 files changed

+236
-24
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ class PhotoLoader {
192192
2. Keep components focused - use hooks and component composition.
193193
3. Follow React best practices - proper Context usage, state management.
194194
4. Use TypeScript strictly - leverage type safety throughout.
195+
5. Build React features out of small, atomic components. Push data fetching, stores, and providers down to the feature or tab that actually needs them so switching views unmounts unused logic and prevents runaway updates instead of centralizing everything in a mega component.
195196

196197
### i18n Guidelines
197198

be/apps/dashboard/agents.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,20 @@ Providers:
4747
- LazyMotion + MotionConfig
4848
- TanStack QueryClientProvider
4949
- Jotai Provider with a global store
50+
- I18nProvider (react-i18next)
5051
- Event, Context menu, and settings sync providers
5152
- StableRouterProvider to stabilize routing data and navigation
5253
- ModalContainer and Toaster
5354
- Add new cross-cutting providers here, keeping order and side effects in mind.
5455

56+
### i18n usage
57+
58+
- Localization lives under `locales/dashboard/*.json`. Follow the same flat-key rules documented in the repo root AGENTS instructions (no nested parents vs. leaf conflicts). Update English first before translating other languages.
59+
- Resource metadata/types live in `src/@types/constants.ts`, `src/@types/resources.ts`, and `src/@types/i18next.d.ts`. Keep these files in sync when adding new locales or namespaces.
60+
- `src/i18n.ts` configures `i18next` with `react-i18next` + `i18next-browser-languagedetector`. The singleton is stored in a jotai atom for hot-refresh support.
61+
- Use `useTranslation()` from `react-i18next` inside components. Example: `const { t } = useTranslation(); <span>{t('nav.overview')}</span>`.
62+
- Trigger `EventBus.dispatch('I18N_UPDATE')` in development when you need to reload resources without a full refresh.
63+
5564
Animation rules:
5665

5766
- Always use m.\* components imported from motion/react.

be/apps/dashboard/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"clsx": "2.1.1",
4343
"es-toolkit": "1.41.0",
4444
"foxact": "0.2.49",
45+
"i18next": "25.6.2",
46+
"i18next-browser-languagedetector": "8.2.0",
4547
"immer": "10.2.0",
4648
"jotai": "2.15.1",
4749
"lucide-react": "0.553.0",
@@ -51,6 +53,7 @@
5153
"radix-ui": "1.4.3",
5254
"react": "19.2.0",
5355
"react-dom": "19.2.0",
56+
"react-i18next": "16.3.1",
5457
"react-router": "7.9.5",
5558
"react-scan": "0.4.3",
5659
"sonner": "2.0.7",
@@ -105,4 +108,4 @@
105108
"eslint --fix"
106109
]
107110
}
108-
}
111+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const langs = ['en', 'zh-CN'] as const
2+
3+
export const currentSupportedLanguages = [...langs].sort() as string[]
4+
export type DashboardSupportedLanguages = (typeof langs)[number]
5+
6+
export const ns = ['dashboard'] as const
7+
export const defaultNS = 'dashboard' as const
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { defaultNS, ns } from './constants'
2+
import type { resources } from './resources'
3+
4+
declare module 'i18next' {
5+
interface CustomTypeOptions {
6+
ns: typeof ns
7+
defaultNS: typeof defaultNS
8+
resources: (typeof resources)['en']
9+
}
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import en from '@locales/dashboard/en.json'
2+
import zhCn from '@locales/dashboard/zh-CN.json'
3+
4+
import type { DashboardSupportedLanguages, ns } from './constants'
5+
6+
export const resources = {
7+
en: {
8+
dashboard: en,
9+
},
10+
'zh-CN': {
11+
dashboard: zhCn,
12+
},
13+
} satisfies Record<DashboardSupportedLanguages, Record<(typeof ns)[number], Record<string, string>>>

be/apps/dashboard/src/components/common/ErrorElement.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Button } from '@afilmory/ui'
22
import { repository } from '@pkg'
33
import { useEffect, useRef } from 'react'
4+
import { useTranslation } from 'react-i18next'
45
import { isRouteErrorResponse, useRouteError } from 'react-router'
56

67
import { attachOpenInEditor } from '~/lib/dev'
78

89
export function ErrorElement() {
910
const error = useRouteError()
11+
const { t } = useTranslation()
1012
const message = isRouteErrorResponse(error)
1113
? `${error.status} ${error.statusText}`
1214
: error instanceof Error
@@ -55,8 +57,8 @@ export function ErrorElement() {
5557
/>
5658
</svg>
5759
</div>
58-
<h1 className="text-text mb-2 text-3xl font-medium">Something went wrong</h1>
59-
<p className="text-text-secondary text-lg">We encountered an unexpected error</p>
60+
<h1 className="text-text mb-2 text-3xl font-medium">{t('error.boundary.title')}</h1>
61+
<p className="text-text-secondary text-lg">{t('error.boundary.description')}</p>
6062
</div>
6163

6264
{/* Error message */}
@@ -81,19 +83,19 @@ export function ErrorElement() {
8183
onClick={() => (window.location.href = '/')}
8284
className="bg-material-opaque text-text-vibrant hover:bg-control-enabled/90 h-10 flex-1 border-0 font-medium transition-colors"
8385
>
84-
Reload Application
86+
{t('error.boundary.reload')}
8587
</Button>
8688
<Button
8789
onClick={() => window.history.back()}
8890
className="bg-material-thin text-text border-fill-tertiary hover:bg-fill-tertiary h-10 flex-1 border font-medium transition-colors"
8991
>
90-
Go Back
92+
{t('error.boundary.go-back')}
9193
</Button>
9294
</div>
9395

9496
{/* Help text */}
9597
<div className="text-center">
96-
<p className="text-text-secondary mb-3 text-sm">If this problem persists, please report it to our team.</p>
98+
<p className="text-text-secondary mb-3 text-sm">{t('error.boundary.help')}</p>
9799
<a
98100
href={`${repository.url}/issues/new?title=${encodeURIComponent(
99101
`Error: ${message}`,
@@ -107,7 +109,7 @@ export function ErrorElement() {
107109
<svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
108110
<path d="M12 0C5.374 0 0 5.373 0 12 0 17.302 3.438 21.8 8.207 23.387c.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
109111
</svg>
110-
Report on GitHub
112+
{t('error.boundary.report')}
111113
</a>
112114
</div>
113115
</div>

be/apps/dashboard/src/i18n.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import i18next from 'i18next'
2+
import LanguageDetector from 'i18next-browser-languagedetector'
3+
import { atom } from 'jotai'
4+
import { initReactI18next } from 'react-i18next'
5+
6+
import { currentSupportedLanguages, defaultNS, ns } from './@types/constants'
7+
import { resources } from './@types/resources'
8+
import { jotaiStore } from './lib/jotai'
9+
10+
const i18n = i18next.createInstance()
11+
12+
i18n
13+
.use(LanguageDetector)
14+
.use(initReactI18next)
15+
.init({
16+
fallbackLng: {
17+
default: ['en'],
18+
},
19+
supportedLngs: currentSupportedLanguages,
20+
defaultNS,
21+
ns,
22+
resources,
23+
interpolation: {
24+
escapeValue: false,
25+
},
26+
detection: {
27+
order: ['localStorage', 'navigator', 'htmlTag'],
28+
caches: ['localStorage'],
29+
},
30+
returnNull: false,
31+
})
32+
33+
export const i18nAtom = atom(i18n)
34+
35+
export const getI18n = () => jotaiStore.get(i18nAtom)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
2+
export interface CustomEvent {}
3+
export interface EventBusMap extends CustomEvent {}
4+
5+
class EventBusEvent extends Event {
6+
static type = 'EventBusEvent'
7+
constructor(
8+
public _type: string,
9+
public data: any,
10+
) {
11+
super(EventBusEvent.type)
12+
}
13+
}
14+
15+
type IDispatcher<E> = <T extends keyof E>(...args: E[T] extends never ? [event: T] : [event: T, data: E[T]]) => void
16+
type AnyObject = Record<string, any>
17+
class EventBusStatic<E extends AnyObject> {
18+
constructor() {
19+
this.dispatch = this.dispatch.bind(this)
20+
this.subscribe = this.subscribe.bind(this)
21+
this.unsubscribe = this.unsubscribe.bind(this)
22+
}
23+
dispatch: IDispatcher<E> = <T extends keyof E>(event: T, data?: E[T]) => {
24+
window.dispatchEvent(new EventBusEvent(event as string, data))
25+
}
26+
27+
subscribe<T extends keyof E>(event: T, callback: (data: E[T]) => void) {
28+
const handler = (e: any) => {
29+
if (e instanceof EventBusEvent && e._type === event) {
30+
callback(e.data)
31+
}
32+
}
33+
window.addEventListener(EventBusEvent.type, handler)
34+
35+
return this.unsubscribe.bind(this, event as string, handler)
36+
}
37+
38+
unsubscribe(_event: string, handler: (e: any) => void) {
39+
window.removeEventListener(EventBusEvent.type, handler)
40+
}
41+
}
42+
43+
export const EventBus = new EventBusStatic<EventBusMap>()
44+
export const createEventBus = <E extends AnyObject>() => new EventBusStatic<E>()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import i18next from 'i18next'
2+
import { useAtom } from 'jotai'
3+
import type { FC, PropsWithChildren } from 'react'
4+
import { useEffect } from 'react'
5+
import { I18nextProvider } from 'react-i18next'
6+
7+
import { EventBus } from '~/lib/event-bus'
8+
9+
import { i18nAtom } from '../i18n'
10+
11+
export const I18nProvider: FC<PropsWithChildren> = ({ children }) => {
12+
const [currentI18nInstance, setInstance] = useAtom(i18nAtom)
13+
14+
useEffect(() => {
15+
if (!import.meta.env.DEV) {
16+
return
17+
}
18+
19+
return EventBus.subscribe('I18N_UPDATE', () => {
20+
const nextI18n = i18next.cloneInstance({})
21+
setInstance(nextI18n)
22+
})
23+
}, [setInstance])
24+
25+
return <I18nextProvider i18n={currentI18nInstance}>{children}</I18nextProvider>
26+
}
27+
28+
declare module '~/lib/event-bus' {
29+
interface CustomEvent {
30+
I18N_UPDATE: string
31+
}
32+
}

0 commit comments

Comments
 (0)