@croacroa/react-native-template 2.0.1 → 3.2.0

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.
Files changed (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @fileoverview Upload hook with progress tracking, cancellation, and retry
3
+ * Wraps the media-upload service in a React hook with state management.
4
+ * @module hooks/useUpload
5
+ */
6
+
7
+ import { useState, useCallback, useRef } from "react";
8
+
9
+ import {
10
+ uploadFile,
11
+ type UploadProgress,
12
+ type UploadResult,
13
+ } from "@/services/media/media-upload";
14
+
15
+ /**
16
+ * Options for the useUpload hook
17
+ */
18
+ export interface UseUploadOptions {
19
+ /** Server URL to upload to */
20
+ url: string;
21
+ /** Additional HTTP headers */
22
+ headers?: Record<string, string>;
23
+ /** Form field name for the file (default: 'file') */
24
+ fieldName?: string;
25
+ }
26
+
27
+ /**
28
+ * Return type for the useUpload hook
29
+ */
30
+ export interface UseUploadReturn {
31
+ /** Start an upload */
32
+ upload: (
33
+ uri: string,
34
+ extraFields?: Record<string, string>
35
+ ) => Promise<UploadResult | null>;
36
+ /** Current upload progress */
37
+ progress: UploadProgress | null;
38
+ /** Whether an upload is in progress */
39
+ isUploading: boolean;
40
+ /** Last upload error */
41
+ error: string | null;
42
+ /** Cancel the current upload */
43
+ cancel: () => void;
44
+ /** Reset all state */
45
+ reset: () => void;
46
+ }
47
+
48
+ /**
49
+ * Hook for uploading files with progress tracking, cancellation, and retry support.
50
+ *
51
+ * @param options - Upload configuration
52
+ * @returns Upload function, progress state, and control functions
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * function UploadScreen() {
57
+ * const { upload, progress, isUploading, error, cancel, reset } = useUpload({
58
+ * url: 'https://api.example.com/upload',
59
+ * headers: { Authorization: `Bearer ${token}` },
60
+ * });
61
+ *
62
+ * const handleUpload = async (uri: string) => {
63
+ * const result = await upload(uri, { userId: '123' });
64
+ * if (result) {
65
+ * console.log('Upload complete:', result.status);
66
+ * }
67
+ * };
68
+ *
69
+ * return (
70
+ * <View>
71
+ * {isUploading && (
72
+ * <>
73
+ * <Text>Uploading... {progress?.percentage ?? 0}%</Text>
74
+ * <Button onPress={cancel}>Cancel</Button>
75
+ * </>
76
+ * )}
77
+ * {error && (
78
+ * <>
79
+ * <Text>{error}</Text>
80
+ * <Button onPress={() => handleUpload(lastUri)}>Retry</Button>
81
+ * </>
82
+ * )}
83
+ * </View>
84
+ * );
85
+ * }
86
+ * ```
87
+ */
88
+ export function useUpload(options: UseUploadOptions): UseUploadReturn {
89
+ const { url, headers, fieldName = "file" } = options;
90
+
91
+ const [progress, setProgress] = useState<UploadProgress | null>(null);
92
+ const [isUploading, setIsUploading] = useState(false);
93
+ const [error, setError] = useState<string | null>(null);
94
+
95
+ const abortRef = useRef<(() => void) | null>(null);
96
+
97
+ /**
98
+ * Start uploading a file
99
+ */
100
+ const upload = useCallback(
101
+ async (
102
+ uri: string,
103
+ extraFields?: Record<string, string>
104
+ ): Promise<UploadResult | null> => {
105
+ setError(null);
106
+ setProgress(null);
107
+ setIsUploading(true);
108
+
109
+ try {
110
+ const { promise, abort } = uploadFile({
111
+ url,
112
+ uri,
113
+ fieldName,
114
+ headers,
115
+ extraFields,
116
+ onProgress: (p) => {
117
+ setProgress(p);
118
+ },
119
+ });
120
+
121
+ abortRef.current = abort;
122
+
123
+ const result = await promise;
124
+ return result;
125
+ } catch (err) {
126
+ const message = err instanceof Error ? err.message : "Upload failed";
127
+ setError(message);
128
+ return null;
129
+ } finally {
130
+ abortRef.current = null;
131
+ setIsUploading(false);
132
+ }
133
+ },
134
+ [url, headers, fieldName]
135
+ );
136
+
137
+ /**
138
+ * Cancel the current upload
139
+ */
140
+ const cancel = useCallback(() => {
141
+ if (abortRef.current) {
142
+ abortRef.current();
143
+ abortRef.current = null;
144
+ }
145
+ }, []);
146
+
147
+ /**
148
+ * Reset all state
149
+ */
150
+ const reset = useCallback(() => {
151
+ cancel();
152
+ setProgress(null);
153
+ setIsUploading(false);
154
+ setError(null);
155
+ }, [cancel]);
156
+
157
+ return {
158
+ upload,
159
+ progress,
160
+ isUploading,
161
+ error,
162
+ cancel,
163
+ reset,
164
+ };
165
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @fileoverview WebSocket connection lifecycle hook
3
+ * Manages a WebSocketManager instance with auto-connect on mount
4
+ * and clean disconnect on unmount.
5
+ * @module hooks/useWebSocket
6
+ */
7
+
8
+ import { useEffect, useRef, useState, useCallback } from "react";
9
+
10
+ import { WebSocketManager } from "@/services/realtime/websocket-manager";
11
+ import type {
12
+ ConnectionStatus,
13
+ WebSocketConfig,
14
+ } from "@/services/realtime/types";
15
+
16
+ /**
17
+ * Return type for the useWebSocket hook.
18
+ */
19
+ export interface UseWebSocketReturn {
20
+ /** Current connection status */
21
+ status: ConnectionStatus;
22
+ /** Send a typed message over the WebSocket */
23
+ send: <T = unknown>(type: string, payload: T, channel?: string) => void;
24
+ /** Manually open the WebSocket connection */
25
+ connect: () => Promise<void>;
26
+ /** Manually close the WebSocket connection */
27
+ disconnect: () => void;
28
+ /** The underlying WebSocketManager instance */
29
+ manager: WebSocketManager;
30
+ }
31
+
32
+ /**
33
+ * Hook for managing a WebSocket connection lifecycle.
34
+ *
35
+ * Creates a single `WebSocketManager` instance (persisted across re-renders via ref),
36
+ * connects on mount, and disconnects on unmount. Tracks the connection status
37
+ * in React state so the component re-renders on status changes.
38
+ *
39
+ * @param config - WebSocket configuration (url, auth, reconnect settings)
40
+ * @returns Object with status, send, connect, disconnect, and the manager instance
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * function ChatScreen() {
45
+ * const { status, send, manager } = useWebSocket({
46
+ * url: 'wss://api.example.com/ws',
47
+ * getToken: async () => authStore.getState().token,
48
+ * });
49
+ *
50
+ * const handleSend = () => {
51
+ * send('chat:message', { text: 'Hello!' }, 'room-1');
52
+ * };
53
+ *
54
+ * return (
55
+ * <View>
56
+ * <Text>Status: {status}</Text>
57
+ * <Button onPress={handleSend} title="Send" />
58
+ * </View>
59
+ * );
60
+ * }
61
+ * ```
62
+ */
63
+ export function useWebSocket(config: WebSocketConfig): UseWebSocketReturn {
64
+ const managerRef = useRef<WebSocketManager | null>(null);
65
+ const [status, setStatus] = useState<ConnectionStatus>("disconnected");
66
+
67
+ // Create the manager once and keep it stable across re-renders
68
+ if (!managerRef.current) {
69
+ managerRef.current = new WebSocketManager(config);
70
+ }
71
+
72
+ const manager = managerRef.current;
73
+
74
+ useEffect(() => {
75
+ // Subscribe to status changes
76
+ const unsubscribe = manager.onStatusChange((newStatus) => {
77
+ setStatus(newStatus);
78
+ });
79
+
80
+ // Auto-connect on mount
81
+ manager.connect();
82
+
83
+ return () => {
84
+ unsubscribe();
85
+ manager.disconnect();
86
+ };
87
+ }, [manager]);
88
+
89
+ const send = useCallback(
90
+ <T = unknown>(type: string, payload: T, channel?: string) => {
91
+ manager.send(type, payload, channel);
92
+ },
93
+ [manager]
94
+ );
95
+
96
+ const connect = useCallback(async () => {
97
+ await manager.connect();
98
+ }, [manager]);
99
+
100
+ const disconnect = useCallback(() => {
101
+ manager.disconnect();
102
+ }, [manager]);
103
+
104
+ return {
105
+ status,
106
+ send,
107
+ connect,
108
+ disconnect,
109
+ manager,
110
+ };
111
+ }
package/i18n/index.ts CHANGED
@@ -1,194 +1,197 @@
1
- /**
2
- * @fileoverview Internationalization (i18n) setup with RTL support
3
- * Provides multi-language support with automatic device language detection.
4
- * @module i18n
5
- */
6
-
7
- import i18n from "i18next";
8
- import { initReactI18next } from "react-i18next";
9
- import * as Localization from "expo-localization";
10
- import { I18nManager } from "react-native";
11
- import { storage } from "@/services/storage";
12
-
13
- import en from "./locales/en.json";
14
- import fr from "./locales/fr.json";
15
- import es from "./locales/es.json";
16
- import de from "./locales/de.json";
17
- import ar from "./locales/ar.json";
18
-
19
- /**
20
- * Supported languages configuration.
21
- * Each language includes its English name, native name, and RTL flag.
22
- */
23
- export const LANGUAGES = {
24
- en: { name: "English", nativeName: "English", rtl: false },
25
- fr: { name: "French", nativeName: "Français", rtl: false },
26
- es: { name: "Spanish", nativeName: "Español", rtl: false },
27
- de: { name: "German", nativeName: "Deutsch", rtl: false },
28
- ar: { name: "Arabic", nativeName: "العربية", rtl: true },
29
- } as const;
30
-
31
- export type LanguageCode = keyof typeof LANGUAGES;
32
-
33
- const LANGUAGE_STORAGE_KEY = "app_language";
34
-
35
- const resources = {
36
- en: { translation: en },
37
- fr: { translation: fr },
38
- es: { translation: es },
39
- de: { translation: de },
40
- ar: { translation: ar },
41
- };
42
-
43
- /**
44
- * Get the device's preferred language
45
- * Falls back to 'en' if not supported
46
- */
47
- function getDeviceLanguage(): LanguageCode {
48
- const locale = Localization.getLocales()[0];
49
- const languageCode = locale?.languageCode || "en";
50
-
51
- // Check if we support this language
52
- if (languageCode in LANGUAGES) {
53
- return languageCode as LanguageCode;
54
- }
55
-
56
- return "en";
57
- }
58
-
59
- /**
60
- * Initialize i18n with the saved language or device language
61
- */
62
- export async function initI18n(): Promise<void> {
63
- // Try to get saved language preference
64
- const savedLanguage = await storage.get<LanguageCode>(LANGUAGE_STORAGE_KEY);
65
- const initialLanguage = savedLanguage || getDeviceLanguage();
66
-
67
- await i18n.use(initReactI18next).init({
68
- resources,
69
- lng: initialLanguage,
70
- fallbackLng: "en",
71
- compatibilityJSON: "v3",
72
- interpolation: {
73
- escapeValue: false, // React already escapes values
74
- },
75
- react: {
76
- useSuspense: false, // Prevents issues with SSR/async loading
77
- },
78
- });
79
- }
80
-
81
- /**
82
- * Change the app language and apply RTL settings if needed.
83
- * Note: RTL changes require an app restart to take full effect.
84
- *
85
- * @param language - The language code to switch to
86
- * @returns Promise that resolves when the language is changed
87
- */
88
- export async function changeLanguage(language: LanguageCode): Promise<void> {
89
- await i18n.changeLanguage(language);
90
- await storage.set(LANGUAGE_STORAGE_KEY, language);
91
-
92
- // Handle RTL layout direction
93
- const isRTL = LANGUAGES[language].rtl;
94
- if (I18nManager.isRTL !== isRTL) {
95
- I18nManager.allowRTL(isRTL);
96
- I18nManager.forceRTL(isRTL);
97
- // Note: App restart is required for RTL changes to take full effect
98
- }
99
- }
100
-
101
- /**
102
- * Check if the current language is RTL
103
- */
104
- export function isCurrentLanguageRTL(): boolean {
105
- const currentLang = getCurrentLanguage();
106
- return LANGUAGES[currentLang]?.rtl ?? false;
107
- }
108
-
109
- /**
110
- * Get all available languages as an array for UI selectors
111
- */
112
- export function getAvailableLanguages(): Array<{
113
- code: LanguageCode;
114
- name: string;
115
- nativeName: string;
116
- rtl: boolean;
117
- }> {
118
- return Object.entries(LANGUAGES).map(([code, config]) => ({
119
- code: code as LanguageCode,
120
- ...config,
121
- }));
122
- }
123
-
124
- /**
125
- * Get current language
126
- */
127
- export function getCurrentLanguage(): LanguageCode {
128
- return (i18n.language || "en") as LanguageCode;
129
- }
130
-
131
- /**
132
- * Hook-friendly RTL detection
133
- * Returns current RTL state from I18nManager
134
- */
135
- export function isRTL(): boolean {
136
- return I18nManager.isRTL;
137
- }
138
-
139
- /**
140
- * Get text alignment based on RTL
141
- * Useful for styling text components
142
- */
143
- export function getTextAlign(): "left" | "right" {
144
- return I18nManager.isRTL ? "right" : "left";
145
- }
146
-
147
- /**
148
- * Get flex direction based on RTL
149
- * Useful for horizontal layouts
150
- */
151
- export function getFlexDirection(): "row" | "row-reverse" {
152
- return I18nManager.isRTL ? "row-reverse" : "row";
153
- }
154
-
155
- /**
156
- * Get start/end values swapped for RTL
157
- * Useful for margins, paddings, and positioning
158
- */
159
- export function getStartEnd(): { start: "left" | "right"; end: "left" | "right" } {
160
- return I18nManager.isRTL
161
- ? { start: "right", end: "left" }
162
- : { start: "left", end: "right" };
163
- }
164
-
165
- /**
166
- * Transform a value for RTL (e.g., for translateX animations)
167
- * @param value - The original value
168
- * @returns The transformed value (negated for RTL)
169
- */
170
- export function rtlTransform(value: number): number {
171
- return I18nManager.isRTL ? -value : value;
172
- }
173
-
174
- /**
175
- * Check if a specific language requires RTL
176
- */
177
- export function isLanguageRTL(languageCode: string): boolean {
178
- const lang = LANGUAGES[languageCode as LanguageCode];
179
- return lang?.rtl ?? false;
180
- }
181
-
182
- /**
183
- * Force app restart for RTL changes to take effect
184
- * Call this after changing to/from an RTL language
185
- */
186
- export async function applyRTLAndRestart(isRTL: boolean): Promise<void> {
187
- I18nManager.allowRTL(isRTL);
188
- I18nManager.forceRTL(isRTL);
189
- // Note: In production, use expo-updates to reload the app
190
- // await Updates.reloadAsync();
191
- }
192
-
193
- export { i18n };
194
- export default i18n;
1
+ /**
2
+ * @fileoverview Internationalization (i18n) setup with RTL support
3
+ * Provides multi-language support with automatic device language detection.
4
+ * @module i18n
5
+ */
6
+
7
+ import i18n from "i18next";
8
+ import { initReactI18next } from "react-i18next";
9
+ import * as Localization from "expo-localization";
10
+ import { I18nManager } from "react-native";
11
+ import { storage } from "@/services/storage";
12
+
13
+ import en from "./locales/en.json";
14
+ import fr from "./locales/fr.json";
15
+ import es from "./locales/es.json";
16
+ import de from "./locales/de.json";
17
+ import ar from "./locales/ar.json";
18
+
19
+ /**
20
+ * Supported languages configuration.
21
+ * Each language includes its English name, native name, and RTL flag.
22
+ */
23
+ export const LANGUAGES = {
24
+ en: { name: "English", nativeName: "English", rtl: false },
25
+ fr: { name: "French", nativeName: "Français", rtl: false },
26
+ es: { name: "Spanish", nativeName: "Español", rtl: false },
27
+ de: { name: "German", nativeName: "Deutsch", rtl: false },
28
+ ar: { name: "Arabic", nativeName: "العربية", rtl: true },
29
+ } as const;
30
+
31
+ export type LanguageCode = keyof typeof LANGUAGES;
32
+
33
+ const LANGUAGE_STORAGE_KEY = "app_language";
34
+
35
+ const resources = {
36
+ en: { translation: en },
37
+ fr: { translation: fr },
38
+ es: { translation: es },
39
+ de: { translation: de },
40
+ ar: { translation: ar },
41
+ };
42
+
43
+ /**
44
+ * Get the device's preferred language
45
+ * Falls back to 'en' if not supported
46
+ */
47
+ function getDeviceLanguage(): LanguageCode {
48
+ const locale = Localization.getLocales()[0];
49
+ const languageCode = locale?.languageCode || "en";
50
+
51
+ // Check if we support this language
52
+ if (languageCode in LANGUAGES) {
53
+ return languageCode as LanguageCode;
54
+ }
55
+
56
+ return "en";
57
+ }
58
+
59
+ /**
60
+ * Initialize i18n with the saved language or device language
61
+ */
62
+ export async function initI18n(): Promise<void> {
63
+ // Try to get saved language preference
64
+ const savedLanguage = await storage.get<LanguageCode>(LANGUAGE_STORAGE_KEY);
65
+ const initialLanguage = savedLanguage || getDeviceLanguage();
66
+
67
+ await i18n.use(initReactI18next).init({
68
+ resources,
69
+ lng: initialLanguage,
70
+ fallbackLng: "en",
71
+ compatibilityJSON: "v3",
72
+ interpolation: {
73
+ escapeValue: false, // React already escapes values
74
+ },
75
+ react: {
76
+ useSuspense: false, // Prevents issues with SSR/async loading
77
+ },
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Change the app language and apply RTL settings if needed.
83
+ * Note: RTL changes require an app restart to take full effect.
84
+ *
85
+ * @param language - The language code to switch to
86
+ * @returns Promise that resolves when the language is changed
87
+ */
88
+ export async function changeLanguage(language: LanguageCode): Promise<void> {
89
+ await i18n.changeLanguage(language);
90
+ await storage.set(LANGUAGE_STORAGE_KEY, language);
91
+
92
+ // Handle RTL layout direction
93
+ const isRTL = LANGUAGES[language].rtl;
94
+ if (I18nManager.isRTL !== isRTL) {
95
+ I18nManager.allowRTL(isRTL);
96
+ I18nManager.forceRTL(isRTL);
97
+ // Note: App restart is required for RTL changes to take full effect
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Check if the current language is RTL
103
+ */
104
+ export function isCurrentLanguageRTL(): boolean {
105
+ const currentLang = getCurrentLanguage();
106
+ return LANGUAGES[currentLang]?.rtl ?? false;
107
+ }
108
+
109
+ /**
110
+ * Get all available languages as an array for UI selectors
111
+ */
112
+ export function getAvailableLanguages(): {
113
+ code: LanguageCode;
114
+ name: string;
115
+ nativeName: string;
116
+ rtl: boolean;
117
+ }[] {
118
+ return Object.entries(LANGUAGES).map(([code, config]) => ({
119
+ code: code as LanguageCode,
120
+ ...config,
121
+ }));
122
+ }
123
+
124
+ /**
125
+ * Get current language
126
+ */
127
+ export function getCurrentLanguage(): LanguageCode {
128
+ return (i18n.language || "en") as LanguageCode;
129
+ }
130
+
131
+ /**
132
+ * Hook-friendly RTL detection
133
+ * Returns current RTL state from I18nManager
134
+ */
135
+ export function isRTL(): boolean {
136
+ return I18nManager.isRTL;
137
+ }
138
+
139
+ /**
140
+ * Get text alignment based on RTL
141
+ * Useful for styling text components
142
+ */
143
+ export function getTextAlign(): "left" | "right" {
144
+ return I18nManager.isRTL ? "right" : "left";
145
+ }
146
+
147
+ /**
148
+ * Get flex direction based on RTL
149
+ * Useful for horizontal layouts
150
+ */
151
+ export function getFlexDirection(): "row" | "row-reverse" {
152
+ return I18nManager.isRTL ? "row-reverse" : "row";
153
+ }
154
+
155
+ /**
156
+ * Get start/end values swapped for RTL
157
+ * Useful for margins, paddings, and positioning
158
+ */
159
+ export function getStartEnd(): {
160
+ start: "left" | "right";
161
+ end: "left" | "right";
162
+ } {
163
+ return I18nManager.isRTL
164
+ ? { start: "right", end: "left" }
165
+ : { start: "left", end: "right" };
166
+ }
167
+
168
+ /**
169
+ * Transform a value for RTL (e.g., for translateX animations)
170
+ * @param value - The original value
171
+ * @returns The transformed value (negated for RTL)
172
+ */
173
+ export function rtlTransform(value: number): number {
174
+ return I18nManager.isRTL ? -value : value;
175
+ }
176
+
177
+ /**
178
+ * Check if a specific language requires RTL
179
+ */
180
+ export function isLanguageRTL(languageCode: string): boolean {
181
+ const lang = LANGUAGES[languageCode as LanguageCode];
182
+ return lang?.rtl ?? false;
183
+ }
184
+
185
+ /**
186
+ * Force app restart for RTL changes to take effect
187
+ * Call this after changing to/from an RTL language
188
+ */
189
+ export async function applyRTLAndRestart(isRTL: boolean): Promise<void> {
190
+ I18nManager.allowRTL(isRTL);
191
+ I18nManager.forceRTL(isRTL);
192
+ // Note: In production, use expo-updates to reload the app
193
+ // await Updates.reloadAsync();
194
+ }
195
+
196
+ export { i18n };
197
+ export default i18n;