diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ab85393..8dc7480 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -6,6 +6,7 @@ import { TransactionProvider, } from '@bridgewise/ui-components'; import { OfflineBanner } from '../components/OfflineBanner'; +import { I18nProvider } from '../components/I18nProvider'; import './globals.css'; const customTheme = { @@ -47,12 +48,14 @@ export default function RootLayout({ - - - - {children} - - + + + + + {children} + + + ); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index e5f5d97..2b632e1 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,6 +2,7 @@ 'use client'; import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { BridgeWiseProvider, TransactionHeartbeat, @@ -9,6 +10,7 @@ import { BridgeStatus, } from '@bridgewise/ui-components'; import VersionDisplay from '../components/VersionDisplay'; +import { LanguageSwitcher } from '../components/LanguageSwitcher'; const customTheme = { primaryColor: '#22c55e', @@ -37,28 +39,29 @@ function TransactionDemo() { let nextProgress = state.progress + 5; let nextStep = state.step; - if (nextProgress > 20 && nextProgress < 40) nextStep = 'Confirming on source chain...'; - if (nextProgress > 50 && nextProgress < 70) nextStep = 'Bridging assets...'; - if (nextProgress > 80) nextStep = 'Finalizing on destination...'; + if (nextProgress > 20 && nextProgress < 40) nextStep = t('app.statusConfirming'); + if (nextProgress > 50 && nextProgress < 70) nextStep = t('app.statusBridging'); + if (nextProgress > 80) nextStep = t('app.statusFinalizing'); updateState({ progress: Math.min(nextProgress, 100), step: nextStep }); }, 800); return () => clearInterval(interval); - }, [state, updateState]); + }, [state, updateState, t]); + + const { t } = useTranslation(); return (
+

- BridgeWise Theming Demo + {t('app.title')}

- This page demonstrates the BridgeWise theme system. The heartbeat and status components - are styled via CSS variables injected by BridgeWiseProvider, with a custom - primary color and dark background. + {t('app.description')}

@@ -66,23 +69,23 @@ function TransactionDemo() { onClick={() => startTransaction('tx-' + Date.now())} className="px-6 py-3 rounded-lg text-sm font-medium text-white bg-emerald-500 hover:bg-emerald-600 active:scale-95 transition" > - Start Transaction + {t('app.startTransaction')}

- Inline BridgeStatus + {t('app.inlineStatusTitle')}

- An inline status card using the same theme variables as the floating heartbeat. + {t('app.inlineStatusText')}

- Component-level Overrides + {t('app.componentOverridesTitle')}

- The floating heartbeat below uses a custom className to adjust its - position while still inheriting all theme variables. + {t('app.componentOverridesText')}

- Trigger a transaction and you'll see the heartbeat appear in the bottom-left corner. + {t('app.componentOverridesHint')}

diff --git a/apps/web/components/I18nProvider.tsx b/apps/web/components/I18nProvider.tsx new file mode 100644 index 0000000..9a90e38 --- /dev/null +++ b/apps/web/components/I18nProvider.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { ReactNode, useEffect } from 'react'; +import { I18nextProvider } from 'react-i18next'; +import i18n from '../i18n'; + +export function I18nProvider({ children }: { children: ReactNode }) { + useEffect(() => { + const storedLanguage = window.localStorage.getItem('bridgewise-language'); + if (storedLanguage && storedLanguage !== i18n.language) { + i18n.changeLanguage(storedLanguage); + } + }, []); + + return {children}; +} diff --git a/apps/web/components/LanguageSwitcher.tsx b/apps/web/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..5471602 --- /dev/null +++ b/apps/web/components/LanguageSwitcher.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const AVAILABLE_LANGUAGES = [ + { code: 'en', label: 'English' }, + { code: 'fr', label: 'Français' }, +]; + +export function LanguageSwitcher() { + const { i18n, t } = useTranslation(); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const stored = window.localStorage.getItem('bridgewise-language'); + if (stored && stored !== i18n.language) { + i18n.changeLanguage(stored); + } + setLoaded(true); + }, [i18n]); + + const handleChange = (event: ChangeEvent) => { + const nextLng = event.target.value; + i18n.changeLanguage(nextLng); + window.localStorage.setItem('bridgewise-language', nextLng); + }; + + if (!loaded) return null; + + return ( +
+ + +
+ ); +} diff --git a/apps/web/components/OfflineBanner.tsx b/apps/web/components/OfflineBanner.tsx index 8099d65..5b07be6 100644 --- a/apps/web/components/OfflineBanner.tsx +++ b/apps/web/components/OfflineBanner.tsx @@ -1,9 +1,11 @@ 'use client'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useOfflineDetection } from '../hooks/useOfflineDetection'; export function OfflineBanner() { + const { t } = useTranslation(); const { isOffline, cache } = useOfflineDetection(); if (!isOffline) return null; @@ -33,8 +35,9 @@ export function OfflineBanner() { /> - You are offline. Showing{' '} - {cachedAt ? `cached data from ${cachedAt}` : 'limited functionality'}. + {t('app.offline', { + cached: cachedAt ? t('app.offlineCached', { when: cachedAt }) : t('app.offlineLimited'), + })}
); diff --git a/apps/web/components/VersionDisplay.tsx b/apps/web/components/VersionDisplay.tsx index af8d50b..0b4a433 100644 --- a/apps/web/components/VersionDisplay.tsx +++ b/apps/web/components/VersionDisplay.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useVersion, VersionData } from '../hooks/useVersion'; export interface VersionDisplayProps { @@ -29,6 +30,7 @@ export const VersionDisplay: React.FC = ({ apiUrl, onClick, }) => { + const { t } = useTranslation(); const { version, loading, error } = useVersion({ apiUrl, enableLogging, @@ -64,7 +66,7 @@ export const VersionDisplay: React.FC = ({ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> - Loading... + {t('app.loading')} ); } diff --git a/apps/web/i18n.ts b/apps/web/i18n.ts new file mode 100644 index 0000000..b6d6f11 --- /dev/null +++ b/apps/web/i18n.ts @@ -0,0 +1,37 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import en from './locales/en.json'; +import fr from './locales/fr.json'; + +const resources = { + en: { + translation: en, + }, + fr: { + translation: fr, + }, +}; + +if (!i18n.isInitialized) { + i18n + .use(initReactI18next) + .init({ + resources, + lng: 'en', + fallbackLng: 'en', + supportedLngs: ['en', 'fr'], + interpolation: { + escapeValue: false, + }, + react: { + useSuspense: false, + }, + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('i18next init failed', error); + }); +} + +export default i18n; diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json new file mode 100644 index 0000000..b6e8b13 --- /dev/null +++ b/apps/web/locales/en.json @@ -0,0 +1,21 @@ +{ + "app": { + "title": "BridgeWise Theming Demo", + "description": "This page demonstrates the BridgeWise theme system. The heartbeat and status components are styled via CSS variables injected by BridgeWiseProvider, with a custom primary color and dark background.", + "startTransaction": "Start Transaction", + "clearState": "Clear State", + "inlineStatusTitle": "Inline BridgeStatus", + "inlineStatusText": "An inline status card using the same theme variables as the floating heartbeat.", + "componentOverridesTitle": "Component-level Overrides", + "componentOverridesText": "The floating heartbeat below uses a custom className to adjust its position while still inheriting all theme variables.", + "componentOverridesHint": "Trigger a transaction and you'll see the heartbeat appear in the bottom-left corner.", + "statusConfirming": "Confirming on source chain...", + "statusBridging": "Bridging assets...", + "statusFinalizing": "Finalizing on destination...", + "offline": "You are offline. Showing {{cached}}.", + "offlineCached": "cached data from {{when}}", + "offlineLimited": "limited functionality", + "language": "Language", + "loading": "Loading..." + } +} diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json new file mode 100644 index 0000000..cdc032a --- /dev/null +++ b/apps/web/locales/fr.json @@ -0,0 +1,21 @@ +{ + "app": { + "title": "Démo de thème BridgeWise", + "description": "Cette page démontre le système de thème BridgeWise. Les composants heartbeat et status sont stylés via des variables CSS injectées par BridgeWiseProvider, avec une couleur principale personnalisée et un fond sombre.", + "startTransaction": "Démarrer la transaction", + "clearState": "Réinitialiser l'état", + "inlineStatusTitle": "BridgeStatus en ligne", + "inlineStatusText": "Une carte d'état en ligne utilisant les mêmes variables de thème que le heartbeat flottant.", + "componentOverridesTitle": "Remplacements au niveau du composant", + "componentOverridesText": "Le heartbeat flottant ci-dessous utilise un className personnalisé pour ajuster sa position tout en héritant de toutes les variables de thème.", + "componentOverridesHint": "Déclenchez une transaction et vous verrez le heartbeat apparaître en bas à gauche.", + "statusConfirming": "Confirmation sur la chaîne source...", + "statusBridging": "Transfert d'actifs...", + "statusFinalizing": "Finalisation sur la chaîne de destination...", + "offline": "Vous êtes hors ligne. Affichage de {{cached}}.", + "offlineCached": "données mises en cache depuis {{when}}", + "offlineLimited": "fonctionnalité limitée", + "language": "Langue", + "loading": "Chargement..." + } +}