From 76a8d034eea61103d2abff351727d1ead5e96795 Mon Sep 17 00:00:00 2001 From: mengstabketemaw Date: Mon, 15 Jun 2026 16:56:39 +0300 Subject: [PATCH] added internationalization for backoffice --- .../dashboard/pages/DashboardPage.tsx | 68 ++++++++------- apps/backoffice/src/app/i18n/config.ts | 47 +++++++++++ apps/backoffice/src/app/i18n/locales/am.ts | 82 +++++++++++++++++++ apps/backoffice/src/app/i18n/locales/en.ts | 82 +++++++++++++++++++ .../src/app/layouts/components/AppHeader.tsx | 24 +++--- .../src/app/layouts/components/AppSidebar.tsx | 49 ++++++----- .../layouts/components/LanguageSwitcher.tsx | 24 +++--- .../src/app/providers/AppProviders.tsx | 6 +- apps/backoffice/src/main.tsx | 1 + 9 files changed, 303 insertions(+), 80 deletions(-) create mode 100644 apps/backoffice/src/app/i18n/config.ts create mode 100644 apps/backoffice/src/app/i18n/locales/am.ts create mode 100644 apps/backoffice/src/app/i18n/locales/en.ts diff --git a/apps/backoffice/src/app/features/dashboard/pages/DashboardPage.tsx b/apps/backoffice/src/app/features/dashboard/pages/DashboardPage.tsx index 6a713f082..a7c58d48b 100644 --- a/apps/backoffice/src/app/features/dashboard/pages/DashboardPage.tsx +++ b/apps/backoffice/src/app/features/dashboard/pages/DashboardPage.tsx @@ -1,4 +1,5 @@ import { useState, type ElementType, type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { Paper, Text, @@ -64,21 +65,25 @@ const ROLES = [ { name: 'Guest', value: 10, color: '#93c5fd' }, ]; -const KPIS = [ - { label: 'Total Users', value: '9,431', icon: IconUsers, color: 'blue', trend: '+12.5%', up: true }, - { label: 'Active Users', value: '7,218', icon: IconUserCheck, color: 'indigo', trend: '+8.2%', up: true }, - { label: 'New This Month', value: '642', icon: IconUserPlus, color: 'cyan', trend: '+23.1%', up: true }, - { label: 'Pending Approvals', value: '37', icon: IconClockHour4, color: 'orange', trend: '-4.0%', up: false }, -]; +function useKpis(t: (key: string) => string) { + return [ + { label: t('dashboard.kpis.totalUsers'), value: '9,431', icon: IconUsers, color: 'blue', trend: '+12.5%', up: true }, + { label: t('dashboard.kpis.activeUsers'), value: '7,218', icon: IconUserCheck, color: 'indigo', trend: '+8.2%', up: true }, + { label: t('dashboard.kpis.newThisMonth'), value: '642', icon: IconUserPlus, color: 'cyan', trend: '+23.1%', up: true }, + { label: t('dashboard.kpis.pendingApprovals'), value: '37', icon: IconClockHour4, color: 'orange', trend: '-4.0%', up: false }, + ]; +} -const QUICK_LINKS = [ - { label: 'Add User', desc: 'Create a new account', icon: IconUserPlus, color: 'blue' }, - { label: 'Roles & Permissions', desc: 'Manage access levels', icon: IconShieldLock, color: 'indigo' }, - { label: 'Invite Members', desc: 'Send email invites', icon: IconMail, color: 'cyan' }, - { label: 'Import Users', desc: 'Bulk CSV import', icon: IconUpload, color: 'violet' }, - { label: 'Export Data', desc: 'Download reports', icon: IconDownload, color: 'blue' }, - { label: 'Audit Log', desc: 'Review activity', icon: IconClipboardList, color: 'grape' }, -]; +function useQuickLinks(t: (key: string) => string) { + return [ + { label: t('dashboard.quickLinks.addUser'), desc: t('dashboard.quickLinks.addUserDesc'), icon: IconUserPlus, color: 'blue' }, + { label: t('dashboard.quickLinks.rolesAndPermissions'), desc: t('dashboard.quickLinks.rolesAndPermissionsDesc'), icon: IconShieldLock, color: 'indigo' }, + { label: t('dashboard.quickLinks.inviteMembers'), desc: t('dashboard.quickLinks.inviteMembersDesc'), icon: IconMail, color: 'cyan' }, + { label: t('dashboard.quickLinks.importUsers'), desc: t('dashboard.quickLinks.importUsersDesc'), icon: IconUpload, color: 'violet' }, + { label: t('dashboard.quickLinks.exportData'), desc: t('dashboard.quickLinks.exportDataDesc'), icon: IconDownload, color: 'blue' }, + { label: t('dashboard.quickLinks.auditLog'), desc: t('dashboard.quickLinks.auditLogDesc'), icon: IconClipboardList, color: 'grape' }, + ]; +} type UserStatus = 'Active' | 'Pending' | 'Invited'; const STATUS_COLOR: Record = { @@ -214,16 +219,19 @@ function QuickLinkTile({ /* ------------------------------------------------------------------ */ export function DashboardPage() { + const { t } = useTranslation(); const [range, setRange] = useState('week'); + const kpis = useKpis(t); + const quickLinks = useQuickLinks(t); return ( {/* header */} - Overview + {t('dashboard.title')} - Welcome back — here's what's happening across your platform. + {t('dashboard.subtitle')} @@ -232,20 +240,20 @@ export function DashboardPage() { onChange={setRange} radius="md" data={[ - { label: 'Today', value: 'today' }, - { label: 'This Week', value: 'week' }, - { label: 'This Month', value: 'month' }, + { label: t('dashboard.timeRange.today'), value: 'today' }, + { label: t('dashboard.timeRange.thisWeek'), value: 'week' }, + { label: t('dashboard.timeRange.thisMonth'), value: 'month' }, ]} /> {/* KPIs */} - {KPIS.map((k) => ( + {kpis.map((k) => ( ))} @@ -256,15 +264,15 @@ export function DashboardPage() { +18.2% - vs previous period + {t('dashboard.charts.vsPreviousPeriod')} @@ -309,7 +317,7 @@ export function DashboardPage() { - + @@ -348,7 +356,7 @@ export function DashboardPage() { 9,431 - Total users + {t('dashboard.charts.totalUsers')} @@ -376,9 +384,9 @@ export function DashboardPage() { - + - {QUICK_LINKS.map((q) => ( + {quickLinks.map((q) => ( ))} @@ -389,11 +397,11 @@ export function DashboardPage() { - Recent Users + {t('dashboard.recentUsers.title')} - View all + {t('common.viewAll')} diff --git a/apps/backoffice/src/app/i18n/config.ts b/apps/backoffice/src/app/i18n/config.ts new file mode 100644 index 000000000..b506b2ce0 --- /dev/null +++ b/apps/backoffice/src/app/i18n/config.ts @@ -0,0 +1,47 @@ +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { en } from './locales/en'; +import { am } from './locales/am'; + +export const SUPPORTED_LANGUAGES = ['en', 'am'] as const; +export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]; + +const STORAGE_KEY = 'ema-backoffice-lang'; + +function getInitialLanguage(): AppLanguage { + const stored = + typeof localStorage !== 'undefined' + ? (localStorage.getItem(STORAGE_KEY) as AppLanguage | null) + : null; + if (stored && SUPPORTED_LANGUAGES.includes(stored)) return stored; + return 'en'; +} + +export const i18n = i18next.createInstance(); + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + am: { translation: am }, + }, + lng: getInitialLanguage(), + fallbackLng: 'en', + supportedLngs: [...SUPPORTED_LANGUAGES], + interpolation: { escapeValue: false }, + returnNull: false, +}); + +i18n.on('languageChanged', (lng) => { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, lng); + } + if (typeof document !== 'undefined') { + document.documentElement.lang = lng; + } +}); + +if (typeof document !== 'undefined') { + document.documentElement.lang = i18n.language; +} + +export default i18n; diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts new file mode 100644 index 000000000..b9922ace5 --- /dev/null +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -0,0 +1,82 @@ +import type { Translations } from './en'; + +export const am: Translations = { + app: { + name: 'ኢማ አስተዳደር', + shortName: 'ኢማ', + authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን', + tagline: 'የቁጥጥር ማዕከል', + }, + + language: { + label: 'ቋንቋ', + en: 'English', + am: 'አማርኛ', + }, + + nav: { + menu: 'ምናሌ', + dashboard: 'ዳሽቦርድ', + userManagement: 'የተጠቃሚ አስተዳደር', + }, + + common: { + logout: 'ውጣ', + profile: 'መገለጫ', + settings: 'ቅንብሮች', + export: 'ላክ', + viewAll: 'ሁሉንም ይመልከቱ', + home: 'መነሻ', + notifications: 'ማሳወቂያዎች', + administrator: 'አስተዳዳሪ', + }, + + breadcrumbs: { + dashboard: 'ዳሽቦርድ', + um: 'የተጠቃሚ አስተዳደር', + home: 'መነሻ', + }, + + dashboard: { + title: 'አጠቃላይ እይታ', + subtitle: 'እንኳን ደህና መጡ — በመድረክዎ ላይ ያለው እንቅስቃሴ አጠቃላይ እይታ።', + kpis: { + totalUsers: 'ጠቅላላ ተጠቃሚዎች', + activeUsers: 'ንቁ ተጠቃሚዎች', + newThisMonth: 'በዚህ ወር አዲስ', + pendingApprovals: 'በመጠባበቅ ላይ ያሉ', + }, + charts: { + userRegistrations: 'የተጠቃሚ ምዝገባ', + registrationsSubtitle: 'ባለፉት 8 ወራት አዲስ ምዝገባ', + vsPreviousPeriod: 'ካለፈው ጊዜ ጋር ሲነጻጸር', + usersByRole: 'ተጠቃሚዎች በሚና', + roleDistribution: 'በተደራሽነት ደረጃ ስርጭት', + totalUsers: 'ጠቅላላ ተጠቃሚዎች', + }, + quickLinks: { + title: 'ፈጣን አገናኞች', + subtitle: 'የተለመዱ የአስተዳደር አቋራጮች', + addUser: 'ተጠቃሚ ያክሉ', + addUserDesc: 'አዲስ መለያ ይፍጠሩ', + rolesAndPermissions: 'ሚናዎች እና ፈቃዶች', + rolesAndPermissionsDesc: 'የተደራሽነት ደረጃዎችን ያስተዳድሩ', + inviteMembers: 'አባላትን ይጋብዙ', + inviteMembersDesc: 'በኢሜይል ግብዣ ይላኩ', + importUsers: 'ተጠቃሚዎችን ያስመጡ', + importUsersDesc: 'የCSV ጅምላ ማስመጣት', + exportData: 'ውሂብ ያላኩ', + exportDataDesc: 'ሪፖርቶችን ያውርዱ', + auditLog: 'የኦዲት መዝገብ', + auditLogDesc: 'እንቅስቃሴን ይገምግሙ', + }, + recentUsers: { + title: 'የቅርብ ጊዜ ተጠቃሚዎች', + }, + timeRange: { + today: 'ዛሬ', + thisWeek: 'በዚህ ሳምንት', + thisMonth: 'በዚህ ወር', + }, + }, +}; diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts new file mode 100644 index 000000000..fee962d38 --- /dev/null +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -0,0 +1,82 @@ +export const en = { + app: { + name: 'EMA Admin', + shortName: 'EMA', + authority: 'Ethiopian Maritime Authority', + tagline: 'Control Center', + }, + + language: { + label: 'Language', + en: 'English', + am: 'አማርኛ', + }, + + nav: { + menu: 'MENU', + dashboard: 'Dashboard', + userManagement: 'User Management', + }, + + common: { + logout: 'Log out', + profile: 'Profile', + settings: 'Settings', + export: 'Export', + viewAll: 'View all', + home: 'Home', + notifications: 'Notifications', + administrator: 'Administrator', + }, + + breadcrumbs: { + dashboard: 'Dashboard', + um: 'User Management', + home: 'Home', + }, + + dashboard: { + title: 'Overview', + subtitle: 'Welcome back — here\u2019s what\u2019s happening across your platform.', + kpis: { + totalUsers: 'Total Users', + activeUsers: 'Active Users', + newThisMonth: 'New This Month', + pendingApprovals: 'Pending Approvals', + }, + charts: { + userRegistrations: 'User Registrations', + registrationsSubtitle: 'New sign-ups over the last 8 months', + vsPreviousPeriod: 'vs previous period', + usersByRole: 'Users by Role', + roleDistribution: 'Distribution across access levels', + totalUsers: 'Total users', + }, + quickLinks: { + title: 'Quick Links', + subtitle: 'Common administrative shortcuts', + addUser: 'Add User', + addUserDesc: 'Create a new account', + rolesAndPermissions: 'Roles & Permissions', + rolesAndPermissionsDesc: 'Manage access levels', + inviteMembers: 'Invite Members', + inviteMembersDesc: 'Send email invites', + importUsers: 'Import Users', + importUsersDesc: 'Bulk CSV import', + exportData: 'Export Data', + exportDataDesc: 'Download reports', + auditLog: 'Audit Log', + auditLogDesc: 'Review activity', + }, + recentUsers: { + title: 'Recent Users', + }, + timeRange: { + today: 'Today', + thisWeek: 'This Week', + thisMonth: 'This Month', + }, + }, +}; + +export type Translations = typeof en; diff --git a/apps/backoffice/src/app/layouts/components/AppHeader.tsx b/apps/backoffice/src/app/layouts/components/AppHeader.tsx index 09db5af02..85d2216a0 100644 --- a/apps/backoffice/src/app/layouts/components/AppHeader.tsx +++ b/apps/backoffice/src/app/layouts/components/AppHeader.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { Group, Text, @@ -29,23 +30,19 @@ interface AppHeaderProps { onToggle: () => void; } -const ROUTE_LABELS: Record = { - dashboard: 'Dashboard', - um: 'User Management', -}; - function useBreadcrumbs() { + const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const segments = location.pathname.split('/').filter(Boolean); const crumbs = [ - { label: 'Home', path: '/dashboard', icon: }, + { label: t('breadcrumbs.home'), path: '/dashboard', icon: }, ...segments - .filter((seg) => ROUTE_LABELS[seg]) + .filter((seg) => ['dashboard', 'um'].includes(seg)) .map((seg, i) => ({ - label: ROUTE_LABELS[seg], + label: t(`breadcrumbs.${seg}`), path: '/' + segments.slice(0, i + 1).join('/'), icon: null, })), @@ -55,6 +52,7 @@ function useBreadcrumbs() { } export function AppHeader({ onToggle }: AppHeaderProps) { + const { t } = useTranslation(); const dispatch = useDispatch(); const navigate = useNavigate(); const { crumbs } = useBreadcrumbs(); @@ -106,7 +104,7 @@ export function AppHeader({ onToggle }: AppHeaderProps) { - + @@ -123,7 +121,7 @@ export function AppHeader({ onToggle }: AppHeaderProps) { Amanuel D. - Administrator + {t('common.administrator')} @@ -131,11 +129,11 @@ export function AppHeader({ onToggle }: AppHeaderProps) { - }>Profile - }>Settings + }>{t('common.profile')} + }>{t('common.settings')} } onClick={handleLogout}> - Logout + {t('common.logout')} diff --git a/apps/backoffice/src/app/layouts/components/AppSidebar.tsx b/apps/backoffice/src/app/layouts/components/AppSidebar.tsx index d5d4bacef..6533e9c15 100644 --- a/apps/backoffice/src/app/layouts/components/AppSidebar.tsx +++ b/apps/backoffice/src/app/layouts/components/AppSidebar.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { AppShell, Box, @@ -20,12 +21,8 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { logout } from '@ema-platform/auth'; -const NAV_ITEMS = [ - { label: 'Dashboard', icon: IconLayoutDashboard, path: '/dashboard' }, - { label: 'User Management', icon: IconUsers, path: '/um/user-management/dashboard' }, -]; - export function AppSidebar() { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const { pathname } = useLocation(); @@ -44,10 +41,10 @@ export function AppSidebar() {
- EMA Admin + {t('app.name')} - Control Center + {t('app.tagline')}
@@ -56,20 +53,28 @@ export function AppSidebar() { - MENU + {t('nav.menu')} - {NAV_ITEMS.map(({ label, icon: Icon, path }) => ( - } - active={pathname.startsWith(path)} - onClick={() => navigate(path)} - variant="light" - color="blue" - styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }} - /> - ))} + } + active={pathname.startsWith('/dashboard')} + onClick={() => navigate('/dashboard')} + variant="light" + color="blue" + styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }} + /> + } + active={pathname.startsWith('/um')} + onClick={() => navigate('/um/user-management/dashboard')} + variant="light" + color="blue" + styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }} + /> @@ -91,13 +96,13 @@ export function AppSidebar() { Amanuel D. - Administrator + {t('common.administrator')} diff --git a/apps/backoffice/src/app/layouts/components/LanguageSwitcher.tsx b/apps/backoffice/src/app/layouts/components/LanguageSwitcher.tsx index 062164cc1..924689d9c 100644 --- a/apps/backoffice/src/app/layouts/components/LanguageSwitcher.tsx +++ b/apps/backoffice/src/app/layouts/components/LanguageSwitcher.tsx @@ -1,31 +1,27 @@ -import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Menu, ActionIcon } from '@mantine/core'; import { IconWorld, IconCheck } from '@tabler/icons-react'; - -const LANGUAGES = [ - { code: 'en', label: 'English' }, - { code: 'am', label: 'አማርኛ' }, -]; +import { i18n, SUPPORTED_LANGUAGES } from '../../i18n/config'; export function LanguageSwitcher() { - const [current, setCurrent] = useState('en'); + const { t } = useTranslation(); return ( - + - Language - {LANGUAGES.map((lng) => ( + {t('language.label')} + {SUPPORTED_LANGUAGES.map((code) => ( setCurrent(lng.code)} - rightSection={current === lng.code ? : undefined} + key={code} + onClick={() => i18n.changeLanguage(code)} + rightSection={i18n.language === code ? : undefined} > - {lng.label} + {t(`language.${code}`)} ))} diff --git a/apps/backoffice/src/app/providers/AppProviders.tsx b/apps/backoffice/src/app/providers/AppProviders.tsx index c8521ac36..8782892f6 100644 --- a/apps/backoffice/src/app/providers/AppProviders.tsx +++ b/apps/backoffice/src/app/providers/AppProviders.tsx @@ -1,8 +1,10 @@ +import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthConfigProvider } from '@ema-platform/auth'; import type { ReactNode } from 'react'; import { store } from '../store'; +import { i18n } from '../i18n/config'; import { MantineThemeProvider } from './MantineThemeProvider'; const queryClient = new QueryClient({ @@ -24,7 +26,9 @@ export function AppProviders({ children }: { children: ReactNode }) { enableForgotPassword: true, }} > - {children} + + {children} + diff --git a/apps/backoffice/src/main.tsx b/apps/backoffice/src/main.tsx index 23cfe23af..169d9c9e9 100644 --- a/apps/backoffice/src/main.tsx +++ b/apps/backoffice/src/main.tsx @@ -4,6 +4,7 @@ import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; import '@mantine/dates/styles.css'; import './styles.css'; +import './app/i18n/config'; import { App } from './app/app'; window.__USER_MANAGEMENT_BRANDING__ = {