@croacroa/react-native-template 2.1.0 → 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.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -21
- package/README.md +446 -402
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -418
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -30
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -40
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +5 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -375
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -176
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- 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():
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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;
|