saafe-redirection-flow 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/docs/features/account-discovery.md +803 -0
- package/docs/features/authentication.md +583 -0
- package/docs/features/consent-management.md +740 -0
- package/docs/features/index.md +206 -0
- package/docs/features/navigation-routing.md +846 -0
- package/docs/features/overview.md +554 -0
- package/docs/features/theming-internationalization.md +982 -0
- package/docs/index.md +1 -1
- package/package.json +1 -1
- package/prod-aa-2307251430.zip +0 -0
- package/src/components/AutoDiscoveryLoader.tsx +29 -0
- package/src/components/LinkedAccountTypeAccordion.tsx +165 -0
- package/src/components/auth/AuthGuard.tsx +4 -3
- package/src/components/title/AppBar.tsx +5 -2
- package/src/components/title/SectionTitle.tsx +2 -2
- package/src/components/ui/otp-input.tsx +33 -3
- package/src/hooks/use-fip-query.ts +1 -1
- package/src/pages/accounts/AccountsToProceed.tsx +618 -237
- package/src/pages/accounts/Discover.tsx +5 -5
- package/src/pages/accounts/DiscoverAccount.tsx +130 -88
- package/src/pages/accounts/OldUser.tsx +8 -55
- package/src/pages/consent/ReviewConsent.tsx +1 -0
- package/src/services/api/account.service.ts +2 -2
- package/src/store/fip.store.ts +1 -0
- package/src/store/redirect.store.ts +6 -0
- package/src/utils/removeUnderscore.ts +3 -0
- package/src/utils/toast-helpers.ts +5 -0
- package/stage-aa-2307251430.zip +0 -0
- package/stage-aa-2407251347.zip +0 -0
- package/stage-aa-0407251720.zip +0 -0
- package/stage-aa-2506251021.zip +0 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
# Theming and Internationalization Module Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Theming and Internationalization module provides comprehensive support for multiple visual themes and languages in the SAAFE Redirection Flow. This module ensures the application can adapt to different user preferences, regional requirements, and accessibility needs while maintaining brand consistency.
|
|
6
|
+
|
|
7
|
+
## Theming System
|
|
8
|
+
|
|
9
|
+
### Theme Context (`ThemeContext.tsx`)
|
|
10
|
+
|
|
11
|
+
**Location**: `src/contexts/ThemeContext.tsx`
|
|
12
|
+
|
|
13
|
+
**Core Features**:
|
|
14
|
+
- Light, dark, and system theme support
|
|
15
|
+
- Dynamic theme switching
|
|
16
|
+
- Platform-specific theme detection
|
|
17
|
+
- CSS custom properties integration
|
|
18
|
+
- Theme persistence across sessions
|
|
19
|
+
|
|
20
|
+
**Theme Types**:
|
|
21
|
+
```typescript
|
|
22
|
+
type Theme = "light" | "dark" | "system";
|
|
23
|
+
|
|
24
|
+
interface ThemeContextType {
|
|
25
|
+
theme: Theme;
|
|
26
|
+
setTheme: (theme: Theme) => void;
|
|
27
|
+
effectiveTheme: "light" | "dark";
|
|
28
|
+
isSystemTheme: boolean;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Theme Implementation
|
|
33
|
+
|
|
34
|
+
**Theme Provider Setup**:
|
|
35
|
+
```tsx
|
|
36
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
37
|
+
children,
|
|
38
|
+
defaultTheme = "system",
|
|
39
|
+
storageKey = "saafe-ui-theme",
|
|
40
|
+
...props
|
|
41
|
+
}) => {
|
|
42
|
+
const [theme, setTheme] = useState<Theme>(
|
|
43
|
+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const value = {
|
|
47
|
+
theme,
|
|
48
|
+
setTheme: (theme: Theme) => {
|
|
49
|
+
localStorage.setItem(storageKey, theme);
|
|
50
|
+
setTheme(theme);
|
|
51
|
+
applyThemeToDocument(theme);
|
|
52
|
+
},
|
|
53
|
+
effectiveTheme: getEffectiveTheme(theme),
|
|
54
|
+
isSystemTheme: theme === "system"
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ThemeContext.Provider {...props} value={value}>
|
|
59
|
+
{children}
|
|
60
|
+
</ThemeContext.Provider>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### CSS Variables System
|
|
66
|
+
|
|
67
|
+
**Theme Variables**:
|
|
68
|
+
```css
|
|
69
|
+
:root {
|
|
70
|
+
/* Light theme variables */
|
|
71
|
+
--background: 0 0% 100%;
|
|
72
|
+
--foreground: 222.2 84% 4.9%;
|
|
73
|
+
--card: 0 0% 100%;
|
|
74
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
75
|
+
--popover: 0 0% 100%;
|
|
76
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
77
|
+
--primary: 221.2 83.2% 53.3%;
|
|
78
|
+
--primary-foreground: 210 40% 98%;
|
|
79
|
+
--secondary: 210 40% 96%;
|
|
80
|
+
--secondary-foreground: 222.2 84% 4.9%;
|
|
81
|
+
--muted: 210 40% 96%;
|
|
82
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
83
|
+
--accent: 210 40% 96%;
|
|
84
|
+
--accent-foreground: 222.2 84% 4.9%;
|
|
85
|
+
--destructive: 0 84.2% 60.2%;
|
|
86
|
+
--destructive-foreground: 210 40% 98%;
|
|
87
|
+
--border: 214.3 31.8% 91.4%;
|
|
88
|
+
--input: 214.3 31.8% 91.4%;
|
|
89
|
+
--ring: 221.2 83.2% 53.3%;
|
|
90
|
+
--radius: 0.5rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.dark {
|
|
94
|
+
/* Dark theme variables */
|
|
95
|
+
--background: 222.2 84% 4.9%;
|
|
96
|
+
--foreground: 210 40% 98%;
|
|
97
|
+
--card: 222.2 84% 4.9%;
|
|
98
|
+
--card-foreground: 210 40% 98%;
|
|
99
|
+
--popover: 222.2 84% 4.9%;
|
|
100
|
+
--popover-foreground: 210 40% 98%;
|
|
101
|
+
--primary: 217.2 91.2% 59.8%;
|
|
102
|
+
--primary-foreground: 222.2 84% 4.9%;
|
|
103
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
104
|
+
--secondary-foreground: 210 40% 98%;
|
|
105
|
+
--muted: 217.2 32.6% 17.5%;
|
|
106
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
107
|
+
--accent: 217.2 32.6% 17.5%;
|
|
108
|
+
--accent-foreground: 210 40% 98%;
|
|
109
|
+
--destructive: 0 62.8% 30.6%;
|
|
110
|
+
--destructive-foreground: 210 40% 98%;
|
|
111
|
+
--border: 217.2 32.6% 17.5%;
|
|
112
|
+
--input: 217.2 32.6% 17.5%;
|
|
113
|
+
--ring: 224.3 76.3% 94.1%;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Theme Toggle Component
|
|
118
|
+
|
|
119
|
+
**Mode Toggle Component**:
|
|
120
|
+
```tsx
|
|
121
|
+
// src/components/mode-toggle.tsx
|
|
122
|
+
export function ModeToggle() {
|
|
123
|
+
const { theme, setTheme } = useTheme();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<DropdownMenu>
|
|
127
|
+
<DropdownMenuTrigger asChild>
|
|
128
|
+
<Button variant="outline" size="icon">
|
|
129
|
+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
130
|
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
131
|
+
<span className="sr-only">Toggle theme</span>
|
|
132
|
+
</Button>
|
|
133
|
+
</DropdownMenuTrigger>
|
|
134
|
+
<DropdownMenuContent align="end">
|
|
135
|
+
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
136
|
+
Light
|
|
137
|
+
</DropdownMenuItem>
|
|
138
|
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
139
|
+
Dark
|
|
140
|
+
</DropdownMenuItem>
|
|
141
|
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
142
|
+
System
|
|
143
|
+
</DropdownMenuItem>
|
|
144
|
+
</DropdownMenuContent>
|
|
145
|
+
</DropdownMenu>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Platform-Specific Theme Detection
|
|
151
|
+
|
|
152
|
+
**URL Parameter Theme Override**:
|
|
153
|
+
```typescript
|
|
154
|
+
const getThemeFromParams = (): Theme | null => {
|
|
155
|
+
const params = new URLSearchParams(window.location.search);
|
|
156
|
+
const themeParam = params.get('theme');
|
|
157
|
+
|
|
158
|
+
if (themeParam && ['light', 'dark', 'system'].includes(themeParam)) {
|
|
159
|
+
return themeParam as Theme;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const initializeTheme = () => {
|
|
166
|
+
const urlTheme = getThemeFromParams();
|
|
167
|
+
const storedTheme = localStorage.getItem('saafe-ui-theme');
|
|
168
|
+
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
169
|
+
|
|
170
|
+
return urlTheme || storedTheme || systemTheme;
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Internationalization (i18n)
|
|
175
|
+
|
|
176
|
+
### i18n Configuration (`i18n.ts`)
|
|
177
|
+
|
|
178
|
+
**Location**: `src/lib/i18n.ts`
|
|
179
|
+
|
|
180
|
+
**Setup and Configuration**:
|
|
181
|
+
```typescript
|
|
182
|
+
import i18n from 'i18next';
|
|
183
|
+
import { initReactI18next } from 'react-i18next';
|
|
184
|
+
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
185
|
+
|
|
186
|
+
// Import translation files
|
|
187
|
+
import enTranslation from '../locales/en/common.json';
|
|
188
|
+
import hiTranslation from '../locales/hi/common.json';
|
|
189
|
+
import taTranslation from '../locales/ta/common.json';
|
|
190
|
+
import mlTranslation from '../locales/ml/common.json';
|
|
191
|
+
import teTranslation from '../locales/te/common.json';
|
|
192
|
+
import knTranslation from '../locales/kn/common.json';
|
|
193
|
+
import urTranslation from '../locales/ur/common.json';
|
|
194
|
+
|
|
195
|
+
const resources = {
|
|
196
|
+
en: { translation: enTranslation },
|
|
197
|
+
hi: { translation: hiTranslation },
|
|
198
|
+
ta: { translation: taTranslation },
|
|
199
|
+
ml: { translation: mlTranslation },
|
|
200
|
+
te: { translation: teTranslation },
|
|
201
|
+
kn: { translation: knTranslation },
|
|
202
|
+
ur: { translation: urTranslation },
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
i18n
|
|
206
|
+
.use(LanguageDetector)
|
|
207
|
+
.use(initReactI18next)
|
|
208
|
+
.init({
|
|
209
|
+
resources,
|
|
210
|
+
fallbackLng: 'en',
|
|
211
|
+
debug: import.meta.env.DEV,
|
|
212
|
+
|
|
213
|
+
interpolation: {
|
|
214
|
+
escapeValue: false,
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
detection: {
|
|
218
|
+
order: ['querystring', 'localStorage', 'navigator'],
|
|
219
|
+
caches: ['localStorage'],
|
|
220
|
+
lookupQuerystring: 'lang',
|
|
221
|
+
lookupLocalStorage: 'i18nextLng',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export default i18n;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Language Context (`LanguageContext.tsx`)
|
|
229
|
+
|
|
230
|
+
**Location**: `src/contexts/LanguageContext.tsx`
|
|
231
|
+
|
|
232
|
+
**Language Management**:
|
|
233
|
+
```typescript
|
|
234
|
+
interface LanguageContextType {
|
|
235
|
+
currentLanguage: string;
|
|
236
|
+
setLanguage: (lang: string) => void;
|
|
237
|
+
availableLanguages: Language[];
|
|
238
|
+
isRTL: boolean;
|
|
239
|
+
t: TFunction;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
|
243
|
+
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
|
|
244
|
+
const { t } = useTranslation();
|
|
245
|
+
|
|
246
|
+
const setLanguage = useCallback((lang: string) => {
|
|
247
|
+
i18n.changeLanguage(lang);
|
|
248
|
+
setCurrentLanguage(lang);
|
|
249
|
+
|
|
250
|
+
// Update document direction for RTL languages
|
|
251
|
+
updateDocumentDirection(lang);
|
|
252
|
+
|
|
253
|
+
// Track language change
|
|
254
|
+
trackEvent('LANGUAGE_CHANGED', {
|
|
255
|
+
from: currentLanguage,
|
|
256
|
+
to: lang
|
|
257
|
+
});
|
|
258
|
+
}, [currentLanguage]);
|
|
259
|
+
|
|
260
|
+
const value = {
|
|
261
|
+
currentLanguage,
|
|
262
|
+
setLanguage,
|
|
263
|
+
availableLanguages: SUPPORTED_LANGUAGES,
|
|
264
|
+
isRTL: RTL_LANGUAGES.includes(currentLanguage),
|
|
265
|
+
t
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<LanguageContext.Provider value={value}>
|
|
270
|
+
{children}
|
|
271
|
+
</LanguageContext.Provider>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Supported Languages
|
|
277
|
+
|
|
278
|
+
**Language Configuration**:
|
|
279
|
+
```typescript
|
|
280
|
+
export const SUPPORTED_LANGUAGES = [
|
|
281
|
+
{ code: 'en', name: 'English', nativeName: 'English' },
|
|
282
|
+
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
|
|
283
|
+
{ code: 'ta', name: 'Tamil', nativeName: 'தமிழ்' },
|
|
284
|
+
{ code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
|
|
285
|
+
{ code: 'te', name: 'Telugu', nativeName: 'తెలుగు' },
|
|
286
|
+
{ code: 'kn', name: 'Kannada', nativeName: 'ಕನ್ನಡ' },
|
|
287
|
+
{ code: 'ur', name: 'Urdu', nativeName: 'اردو' }
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
export const RTL_LANGUAGES = ['ur', 'ar'];
|
|
291
|
+
export const DEFAULT_LANGUAGE = 'en';
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Language Switcher Component
|
|
295
|
+
|
|
296
|
+
**Location**: `src/components/language/LanguageSwitcher.tsx`
|
|
297
|
+
|
|
298
|
+
**Component Features**:
|
|
299
|
+
```tsx
|
|
300
|
+
export function LanguageSwitcher() {
|
|
301
|
+
const { currentLanguage, setLanguage, availableLanguages } = useLanguage();
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<DropdownMenu>
|
|
305
|
+
<DropdownMenuTrigger asChild>
|
|
306
|
+
<Button variant="outline" size="sm">
|
|
307
|
+
<Globe className="mr-2 h-4 w-4" />
|
|
308
|
+
{availableLanguages.find(lang => lang.code === currentLanguage)?.nativeName}
|
|
309
|
+
</Button>
|
|
310
|
+
</DropdownMenuTrigger>
|
|
311
|
+
<DropdownMenuContent align="end">
|
|
312
|
+
{availableLanguages.map((language) => (
|
|
313
|
+
<DropdownMenuItem
|
|
314
|
+
key={language.code}
|
|
315
|
+
onClick={() => setLanguage(language.code)}
|
|
316
|
+
className={currentLanguage === language.code ? 'bg-accent' : ''}
|
|
317
|
+
>
|
|
318
|
+
<span className="text-sm font-medium">{language.nativeName}</span>
|
|
319
|
+
<span className="ml-2 text-xs text-muted-foreground">{language.name}</span>
|
|
320
|
+
</DropdownMenuItem>
|
|
321
|
+
))}
|
|
322
|
+
</DropdownMenuContent>
|
|
323
|
+
</DropdownMenu>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## RTL (Right-to-Left) Support
|
|
329
|
+
|
|
330
|
+
### RTL Context (`RTLContext.tsx`)
|
|
331
|
+
|
|
332
|
+
**Location**: `src/contexts/RTLContext.tsx`
|
|
333
|
+
|
|
334
|
+
**RTL Management**:
|
|
335
|
+
```typescript
|
|
336
|
+
interface RTLContextType {
|
|
337
|
+
isRTL: boolean;
|
|
338
|
+
direction: 'ltr' | 'rtl';
|
|
339
|
+
toggleDirection: () => void;
|
|
340
|
+
setDirection: (direction: 'ltr' | 'rtl') => void;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export const RTLProvider: React.FC<RTLProviderProps> = ({ children }) => {
|
|
344
|
+
const { currentLanguage } = useLanguage();
|
|
345
|
+
const [isRTL, setIsRTL] = useState(RTL_LANGUAGES.includes(currentLanguage));
|
|
346
|
+
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const shouldBeRTL = RTL_LANGUAGES.includes(currentLanguage);
|
|
349
|
+
setIsRTL(shouldBeRTL);
|
|
350
|
+
updateDocumentDirection(shouldBeRTL ? 'rtl' : 'ltr');
|
|
351
|
+
}, [currentLanguage]);
|
|
352
|
+
|
|
353
|
+
const value = {
|
|
354
|
+
isRTL,
|
|
355
|
+
direction: isRTL ? 'rtl' : 'ltr',
|
|
356
|
+
toggleDirection: () => setIsRTL(!isRTL),
|
|
357
|
+
setDirection: (direction: 'ltr' | 'rtl') => setIsRTL(direction === 'rtl')
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<RTLContext.Provider value={value}>
|
|
362
|
+
{children}
|
|
363
|
+
</RTLContext.Provider>
|
|
364
|
+
);
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### RTL Styling
|
|
369
|
+
|
|
370
|
+
**RTL CSS Utilities**:
|
|
371
|
+
```css
|
|
372
|
+
/* src/styles/rtl.css */
|
|
373
|
+
.rtl {
|
|
374
|
+
direction: rtl;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.ltr {
|
|
378
|
+
direction: ltr;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* RTL-aware margins and paddings */
|
|
382
|
+
.ms-auto {
|
|
383
|
+
margin-inline-start: auto;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.me-auto {
|
|
387
|
+
margin-inline-end: auto;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.ps-4 {
|
|
391
|
+
padding-inline-start: 1rem;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.pe-4 {
|
|
395
|
+
padding-inline-end: 1rem;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* RTL-aware text alignment */
|
|
399
|
+
.text-start {
|
|
400
|
+
text-align: start;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.text-end {
|
|
404
|
+
text-align: end;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* RTL-aware transforms */
|
|
408
|
+
.rtl .transform-flip-x {
|
|
409
|
+
transform: scaleX(-1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* RTL-aware animations */
|
|
413
|
+
.rtl .animate-slide-in-right {
|
|
414
|
+
animation: slideInLeft 0.3s ease-out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.rtl .animate-slide-in-left {
|
|
418
|
+
animation: slideInRight 0.3s ease-out;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**RTL Utility Classes**:
|
|
423
|
+
```css
|
|
424
|
+
/* src/styles/rtl-utils.css */
|
|
425
|
+
.border-s {
|
|
426
|
+
border-inline-start-width: 1px;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.border-e {
|
|
430
|
+
border-inline-end-width: 1px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.rounded-s {
|
|
434
|
+
border-start-start-radius: 0.375rem;
|
|
435
|
+
border-end-start-radius: 0.375rem;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.rounded-e {
|
|
439
|
+
border-start-end-radius: 0.375rem;
|
|
440
|
+
border-end-end-radius: 0.375rem;
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### RTL Hooks
|
|
445
|
+
|
|
446
|
+
**useRTL Hook**:
|
|
447
|
+
```typescript
|
|
448
|
+
// src/hooks/use-rtl.ts
|
|
449
|
+
export function useRTL() {
|
|
450
|
+
const context = useContext(RTLContext);
|
|
451
|
+
if (!context) {
|
|
452
|
+
throw new Error('useRTL must be used within an RTLProvider');
|
|
453
|
+
}
|
|
454
|
+
return context;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Helper hook for conditional RTL styling
|
|
458
|
+
export function useRTLClass(baseClass: string, rtlClass?: string) {
|
|
459
|
+
const { isRTL } = useRTL();
|
|
460
|
+
return isRTL && rtlClass ? `${baseClass} ${rtlClass}` : baseClass;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Translation Management
|
|
465
|
+
|
|
466
|
+
### Translation File Structure
|
|
467
|
+
|
|
468
|
+
**Common Translation Keys**:
|
|
469
|
+
```json
|
|
470
|
+
// src/locales/en/common.json
|
|
471
|
+
{
|
|
472
|
+
"common": {
|
|
473
|
+
"continue": "Continue",
|
|
474
|
+
"cancel": "Cancel",
|
|
475
|
+
"back": "Back",
|
|
476
|
+
"next": "Next",
|
|
477
|
+
"save": "Save",
|
|
478
|
+
"loading": "Loading...",
|
|
479
|
+
"error": "Something went wrong",
|
|
480
|
+
"retry": "Retry",
|
|
481
|
+
"close": "Close"
|
|
482
|
+
},
|
|
483
|
+
"authentication": {
|
|
484
|
+
"login": "Login",
|
|
485
|
+
"username": "Username",
|
|
486
|
+
"password": "Password",
|
|
487
|
+
"otp": "Enter OTP",
|
|
488
|
+
"loginButton": "Sign In",
|
|
489
|
+
"forgotPassword": "Forgot Password?",
|
|
490
|
+
"invalidCredentials": "Invalid username or password",
|
|
491
|
+
"otpExpired": "OTP has expired. Please request a new one.",
|
|
492
|
+
"sessionExpired": "Your session has expired. Please login again."
|
|
493
|
+
},
|
|
494
|
+
"accounts": {
|
|
495
|
+
"selectAccounts": "Select Accounts",
|
|
496
|
+
"noAccountsFound": "No accounts found",
|
|
497
|
+
"accountType": "Account Type",
|
|
498
|
+
"accountNumber": "Account Number",
|
|
499
|
+
"balance": "Balance",
|
|
500
|
+
"selectAll": "Select All",
|
|
501
|
+
"deselectAll": "Deselect All",
|
|
502
|
+
"linkedAccounts": "Linked Accounts"
|
|
503
|
+
},
|
|
504
|
+
"categories": {
|
|
505
|
+
"banks": "Banks",
|
|
506
|
+
"nbfc": "NBFCs",
|
|
507
|
+
"insurance": "Insurance",
|
|
508
|
+
"gst": "GST",
|
|
509
|
+
"investments": "Investments",
|
|
510
|
+
"banksDescription": "Traditional banking institutions",
|
|
511
|
+
"nbfcDescription": "Non-Banking Financial Companies",
|
|
512
|
+
"insuranceDescription": "Insurance providers",
|
|
513
|
+
"gstDescription": "GST-related financial data",
|
|
514
|
+
"investmentsDescription": "Investment and mutual fund companies"
|
|
515
|
+
},
|
|
516
|
+
"consent": {
|
|
517
|
+
"reviewConsent": "Review Consent",
|
|
518
|
+
"dataSharing": "Data Sharing",
|
|
519
|
+
"approve": "Approve",
|
|
520
|
+
"reject": "Reject",
|
|
521
|
+
"consentGiven": "Consent has been granted",
|
|
522
|
+
"consentRejected": "Consent has been rejected",
|
|
523
|
+
"dataScope": "Data that will be shared:",
|
|
524
|
+
"purpose": "Purpose of data sharing:",
|
|
525
|
+
"duration": "Data will be retained for:",
|
|
526
|
+
"termsAndConditions": "Terms and Conditions",
|
|
527
|
+
"privacyPolicy": "Privacy Policy"
|
|
528
|
+
},
|
|
529
|
+
"errors": {
|
|
530
|
+
"networkError": "Please check your internet connection",
|
|
531
|
+
"serverError": "Server error occurred. Please try again later.",
|
|
532
|
+
"validationError": "Please check the entered information",
|
|
533
|
+
"sessionTimeout": "Session timeout. Please login again.",
|
|
534
|
+
"accessDenied": "Access denied. Please contact support."
|
|
535
|
+
},
|
|
536
|
+
"success": {
|
|
537
|
+
"accountsLinked": "Accounts successfully linked",
|
|
538
|
+
"consentApproved": "Consent approved successfully",
|
|
539
|
+
"dataWillBeShared": "Your selected data will now be shared",
|
|
540
|
+
"returnToApp": "Return to Application",
|
|
541
|
+
"downloadReceipt": "Download Consent Receipt"
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Pluralization Support
|
|
547
|
+
|
|
548
|
+
**Plural Forms**:
|
|
549
|
+
```json
|
|
550
|
+
{
|
|
551
|
+
"accountCount": "{{count}} account",
|
|
552
|
+
"accountCount_other": "{{count}} accounts",
|
|
553
|
+
"institutionCount": "{{count}} institution",
|
|
554
|
+
"institutionCount_other": "{{count}} institutions",
|
|
555
|
+
"dayCount": "{{count}} day",
|
|
556
|
+
"dayCount_other": "{{count}} days"
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Usage in Components**:
|
|
561
|
+
```tsx
|
|
562
|
+
const AccountSummary: React.FC<{ count: number }> = ({ count }) => {
|
|
563
|
+
const { t } = useTranslation();
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div>
|
|
567
|
+
{t('accountCount', { count })} selected
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
};
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Interpolation and Formatting
|
|
574
|
+
|
|
575
|
+
**Variable Interpolation**:
|
|
576
|
+
```json
|
|
577
|
+
{
|
|
578
|
+
"welcomeMessage": "Welcome, {{userName}}!",
|
|
579
|
+
"accountBalance": "Your balance is {{amount, currency}}",
|
|
580
|
+
"lastLogin": "Last login: {{date, datetime}}",
|
|
581
|
+
"expiryWarning": "Consent expires in {{days}} days"
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Usage Examples**:
|
|
586
|
+
```tsx
|
|
587
|
+
// Simple interpolation
|
|
588
|
+
const welcome = t('welcomeMessage', { userName: 'John Doe' });
|
|
589
|
+
|
|
590
|
+
// Currency formatting
|
|
591
|
+
const balance = t('accountBalance', {
|
|
592
|
+
amount: 25000.50,
|
|
593
|
+
formatParams: {
|
|
594
|
+
amount: { style: 'currency', currency: 'INR' }
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Date formatting
|
|
599
|
+
const lastLogin = t('lastLogin', {
|
|
600
|
+
date: new Date(),
|
|
601
|
+
formatParams: {
|
|
602
|
+
date: {
|
|
603
|
+
year: 'numeric',
|
|
604
|
+
month: 'long',
|
|
605
|
+
day: 'numeric'
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
## Custom Styling and Branding
|
|
612
|
+
|
|
613
|
+
### Dynamic Styling Support
|
|
614
|
+
|
|
615
|
+
**Custom Style Options**:
|
|
616
|
+
```typescript
|
|
617
|
+
interface CustomStyling {
|
|
618
|
+
primaryColor?: string;
|
|
619
|
+
secondaryColor?: string;
|
|
620
|
+
backgroundColor?: string;
|
|
621
|
+
textColor?: string;
|
|
622
|
+
borderRadius?: string;
|
|
623
|
+
fontFamily?: string;
|
|
624
|
+
logoUrl?: string;
|
|
625
|
+
brandName?: string;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const applyCustomStyling = (styles: CustomStyling) => {
|
|
629
|
+
const root = document.documentElement;
|
|
630
|
+
|
|
631
|
+
if (styles.primaryColor) {
|
|
632
|
+
root.style.setProperty('--primary', styles.primaryColor);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (styles.secondaryColor) {
|
|
636
|
+
root.style.setProperty('--secondary', styles.secondaryColor);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (styles.backgroundColor) {
|
|
640
|
+
root.style.setProperty('--background', styles.backgroundColor);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (styles.fontFamily) {
|
|
644
|
+
root.style.setProperty('--font-family', styles.fontFamily);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Brand Integration
|
|
650
|
+
|
|
651
|
+
**Logo and Branding**:
|
|
652
|
+
```tsx
|
|
653
|
+
interface BrandingProps {
|
|
654
|
+
logoUrl?: string;
|
|
655
|
+
brandName?: string;
|
|
656
|
+
primaryColor?: string;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const BrandHeader: React.FC<BrandingProps> = ({
|
|
660
|
+
logoUrl,
|
|
661
|
+
brandName,
|
|
662
|
+
primaryColor
|
|
663
|
+
}) => {
|
|
664
|
+
return (
|
|
665
|
+
<header
|
|
666
|
+
className="flex items-center space-x-4 p-4"
|
|
667
|
+
style={{ color: primaryColor }}
|
|
668
|
+
>
|
|
669
|
+
{logoUrl && (
|
|
670
|
+
<img
|
|
671
|
+
src={logoUrl}
|
|
672
|
+
alt={`${brandName} logo`}
|
|
673
|
+
className="h-8 w-auto"
|
|
674
|
+
/>
|
|
675
|
+
)}
|
|
676
|
+
{brandName && (
|
|
677
|
+
<h1 className="text-xl font-semibold">{brandName}</h1>
|
|
678
|
+
)}
|
|
679
|
+
</header>
|
|
680
|
+
);
|
|
681
|
+
};
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
## Accessibility Features
|
|
685
|
+
|
|
686
|
+
### Theme-Based Accessibility
|
|
687
|
+
|
|
688
|
+
**High Contrast Mode**:
|
|
689
|
+
```css
|
|
690
|
+
@media (prefers-contrast: high) {
|
|
691
|
+
:root {
|
|
692
|
+
--background: 0 0% 100%;
|
|
693
|
+
--foreground: 0 0% 0%;
|
|
694
|
+
--border: 0 0% 0%;
|
|
695
|
+
--primary: 220 100% 40%;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.dark {
|
|
699
|
+
--background: 0 0% 0%;
|
|
700
|
+
--foreground: 0 0% 100%;
|
|
701
|
+
--border: 0 0% 100%;
|
|
702
|
+
--primary: 220 100% 60%;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Reduced Motion Support**:
|
|
708
|
+
```css
|
|
709
|
+
@media (prefers-reduced-motion: reduce) {
|
|
710
|
+
*,
|
|
711
|
+
*::before,
|
|
712
|
+
*::after {
|
|
713
|
+
animation-duration: 0.01ms !important;
|
|
714
|
+
animation-iteration-count: 1 !important;
|
|
715
|
+
transition-duration: 0.01ms !important;
|
|
716
|
+
scroll-behavior: auto !important;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Language-Specific Accessibility
|
|
722
|
+
|
|
723
|
+
**Screen Reader Support**:
|
|
724
|
+
```tsx
|
|
725
|
+
const AccessibleText: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
726
|
+
const { currentLanguage } = useLanguage();
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<span lang={currentLanguage} role="text">
|
|
730
|
+
{children}
|
|
731
|
+
</span>
|
|
732
|
+
);
|
|
733
|
+
};
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## Platform-Specific Adaptations
|
|
737
|
+
|
|
738
|
+
### Mobile Theme Adaptations
|
|
739
|
+
|
|
740
|
+
**Mobile-Specific Variables**:
|
|
741
|
+
```css
|
|
742
|
+
@media (max-width: 768px) {
|
|
743
|
+
:root {
|
|
744
|
+
--mobile-header-height: 60px;
|
|
745
|
+
--mobile-footer-height: 80px;
|
|
746
|
+
--mobile-padding: 1rem;
|
|
747
|
+
--mobile-border-radius: 0.75rem;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Web Theme Adaptations
|
|
753
|
+
|
|
754
|
+
**Desktop Enhancements**:
|
|
755
|
+
```css
|
|
756
|
+
@media (min-width: 1024px) {
|
|
757
|
+
:root {
|
|
758
|
+
--desktop-sidebar-width: 280px;
|
|
759
|
+
--desktop-content-max-width: 1200px;
|
|
760
|
+
--desktop-border-radius: 0.5rem;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
## Performance Optimizations
|
|
766
|
+
|
|
767
|
+
### Lazy Loading of Translations
|
|
768
|
+
|
|
769
|
+
**Dynamic Import Strategy**:
|
|
770
|
+
```typescript
|
|
771
|
+
const loadTranslations = async (language: string) => {
|
|
772
|
+
try {
|
|
773
|
+
const module = await import(`../locales/${language}/common.json`);
|
|
774
|
+
return module.default;
|
|
775
|
+
} catch (error) {
|
|
776
|
+
console.warn(`Failed to load translations for ${language}`);
|
|
777
|
+
return await import('../locales/en/common.json').then(m => m.default);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Theme Caching
|
|
783
|
+
|
|
784
|
+
**Theme Persistence**:
|
|
785
|
+
```typescript
|
|
786
|
+
const THEME_CACHE_KEY = 'saafe-theme-cache';
|
|
787
|
+
const THEME_CACHE_VERSION = '1.0';
|
|
788
|
+
|
|
789
|
+
const cacheTheme = (theme: Theme, customStyles?: CustomStyling) => {
|
|
790
|
+
const cacheData = {
|
|
791
|
+
version: THEME_CACHE_VERSION,
|
|
792
|
+
theme,
|
|
793
|
+
customStyles,
|
|
794
|
+
timestamp: Date.now()
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
localStorage.setItem(THEME_CACHE_KEY, JSON.stringify(cacheData));
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const getCachedTheme = (): { theme: Theme; customStyles?: CustomStyling } | null => {
|
|
801
|
+
try {
|
|
802
|
+
const cached = localStorage.getItem(THEME_CACHE_KEY);
|
|
803
|
+
if (!cached) return null;
|
|
804
|
+
|
|
805
|
+
const data = JSON.parse(cached);
|
|
806
|
+
if (data.version !== THEME_CACHE_VERSION) return null;
|
|
807
|
+
|
|
808
|
+
return { theme: data.theme, customStyles: data.customStyles };
|
|
809
|
+
} catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## Testing Strategy
|
|
816
|
+
|
|
817
|
+
### Theme Testing
|
|
818
|
+
|
|
819
|
+
**Theme Switch Testing**:
|
|
820
|
+
```typescript
|
|
821
|
+
describe('Theme System', () => {
|
|
822
|
+
test('switches between light and dark themes', () => {
|
|
823
|
+
render(<ThemeProvider><TestComponent /></ThemeProvider>);
|
|
824
|
+
|
|
825
|
+
const toggleButton = screen.getByRole('button', { name: /toggle theme/i });
|
|
826
|
+
|
|
827
|
+
// Test initial theme
|
|
828
|
+
expect(document.documentElement).not.toHaveClass('dark');
|
|
829
|
+
|
|
830
|
+
// Switch to dark
|
|
831
|
+
fireEvent.click(toggleButton);
|
|
832
|
+
expect(document.documentElement).toHaveClass('dark');
|
|
833
|
+
|
|
834
|
+
// Switch back to light
|
|
835
|
+
fireEvent.click(toggleButton);
|
|
836
|
+
expect(document.documentElement).not.toHaveClass('dark');
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### i18n Testing
|
|
842
|
+
|
|
843
|
+
**Translation Testing**:
|
|
844
|
+
```typescript
|
|
845
|
+
describe('Internationalization', () => {
|
|
846
|
+
test('displays correct language text', () => {
|
|
847
|
+
render(
|
|
848
|
+
<I18nextProvider i18n={i18n}>
|
|
849
|
+
<TestComponent />
|
|
850
|
+
</I18nextProvider>
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
expect(screen.getByText('Continue')).toBeInTheDocument();
|
|
854
|
+
|
|
855
|
+
// Change language
|
|
856
|
+
act(() => {
|
|
857
|
+
i18n.changeLanguage('hi');
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
expect(screen.getByText('जारी रखें')).toBeInTheDocument();
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### RTL Testing
|
|
866
|
+
|
|
867
|
+
**RTL Layout Testing**:
|
|
868
|
+
```typescript
|
|
869
|
+
describe('RTL Support', () => {
|
|
870
|
+
test('applies RTL styles for RTL languages', () => {
|
|
871
|
+
render(
|
|
872
|
+
<RTLProvider>
|
|
873
|
+
<LanguageProvider>
|
|
874
|
+
<TestComponent />
|
|
875
|
+
</LanguageProvider>
|
|
876
|
+
</RTLProvider>
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
// Switch to RTL language
|
|
880
|
+
act(() => {
|
|
881
|
+
i18n.changeLanguage('ur');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
expect(document.documentElement).toHaveAttribute('dir', 'rtl');
|
|
885
|
+
expect(document.documentElement).toHaveClass('rtl');
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
## Configuration
|
|
891
|
+
|
|
892
|
+
### Theme Configuration
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
export const THEME_CONFIG = {
|
|
896
|
+
defaultTheme: 'system' as Theme,
|
|
897
|
+
storageKey: 'saafe-ui-theme',
|
|
898
|
+
enableSystemDetection: true,
|
|
899
|
+
enableCustomStyling: true,
|
|
900
|
+
supportedThemes: ['light', 'dark', 'system'] as Theme[],
|
|
901
|
+
transitionDuration: '0.3s',
|
|
902
|
+
};
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### i18n Configuration
|
|
906
|
+
|
|
907
|
+
```typescript
|
|
908
|
+
export const I18N_CONFIG = {
|
|
909
|
+
defaultLanguage: 'en',
|
|
910
|
+
fallbackLanguage: 'en',
|
|
911
|
+
supportedLanguages: SUPPORTED_LANGUAGES,
|
|
912
|
+
rtlLanguages: RTL_LANGUAGES,
|
|
913
|
+
storageKey: 'i18nextLng',
|
|
914
|
+
enableDebug: import.meta.env.DEV,
|
|
915
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
916
|
+
interpolation: {
|
|
917
|
+
escapeValue: false,
|
|
918
|
+
formatSeparator: ',',
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
## Future Enhancements
|
|
924
|
+
|
|
925
|
+
### Planned Features
|
|
926
|
+
|
|
927
|
+
1. **Advanced Theming**
|
|
928
|
+
- Multiple theme variants
|
|
929
|
+
- User-created themes
|
|
930
|
+
- Theme marketplace
|
|
931
|
+
- Animation themes
|
|
932
|
+
|
|
933
|
+
2. **Enhanced i18n**
|
|
934
|
+
- Real-time translation updates
|
|
935
|
+
- Context-aware translations
|
|
936
|
+
- AI-powered translations
|
|
937
|
+
- Voice-based language switching
|
|
938
|
+
|
|
939
|
+
3. **Accessibility Improvements**
|
|
940
|
+
- Better screen reader support
|
|
941
|
+
- Voice navigation
|
|
942
|
+
- Enhanced keyboard navigation
|
|
943
|
+
- Color-blind friendly themes
|
|
944
|
+
|
|
945
|
+
4. **Performance Optimizations**
|
|
946
|
+
- Tree-shaking for unused translations
|
|
947
|
+
- Preloading strategies
|
|
948
|
+
- CDN-based theme delivery
|
|
949
|
+
- Runtime optimization
|
|
950
|
+
|
|
951
|
+
## Troubleshooting
|
|
952
|
+
|
|
953
|
+
### Common Issues
|
|
954
|
+
|
|
955
|
+
1. **Theme Not Applying**
|
|
956
|
+
- Check CSS variable definitions
|
|
957
|
+
- Verify theme provider wrapping
|
|
958
|
+
- Clear localStorage cache
|
|
959
|
+
|
|
960
|
+
2. **Translation Missing**
|
|
961
|
+
- Verify translation key exists
|
|
962
|
+
- Check fallback language setup
|
|
963
|
+
- Validate JSON syntax
|
|
964
|
+
|
|
965
|
+
3. **RTL Layout Issues**
|
|
966
|
+
- Use logical CSS properties
|
|
967
|
+
- Test with RTL browser tools
|
|
968
|
+
- Verify direction attribute
|
|
969
|
+
|
|
970
|
+
### Debug Mode
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
// Enable theme debugging
|
|
974
|
+
localStorage.setItem('theme-debug', 'true');
|
|
975
|
+
|
|
976
|
+
// Enable i18n debugging
|
|
977
|
+
localStorage.setItem('i18n-debug', 'true');
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
## Conclusion
|
|
981
|
+
|
|
982
|
+
The Theming and Internationalization module provides comprehensive support for creating accessible, multi-language, and visually adaptable applications. It ensures the SAAFE Redirection Flow can serve users across different regions, languages, and accessibility needs while maintaining a consistent and professional appearance.
|