@umituz/react-native-settings 5.4.21 → 5.4.23
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/package.json +3 -1
- package/src/domains/ai-consent/presentation/components/AIConsentModal.tsx +7 -3
- package/src/domains/ai-consent/presentation/screens/AIConsentScreen.tsx +26 -26
- package/src/domains/feedback/presentation/components/FeedbackForm.styles.ts +8 -2
- package/src/domains/gamification/components/AchievementItem.tsx +3 -2
- package/src/domains/gamification/components/GamificationScreen/GamificationScreen.tsx +2 -1
- package/src/domains/gamification/components/GamificationScreen/styles.ts +43 -37
- package/src/domains/gamification/components/styles/achievementItemStyles.ts +75 -68
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +28 -25
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +6 -6
- package/src/domains/localization/presentation/components/LanguageItem.styles.ts +47 -33
- package/src/domains/localization/presentation/components/LanguageItem.tsx +9 -29
- package/src/domains/localization/scripts/translate-missing.js +37 -9
- package/src/domains/localization/scripts/utils/translation-config.js +13 -109
- package/src/domains/localization/scripts/utils/translator.js +0 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.23",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification - expo-store-review and expo-device now lazy loaded",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -124,6 +124,7 @@
|
|
|
124
124
|
"@react-navigation/stack": ">=6.0.0",
|
|
125
125
|
"@tanstack/react-query": ">=5.0.0",
|
|
126
126
|
"@umituz/react-native-design-system": "*",
|
|
127
|
+
"@umituz/react-native-google-translate": "*",
|
|
127
128
|
"expo": ">=54.0.0",
|
|
128
129
|
"expo-constants": ">=55.0.0",
|
|
129
130
|
"expo-device": ">=6.0.0",
|
|
@@ -162,6 +163,7 @@
|
|
|
162
163
|
"@umituz/react-native-auth": "^4.3.39",
|
|
163
164
|
"@umituz/react-native-design-system": "^4.27.0",
|
|
164
165
|
"@umituz/react-native-firebase": "^2.4.55",
|
|
166
|
+
"@umituz/react-native-google-translate": "^1.0.0",
|
|
165
167
|
"@umituz/react-native-sentry": "latest",
|
|
166
168
|
"eslint": "^8.57.0",
|
|
167
169
|
"eslint-plugin-react": "^7.37.5",
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import React, { memo } from 'react';
|
|
19
|
-
import { Modal, View
|
|
19
|
+
import { Modal, View } from 'react-native';
|
|
20
20
|
import { AIConsentScreen, type AIProvider } from '../screens/AIConsentScreen';
|
|
21
|
+
import { useThemedStyleSheet } from '@umituz/react-native-design-system/theme';
|
|
22
|
+
import type { Theme } from '@umituz/react-native-design-system/theme';
|
|
21
23
|
|
|
22
24
|
export interface AIConsentModalProps {
|
|
23
25
|
visible: boolean;
|
|
@@ -34,6 +36,8 @@ export const AIConsentModal: React.FC<AIConsentModalProps> = memo(({
|
|
|
34
36
|
providers,
|
|
35
37
|
customMessage,
|
|
36
38
|
}) => {
|
|
39
|
+
const styles = useThemedStyleSheet(createStyles);
|
|
40
|
+
|
|
37
41
|
return (
|
|
38
42
|
<Modal
|
|
39
43
|
visible={visible}
|
|
@@ -56,9 +60,9 @@ export const AIConsentModal: React.FC<AIConsentModalProps> = memo(({
|
|
|
56
60
|
|
|
57
61
|
AIConsentModal.displayName = 'AIConsentModal';
|
|
58
62
|
|
|
59
|
-
const
|
|
63
|
+
const createStyles = (theme: Theme) => ({
|
|
60
64
|
container: {
|
|
61
65
|
flex: 1,
|
|
62
|
-
backgroundColor:
|
|
66
|
+
backgroundColor: theme.colors.backgroundPrimary,
|
|
63
67
|
},
|
|
64
68
|
});
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import React, { memo } from 'react';
|
|
16
16
|
import {
|
|
17
17
|
View,
|
|
18
|
-
StyleSheet,
|
|
19
18
|
ScrollView,
|
|
20
19
|
TouchableOpacity,
|
|
21
20
|
} from 'react-native';
|
|
@@ -31,7 +30,8 @@ import {
|
|
|
31
30
|
NavigationHeader,
|
|
32
31
|
useAppNavigation,
|
|
33
32
|
} from '@umituz/react-native-design-system/molecules';
|
|
34
|
-
import {
|
|
33
|
+
import { useThemedStyleSheet } from '@umituz/react-native-design-system/theme';
|
|
34
|
+
import type { Theme } from '@umituz/react-native-design-system/theme';
|
|
35
35
|
|
|
36
36
|
export interface AIProvider {
|
|
37
37
|
name: string;
|
|
@@ -85,7 +85,7 @@ export const AIConsentScreen: React.FC<AIConsentScreenProps> = memo(({
|
|
|
85
85
|
standalone = false,
|
|
86
86
|
}) => {
|
|
87
87
|
const navigation = useAppNavigation();
|
|
88
|
-
const
|
|
88
|
+
const styles = useThemedStyleSheet(createStyles);
|
|
89
89
|
const [loading, setLoading] = React.useState(false);
|
|
90
90
|
|
|
91
91
|
// Get params from route or use props
|
|
@@ -289,7 +289,7 @@ export const AIConsentScreen: React.FC<AIConsentScreenProps> = memo(({
|
|
|
289
289
|
|
|
290
290
|
AIConsentScreen.displayName = 'AIConsentScreen';
|
|
291
291
|
|
|
292
|
-
const
|
|
292
|
+
const createStyles = (theme: Theme) => ({
|
|
293
293
|
scrollView: {
|
|
294
294
|
flex: 1,
|
|
295
295
|
},
|
|
@@ -300,32 +300,32 @@ const styles = StyleSheet.create({
|
|
|
300
300
|
fontSize: 15,
|
|
301
301
|
lineHeight: 22,
|
|
302
302
|
marginBottom: 24,
|
|
303
|
-
color:
|
|
303
|
+
color: theme.colors.textSecondary,
|
|
304
304
|
},
|
|
305
305
|
section: {
|
|
306
306
|
marginBottom: 24,
|
|
307
307
|
padding: 16,
|
|
308
|
-
backgroundColor:
|
|
308
|
+
backgroundColor: theme.colors.surfaceVariant,
|
|
309
309
|
borderRadius: 12,
|
|
310
310
|
},
|
|
311
311
|
sectionTitle: {
|
|
312
312
|
fontSize: 17,
|
|
313
313
|
fontWeight: '600',
|
|
314
|
-
color:
|
|
314
|
+
color: theme.colors.textPrimary,
|
|
315
315
|
marginBottom: 12,
|
|
316
316
|
},
|
|
317
317
|
providerList: {
|
|
318
318
|
gap: 12,
|
|
319
319
|
},
|
|
320
320
|
providerItem: {
|
|
321
|
-
backgroundColor:
|
|
321
|
+
backgroundColor: theme.colors.surface,
|
|
322
322
|
padding: 12,
|
|
323
323
|
borderRadius: 8,
|
|
324
324
|
borderWidth: 1,
|
|
325
|
-
borderColor:
|
|
325
|
+
borderColor: theme.colors.borderLight,
|
|
326
326
|
},
|
|
327
327
|
providerHeader: {
|
|
328
|
-
flexDirection: 'row',
|
|
328
|
+
flexDirection: 'row' as const,
|
|
329
329
|
justifyContent: 'space-between',
|
|
330
330
|
alignItems: 'center',
|
|
331
331
|
marginBottom: 4,
|
|
@@ -333,26 +333,26 @@ const styles = StyleSheet.create({
|
|
|
333
333
|
providerName: {
|
|
334
334
|
fontSize: 15,
|
|
335
335
|
fontWeight: '600',
|
|
336
|
-
color:
|
|
336
|
+
color: theme.colors.textPrimary,
|
|
337
337
|
},
|
|
338
338
|
privacyLink: {
|
|
339
339
|
fontSize: 13,
|
|
340
|
-
color:
|
|
340
|
+
color: theme.colors.primary,
|
|
341
341
|
},
|
|
342
342
|
providerPurpose: {
|
|
343
343
|
fontSize: 14,
|
|
344
|
-
color:
|
|
344
|
+
color: theme.colors.textTertiary,
|
|
345
345
|
},
|
|
346
346
|
bullet: {
|
|
347
347
|
fontSize: 15,
|
|
348
|
-
color:
|
|
348
|
+
color: theme.colors.textSecondary,
|
|
349
349
|
lineHeight: 22,
|
|
350
350
|
marginLeft: 8,
|
|
351
351
|
},
|
|
352
352
|
note: {
|
|
353
353
|
fontSize: 13,
|
|
354
|
-
color:
|
|
355
|
-
fontStyle: 'italic',
|
|
354
|
+
color: theme.colors.textTertiary,
|
|
355
|
+
fontStyle: 'italic' as const,
|
|
356
356
|
marginLeft: 8,
|
|
357
357
|
marginTop: 4,
|
|
358
358
|
},
|
|
@@ -362,15 +362,15 @@ const styles = StyleSheet.create({
|
|
|
362
362
|
},
|
|
363
363
|
linkText: {
|
|
364
364
|
fontSize: 15,
|
|
365
|
-
color:
|
|
365
|
+
color: theme.colors.primary,
|
|
366
366
|
fontWeight: '500',
|
|
367
367
|
},
|
|
368
368
|
declarationSection: {
|
|
369
|
-
flexDirection: 'row',
|
|
369
|
+
flexDirection: 'row' as const,
|
|
370
370
|
alignItems: 'flex-start',
|
|
371
371
|
marginTop: 8,
|
|
372
372
|
padding: 12,
|
|
373
|
-
backgroundColor: '
|
|
373
|
+
backgroundColor: theme.colors.primaryBackground || 'rgba(90, 175, 127, 0.15)',
|
|
374
374
|
borderRadius: 8,
|
|
375
375
|
},
|
|
376
376
|
checkbox: {
|
|
@@ -383,26 +383,26 @@ const styles = StyleSheet.create({
|
|
|
383
383
|
width: 24,
|
|
384
384
|
height: 24,
|
|
385
385
|
borderRadius: 4,
|
|
386
|
-
backgroundColor:
|
|
387
|
-
alignItems: 'center',
|
|
388
|
-
justifyContent: 'center',
|
|
386
|
+
backgroundColor: theme.colors.primary,
|
|
387
|
+
alignItems: 'center' as const,
|
|
388
|
+
justifyContent: 'center' as const,
|
|
389
389
|
},
|
|
390
390
|
checkmark: {
|
|
391
|
-
color: '#FFFFFF',
|
|
391
|
+
color: theme.colors.onPrimary || '#FFFFFF',
|
|
392
392
|
fontSize: 16,
|
|
393
393
|
fontWeight: '700',
|
|
394
394
|
},
|
|
395
395
|
declarationText: {
|
|
396
396
|
flex: 1,
|
|
397
397
|
fontSize: 14,
|
|
398
|
-
color:
|
|
398
|
+
color: theme.colors.textPrimary,
|
|
399
399
|
lineHeight: 20,
|
|
400
400
|
},
|
|
401
401
|
footer: {
|
|
402
|
-
flexDirection: 'row',
|
|
402
|
+
flexDirection: 'row' as const,
|
|
403
403
|
padding: 16,
|
|
404
404
|
borderTopWidth: 1,
|
|
405
|
-
borderTopColor:
|
|
405
|
+
borderTopColor: theme.colors.borderLight,
|
|
406
406
|
gap: 12,
|
|
407
407
|
},
|
|
408
408
|
declineButton: {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { StyleSheet } from "react-native";
|
|
2
2
|
import type { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
3
|
+
import { withAlpha } from "@umituz/react-native-design-system/theme";
|
|
3
4
|
|
|
4
|
-
export const getFeedbackFormStyles = (
|
|
5
|
+
export const getFeedbackFormStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
5
6
|
StyleSheet.create({
|
|
6
7
|
container: {
|
|
7
8
|
width: "100%",
|
|
@@ -25,7 +26,7 @@ export const getFeedbackFormStyles = (_tokens: ReturnType<typeof useAppDesignTok
|
|
|
25
26
|
alignItems: "center",
|
|
26
27
|
marginVertical: 32,
|
|
27
28
|
paddingVertical: 16,
|
|
28
|
-
backgroundColor:
|
|
29
|
+
backgroundColor: withAlpha(tokens.colors.primary, 0.05),
|
|
29
30
|
borderRadius: 16,
|
|
30
31
|
},
|
|
31
32
|
ratingLabel: {
|
|
@@ -34,6 +35,7 @@ export const getFeedbackFormStyles = (_tokens: ReturnType<typeof useAppDesignTok
|
|
|
34
35
|
letterSpacing: 1,
|
|
35
36
|
marginBottom: 16,
|
|
36
37
|
textTransform: "uppercase",
|
|
38
|
+
color: tokens.colors.textSecondary,
|
|
37
39
|
},
|
|
38
40
|
stars: {
|
|
39
41
|
flexDirection: "row",
|
|
@@ -52,10 +54,14 @@ export const getFeedbackFormStyles = (_tokens: ReturnType<typeof useAppDesignTok
|
|
|
52
54
|
borderRadius: 16,
|
|
53
55
|
padding: 16,
|
|
54
56
|
fontSize: 15,
|
|
57
|
+
color: tokens.colors.textPrimary,
|
|
58
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
59
|
+
borderColor: tokens.colors.border,
|
|
55
60
|
},
|
|
56
61
|
errorText: {
|
|
57
62
|
marginTop: 8,
|
|
58
63
|
fontWeight: "600",
|
|
64
|
+
color: tokens.colors.error,
|
|
59
65
|
},
|
|
60
66
|
submitButton: {
|
|
61
67
|
width: "100%",
|
|
@@ -8,7 +8,7 @@ import { View } from "react-native";
|
|
|
8
8
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system/theme";
|
|
10
10
|
import type { AchievementItemProps } from "./types/AchievementItemProps";
|
|
11
|
-
import {
|
|
11
|
+
import { createAchievementItemStyles } from "./styles/achievementItemStyles";
|
|
12
12
|
|
|
13
13
|
export const AchievementItem: React.FC<AchievementItemProps> = ({
|
|
14
14
|
title,
|
|
@@ -28,6 +28,7 @@ export const AchievementItem: React.FC<AchievementItemProps> = ({
|
|
|
28
28
|
lockedOpacity,
|
|
29
29
|
}) => {
|
|
30
30
|
const tokens = useAppDesignTokens();
|
|
31
|
+
const styles = createAchievementItemStyles(tokens);
|
|
31
32
|
const finalAccentColor = accentColor || tokens.colors.primary;
|
|
32
33
|
const finalBackgroundColor = backgroundColor || tokens.colors.surface;
|
|
33
34
|
const finalTextColor = textColor || tokens.colors.textPrimary;
|
|
@@ -61,7 +62,7 @@ export const AchievementItem: React.FC<AchievementItemProps> = ({
|
|
|
61
62
|
</AtomicText>
|
|
62
63
|
{isUnlocked && (
|
|
63
64
|
<View style={[styles.checkmark, { backgroundColor: finalAccentColor }]}>
|
|
64
|
-
<AtomicText style={[styles.checkmarkText, { color: tokens.colors.
|
|
65
|
+
<AtomicText style={[styles.checkmarkText, { color: tokens.colors.onPrimary || '#FFFFFF' }]}>
|
|
65
66
|
✓
|
|
66
67
|
</AtomicText>
|
|
67
68
|
</View>
|
|
@@ -14,7 +14,7 @@ import { LevelProgress } from "../LevelProgress";
|
|
|
14
14
|
import { StreakDisplay } from "../StreakDisplay";
|
|
15
15
|
import { StatsGrid } from "./StatsGrid";
|
|
16
16
|
import { AchievementsList } from "./AchievementsList";
|
|
17
|
-
import {
|
|
17
|
+
import { createStyles } from "./styles";
|
|
18
18
|
import type { GamificationScreenProps } from "./types";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -39,6 +39,7 @@ export const GamificationScreenInner: React.FC<GamificationScreenProps> = ({
|
|
|
39
39
|
}) => {
|
|
40
40
|
const _navigation = useAppNavigation();
|
|
41
41
|
const tokens = useAppDesignTokens();
|
|
42
|
+
const styles = createStyles(tokens);
|
|
42
43
|
|
|
43
44
|
// Use tokens for fallbacks
|
|
44
45
|
const finalAccentColor = accentColor || tokens.colors.primary;
|
|
@@ -3,41 +3,47 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { StyleSheet } from "react-native";
|
|
6
|
+
import type { DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
7
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
8
|
+
export const createStyles = (tokens: DesignTokens) =>
|
|
9
|
+
StyleSheet.create({
|
|
10
|
+
container: {
|
|
11
|
+
flex: 1,
|
|
12
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
13
|
+
},
|
|
14
|
+
scrollView: {
|
|
15
|
+
flex: 1,
|
|
16
|
+
},
|
|
17
|
+
scrollContent: {
|
|
18
|
+
padding: tokens.spacing.md,
|
|
19
|
+
paddingBottom: tokens.spacing.xl,
|
|
20
|
+
},
|
|
21
|
+
header: {
|
|
22
|
+
marginBottom: tokens.spacing.lg,
|
|
23
|
+
},
|
|
24
|
+
title: {
|
|
25
|
+
fontSize: 28,
|
|
26
|
+
fontWeight: "bold",
|
|
27
|
+
color: tokens.colors.textPrimary,
|
|
28
|
+
},
|
|
29
|
+
section: {
|
|
30
|
+
marginBottom: tokens.spacing.xl,
|
|
31
|
+
},
|
|
32
|
+
sectionTitle: {
|
|
33
|
+
fontSize: 18,
|
|
34
|
+
fontWeight: "600",
|
|
35
|
+
marginBottom: tokens.spacing.md,
|
|
36
|
+
color: tokens.colors.textPrimary,
|
|
37
|
+
},
|
|
38
|
+
statsGrid: {
|
|
39
|
+
flexDirection: "row",
|
|
40
|
+
flexWrap: "wrap",
|
|
41
|
+
gap: tokens.spacing.md,
|
|
42
|
+
},
|
|
43
|
+
emptyText: {
|
|
44
|
+
fontSize: 14,
|
|
45
|
+
textAlign: "center",
|
|
46
|
+
paddingVertical: tokens.spacing.lg,
|
|
47
|
+
color: tokens.colors.textSecondary,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -3,72 +3,79 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { StyleSheet } from 'react-native';
|
|
6
|
+
import type { DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
7
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
8
|
+
export const createAchievementItemStyles = (tokens: DesignTokens) =>
|
|
9
|
+
StyleSheet.create({
|
|
10
|
+
container: {
|
|
11
|
+
flexDirection: "row",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
padding: tokens.spacing.md,
|
|
14
|
+
borderRadius: tokens.borders.radius.md,
|
|
15
|
+
marginBottom: tokens.spacing.sm,
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
borderColor: tokens.colors.borderLight,
|
|
18
|
+
},
|
|
19
|
+
iconContainer: {
|
|
20
|
+
width: 48,
|
|
21
|
+
height: 48,
|
|
22
|
+
borderRadius: 24,
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
justifyContent: "center",
|
|
25
|
+
marginRight: tokens.spacing.md,
|
|
26
|
+
},
|
|
27
|
+
content: {
|
|
28
|
+
flex: 1,
|
|
29
|
+
},
|
|
30
|
+
header: {
|
|
31
|
+
flexDirection: "row",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "space-between",
|
|
34
|
+
marginBottom: 4,
|
|
35
|
+
},
|
|
36
|
+
title: {
|
|
37
|
+
fontSize: 16,
|
|
38
|
+
fontWeight: "600",
|
|
39
|
+
flex: 1,
|
|
40
|
+
color: "inherit",
|
|
41
|
+
},
|
|
42
|
+
checkmark: {
|
|
43
|
+
width: 20,
|
|
44
|
+
height: 20,
|
|
45
|
+
borderRadius: 10,
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
marginLeft: 8,
|
|
49
|
+
},
|
|
50
|
+
checkmarkText: {
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
fontWeight: "bold",
|
|
53
|
+
},
|
|
54
|
+
description: {
|
|
55
|
+
fontSize: 13,
|
|
56
|
+
lineHeight: 18,
|
|
57
|
+
color: "inherit",
|
|
58
|
+
},
|
|
59
|
+
progressContainer: {
|
|
60
|
+
marginTop: 8,
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
gap: 8,
|
|
64
|
+
},
|
|
65
|
+
progressBar: {
|
|
66
|
+
flex: 1,
|
|
67
|
+
height: 4,
|
|
68
|
+
borderRadius: 2,
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
},
|
|
71
|
+
progressFill: {
|
|
72
|
+
height: "100%",
|
|
73
|
+
borderRadius: 2,
|
|
74
|
+
},
|
|
75
|
+
progressText: {
|
|
76
|
+
fontSize: 11,
|
|
77
|
+
minWidth: 40,
|
|
78
|
+
textAlign: "right",
|
|
79
|
+
color: "inherit",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -3,36 +3,39 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { StyleSheet } from 'react-native';
|
|
6
|
+
import type { DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_CONFIG = {
|
|
8
9
|
defaultIconSize: 20,
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
12
|
+
export const createLanguageSwitcherStyles = (tokens: DesignTokens) =>
|
|
13
|
+
StyleSheet.create({
|
|
14
|
+
container: {
|
|
15
|
+
flexDirection: 'row',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
gap: tokens.spacing.sm,
|
|
18
|
+
paddingHorizontal: tokens.spacing.xs,
|
|
19
|
+
paddingVertical: tokens.spacing.xs,
|
|
20
|
+
},
|
|
21
|
+
disabled: {
|
|
22
|
+
opacity: 0.5,
|
|
23
|
+
},
|
|
24
|
+
flag: {
|
|
25
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
},
|
|
28
|
+
languageName: {
|
|
29
|
+
fontSize: 14,
|
|
30
|
+
fontWeight: '600',
|
|
31
|
+
textAlign: 'center',
|
|
32
|
+
color: tokens.colors.textPrimary,
|
|
33
|
+
},
|
|
34
|
+
icon: {
|
|
35
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
36
|
+
textAlign: 'center',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
36
39
|
|
|
37
40
|
export const DEFAULT_CONFIG_VALUES = {
|
|
38
41
|
hitSlop: { top: 10, bottom: 10, left: 10, right: 10 } as const,
|
|
@@ -8,7 +8,7 @@ import { TouchableOpacity, type StyleProp, type ViewStyle, type TextStyle } from
|
|
|
8
8
|
import { AtomicText } from '@umituz/react-native-design-system/atoms';
|
|
9
9
|
import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
|
|
10
10
|
import { useLanguageSwitcher } from './useLanguageSwitcher';
|
|
11
|
-
import {
|
|
11
|
+
import { createLanguageSwitcherStyles, DEFAULT_CONFIG_VALUES } from './LanguageSwitcher.styles';
|
|
12
12
|
|
|
13
13
|
export interface LanguageSwitcherProps {
|
|
14
14
|
showName?: boolean;
|
|
@@ -36,6 +36,7 @@ export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
|
|
36
36
|
accessibilityLabel,
|
|
37
37
|
}) => {
|
|
38
38
|
const tokens = useAppDesignTokens();
|
|
39
|
+
const styles = createLanguageSwitcherStyles(tokens);
|
|
39
40
|
const { currentLang, handlePress } = useLanguageSwitcher({ onPress, disabled });
|
|
40
41
|
|
|
41
42
|
const accessibilityProps = useMemo(() => ({
|
|
@@ -50,9 +51,8 @@ export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
|
|
50
51
|
return (
|
|
51
52
|
<TouchableOpacity
|
|
52
53
|
style={[
|
|
53
|
-
styles.container,
|
|
54
|
-
|
|
55
|
-
style,
|
|
54
|
+
styles.container,
|
|
55
|
+
style,
|
|
56
56
|
disabled && styles.disabled
|
|
57
57
|
]}
|
|
58
58
|
onPress={handlePress}
|
|
@@ -68,8 +68,8 @@ export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
|
|
68
68
|
</AtomicText>
|
|
69
69
|
)}
|
|
70
70
|
{showName && (
|
|
71
|
-
<AtomicText
|
|
72
|
-
type="bodySmall"
|
|
71
|
+
<AtomicText
|
|
72
|
+
type="bodySmall"
|
|
73
73
|
style={[styles.languageName, { color: textColor, fontWeight: '600' }, textStyle]}
|
|
74
74
|
>
|
|
75
75
|
{currentLang.nativeName}
|
|
@@ -3,38 +3,52 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { StyleSheet } from 'react-native';
|
|
6
|
+
import type { DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
7
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
8
|
+
export const createLanguageItemStyles = (tokens: DesignTokens) =>
|
|
9
|
+
StyleSheet.create({
|
|
10
|
+
languageItem: {
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'space-between',
|
|
14
|
+
borderWidth: 1,
|
|
15
|
+
borderColor: tokens.colors.borderLight,
|
|
16
|
+
backgroundColor: tokens.colors.surface,
|
|
17
|
+
borderRadius: tokens.borders.radius.md,
|
|
18
|
+
marginBottom: tokens.spacing.sm,
|
|
19
|
+
overflow: 'hidden',
|
|
20
|
+
},
|
|
21
|
+
selectedLanguageItem: {
|
|
22
|
+
borderWidth: 2,
|
|
23
|
+
borderColor: tokens.colors.primary,
|
|
24
|
+
},
|
|
25
|
+
languageContent: {
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
flex: 1,
|
|
29
|
+
padding: tokens.spacing.md,
|
|
30
|
+
},
|
|
31
|
+
flag: {
|
|
32
|
+
fontSize: 24,
|
|
33
|
+
marginRight: tokens.spacing.md,
|
|
34
|
+
},
|
|
35
|
+
languageText: {
|
|
36
|
+
flex: 1,
|
|
37
|
+
flexShrink: 1,
|
|
38
|
+
},
|
|
39
|
+
nativeName: {
|
|
40
|
+
fontSize: 16,
|
|
41
|
+
fontWeight: '500',
|
|
42
|
+
color: tokens.colors.textPrimary,
|
|
43
|
+
marginBottom: 2,
|
|
44
|
+
},
|
|
45
|
+
languageName: {
|
|
46
|
+
fontSize: 14,
|
|
47
|
+
color: tokens.colors.textSecondary,
|
|
48
|
+
},
|
|
49
|
+
checkIcon: {
|
|
50
|
+
padding: tokens.spacing.md,
|
|
51
|
+
// Replaced by AtomicIcon
|
|
52
|
+
},
|
|
53
|
+
});
|
|
40
54
|
|
|
@@ -17,7 +17,7 @@ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system/atoms
|
|
|
17
17
|
import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
|
|
18
18
|
import type { Language } from '../../infrastructure/storage/types/Language';
|
|
19
19
|
import { ICON_PATHS } from '../../../../utils/iconPaths';
|
|
20
|
-
import {
|
|
20
|
+
import { createLanguageItemStyles } from './LanguageItem.styles';
|
|
21
21
|
|
|
22
22
|
interface LanguageItemProps {
|
|
23
23
|
item: Language;
|
|
@@ -42,29 +42,9 @@ export const LanguageItem: React.FC<LanguageItemProps> = React.memo(({
|
|
|
42
42
|
customStyles,
|
|
43
43
|
}) => {
|
|
44
44
|
const tokens = useAppDesignTokens();
|
|
45
|
+
const styles = createLanguageItemStyles(tokens);
|
|
45
46
|
|
|
46
47
|
const themedStyles = useMemo(() => ({
|
|
47
|
-
languageItem: {
|
|
48
|
-
paddingHorizontal: tokens.spacing.md,
|
|
49
|
-
paddingVertical: tokens.spacing.md,
|
|
50
|
-
borderRadius: tokens.borders.radius.lg,
|
|
51
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
52
|
-
marginBottom: tokens.spacing.md,
|
|
53
|
-
flexDirection: 'row' as const,
|
|
54
|
-
alignItems: 'center' as const,
|
|
55
|
-
justifyContent: 'space-between' as const,
|
|
56
|
-
} as ViewStyle,
|
|
57
|
-
selectedLanguageItem: {
|
|
58
|
-
backgroundColor: tokens.colors.surface,
|
|
59
|
-
borderColor: tokens.colors.primary,
|
|
60
|
-
borderWidth: 1.5,
|
|
61
|
-
} as ViewStyle,
|
|
62
|
-
nativeName: {
|
|
63
|
-
color: tokens.colors.textPrimary,
|
|
64
|
-
} as TextStyle,
|
|
65
|
-
languageName: {
|
|
66
|
-
color: tokens.colors.textSecondary,
|
|
67
|
-
} as TextStyle,
|
|
68
48
|
flagContainer: {
|
|
69
49
|
width: 44,
|
|
70
50
|
height: 44,
|
|
@@ -80,8 +60,8 @@ export const LanguageItem: React.FC<LanguageItemProps> = React.memo(({
|
|
|
80
60
|
<TouchableOpacity
|
|
81
61
|
testID="language-item-test"
|
|
82
62
|
style={[
|
|
83
|
-
|
|
84
|
-
isSelected &&
|
|
63
|
+
styles.languageItem,
|
|
64
|
+
isSelected && styles.selectedLanguageItem,
|
|
85
65
|
customStyles?.languageItem,
|
|
86
66
|
]}
|
|
87
67
|
onPress={() => {
|
|
@@ -110,15 +90,15 @@ export const LanguageItem: React.FC<LanguageItemProps> = React.memo(({
|
|
|
110
90
|
)}
|
|
111
91
|
</View>
|
|
112
92
|
<View style={[styles.languageText, customStyles?.languageText]}>
|
|
113
|
-
<AtomicText
|
|
93
|
+
<AtomicText
|
|
114
94
|
type="bodyLarge"
|
|
115
|
-
style={[
|
|
95
|
+
style={[styles.nativeName, { fontWeight: '600' }, customStyles?.nativeName]}
|
|
116
96
|
>
|
|
117
97
|
{item.nativeName}
|
|
118
98
|
</AtomicText>
|
|
119
|
-
<AtomicText
|
|
120
|
-
type="labelMedium"
|
|
121
|
-
style={[
|
|
99
|
+
<AtomicText
|
|
100
|
+
type="labelMedium"
|
|
101
|
+
style={[styles.languageName, customStyles?.nativeName]}
|
|
122
102
|
>
|
|
123
103
|
{item.name}
|
|
124
104
|
</AtomicText>
|
|
@@ -3,17 +3,28 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Translate Missing Script
|
|
5
5
|
* Automatically translates missing strings using Google Translate
|
|
6
|
+
* Refactored to use @umituz/react-native-google-translate package
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import fs from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import {
|
|
13
|
+
googleTranslateService,
|
|
14
|
+
getTargetLanguage,
|
|
15
|
+
getLanguageDisplayName,
|
|
16
|
+
} from '@umituz/react-native-google-translate/services';
|
|
13
17
|
import { setupLanguages } from './setup-languages.js';
|
|
14
18
|
import { syncTranslations } from './sync-translations.js';
|
|
15
19
|
|
|
16
20
|
async function translateMissing(targetDir, srcDir) {
|
|
21
|
+
// Initialize the translation service
|
|
22
|
+
googleTranslateService.initialize({
|
|
23
|
+
minDelay: 100,
|
|
24
|
+
maxRetries: 3,
|
|
25
|
+
timeout: 10000,
|
|
26
|
+
});
|
|
27
|
+
|
|
17
28
|
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
18
29
|
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
19
30
|
|
|
@@ -41,27 +52,44 @@ async function translateMissing(targetDir, srcDir) {
|
|
|
41
52
|
|
|
42
53
|
for (const file of files) {
|
|
43
54
|
const langCode = file.replace('.ts', '');
|
|
44
|
-
const targetLang = getTargetLanguage(langCode);
|
|
45
|
-
const langName = getLangDisplayName(langCode);
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
// Skip English variants
|
|
57
|
+
const targetLang = getTargetLanguage(langCode);
|
|
58
|
+
if (!targetLang || targetLang === 'en') {
|
|
59
|
+
console.log(`⏭️ Skipping ${langCode} (English variant)`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
48
62
|
|
|
63
|
+
const langName = getLanguageDisplayName(langCode);
|
|
49
64
|
console.log(`🌍 Translating ${langCode} (${langName})...`);
|
|
50
65
|
|
|
51
66
|
const targetPath = path.join(localesDir, file);
|
|
52
67
|
const target = parseTypeScriptFile(targetPath);
|
|
53
68
|
|
|
54
|
-
const stats = {
|
|
55
|
-
|
|
69
|
+
const stats = {
|
|
70
|
+
totalCount: 0,
|
|
71
|
+
successCount: 0,
|
|
72
|
+
failureCount: 0,
|
|
73
|
+
skippedCount: 0,
|
|
74
|
+
translatedKeys: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await googleTranslateService.translateObject(
|
|
78
|
+
enUS,
|
|
79
|
+
target,
|
|
80
|
+
targetLang,
|
|
81
|
+
'',
|
|
82
|
+
stats
|
|
83
|
+
);
|
|
56
84
|
|
|
57
85
|
// Clear progress line
|
|
58
86
|
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
59
87
|
|
|
60
|
-
if (stats.
|
|
88
|
+
if (stats.successCount > 0) {
|
|
61
89
|
const content = generateTypeScriptContent(target, langCode);
|
|
62
90
|
fs.writeFileSync(targetPath, content);
|
|
63
91
|
|
|
64
|
-
console.log(` ✅ Successfully translated ${stats.
|
|
92
|
+
console.log(` ✅ Successfully translated ${stats.successCount} keys:`);
|
|
65
93
|
|
|
66
94
|
// Detailed logging of translated keys
|
|
67
95
|
const displayCount = Math.min(stats.translatedKeys.length, 15);
|
|
@@ -1,116 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Translation Configuration
|
|
3
|
-
*
|
|
3
|
+
* Re-exports from @umituz/react-native-google-translate package
|
|
4
|
+
* This file provides backward compatibility for existing imports
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'en-GB': 'en',
|
|
16
|
-
'es-ES': 'es',
|
|
17
|
-
'es-MX': 'es',
|
|
18
|
-
'fi-FI': 'fi',
|
|
19
|
-
'fr-CA': 'fr',
|
|
20
|
-
'fr-FR': 'fr',
|
|
21
|
-
'hi-IN': 'hi',
|
|
22
|
-
'hr-HR': 'hr',
|
|
23
|
-
'hu-HU': 'hu',
|
|
24
|
-
'id-ID': 'id',
|
|
25
|
-
'it-IT': 'it',
|
|
26
|
-
'ja-JP': 'ja',
|
|
27
|
-
'ko-KR': 'ko',
|
|
28
|
-
'ms-MY': 'ms',
|
|
29
|
-
'nl-NL': 'nl',
|
|
30
|
-
'no-NO': 'no',
|
|
31
|
-
'pl-PL': 'pl',
|
|
32
|
-
'pt-BR': 'pt',
|
|
33
|
-
'pt-PT': 'pt',
|
|
34
|
-
'ro-RO': 'ro',
|
|
35
|
-
'ru-RU': 'ru',
|
|
36
|
-
'sk-SK': 'sk',
|
|
37
|
-
'sl-SI': 'sl',
|
|
38
|
-
'sv-SE': 'sv',
|
|
39
|
-
'th-TH': 'th',
|
|
40
|
-
'tl-PH': 'tl',
|
|
41
|
-
'tr-TR': 'tr',
|
|
42
|
-
'uk-UA': 'uk',
|
|
43
|
-
'vi-VN': 'vi',
|
|
44
|
-
'zh-CN': 'zh-CN',
|
|
45
|
-
'zh-TW': 'zh-TW',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const SKIP_WORDS = new Set([
|
|
49
|
-
'Google',
|
|
50
|
-
'Apple',
|
|
51
|
-
'Facebook',
|
|
52
|
-
'Instagram',
|
|
53
|
-
'Twitter',
|
|
54
|
-
'YouTube',
|
|
55
|
-
'WhatsApp',
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
export const LANGUAGE_NAMES = {
|
|
59
|
-
'ar-SA': 'Arabic (Saudi Arabia)',
|
|
60
|
-
'bg-BG': 'Bulgarian',
|
|
61
|
-
'cs-CZ': 'Czech',
|
|
62
|
-
'da-DK': 'Danish',
|
|
63
|
-
'de-DE': 'German',
|
|
64
|
-
'el-GR': 'Greek',
|
|
65
|
-
'en-AU': 'English (Australia)',
|
|
66
|
-
'en-CA': 'English (Canada)',
|
|
67
|
-
'en-GB': 'English (UK)',
|
|
68
|
-
'en-US': 'English (US)',
|
|
69
|
-
'es-ES': 'Spanish (Spain)',
|
|
70
|
-
'es-MX': 'Spanish (Mexico)',
|
|
71
|
-
'fi-FI': 'Finnish',
|
|
72
|
-
'fr-CA': 'French (Canada)',
|
|
73
|
-
'fr-FR': 'French (France)',
|
|
74
|
-
'hi-IN': 'Hindi',
|
|
75
|
-
'hr-HR': 'Croatian',
|
|
76
|
-
'hu-HU': 'Hungarian',
|
|
77
|
-
'id-ID': 'Indonesian',
|
|
78
|
-
'it-IT': 'Italian',
|
|
79
|
-
'ja-JP': 'Japanese',
|
|
80
|
-
'ko-KR': 'Korean',
|
|
81
|
-
'ms-MY': 'Malay',
|
|
82
|
-
'nl-NL': 'Dutch',
|
|
83
|
-
'no-NO': 'Norwegian',
|
|
84
|
-
'pl-PL': 'Polish',
|
|
85
|
-
'pt-BR': 'Portuguese (Brazil)',
|
|
86
|
-
'pt-PT': 'Portuguese (Portugal)',
|
|
87
|
-
'ro-RO': 'Romanian',
|
|
88
|
-
'ru-RU': 'Russian',
|
|
89
|
-
'sk-SK': 'Slovak',
|
|
90
|
-
'sl-SI': 'Slovenian',
|
|
91
|
-
'sv-SE': 'Swedish',
|
|
92
|
-
'th-TH': 'Thai',
|
|
93
|
-
'tl-PH': 'Tagalog',
|
|
94
|
-
'tr-TR': 'Turkish',
|
|
95
|
-
'uk-UA': 'Ukrainian',
|
|
96
|
-
'vi-VN': 'Vietnamese',
|
|
97
|
-
'zh-CN': 'Chinese (Simplified)',
|
|
98
|
-
'zh-TW': 'Chinese (Traditional)',
|
|
99
|
-
};
|
|
7
|
+
export {
|
|
8
|
+
LANGUAGE_MAP,
|
|
9
|
+
SKIP_WORDS,
|
|
10
|
+
LANGUAGE_NAMES,
|
|
11
|
+
getTargetLanguage,
|
|
12
|
+
isEnglishVariant,
|
|
13
|
+
getLanguageDisplayName,
|
|
14
|
+
shouldSkipWord,
|
|
15
|
+
} from '@umituz/react-native-google-translate/services';
|
|
100
16
|
|
|
17
|
+
// Backward compatibility alias
|
|
101
18
|
export function getLangDisplayName(code) {
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function getTargetLanguage(langCode) {
|
|
106
|
-
return LANGUAGE_MAP[langCode];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function shouldSkipWord(word) {
|
|
110
|
-
return SKIP_WORDS.has(word);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function isEnglishVariant(langCode) {
|
|
114
|
-
const targetLang = LANGUAGE_MAP[langCode];
|
|
115
|
-
return targetLang === 'en';
|
|
19
|
+
return getLanguageDisplayName(code);
|
|
116
20
|
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation Utilities
|
|
3
|
-
* Handles call to translation APIs
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getTargetLanguage, shouldSkipWord } from './translation-config.js';
|
|
7
|
-
|
|
8
|
-
let lastCallTime = 0;
|
|
9
|
-
const MIN_DELAY = 100; // ms
|
|
10
|
-
|
|
11
|
-
async function translateText(text, targetLang) {
|
|
12
|
-
if (!text || typeof text !== 'string') return text;
|
|
13
|
-
if (shouldSkipWord(text)) return text;
|
|
14
|
-
|
|
15
|
-
// Rate limiting
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const waitTime = Math.max(0, MIN_DELAY - (now - lastCallTime));
|
|
18
|
-
if (waitTime > 0) await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
19
|
-
lastCallTime = Date.now();
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const encodedText = encodeURIComponent(text);
|
|
23
|
-
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
|
|
24
|
-
|
|
25
|
-
const response = await fetch(url);
|
|
26
|
-
if (!response.ok) return text;
|
|
27
|
-
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
return data && data[0] && data[0][0] && data[0][0][0] ? data[0][0][0] : text;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
return text;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function needsTranslation(value, enValue) {
|
|
36
|
-
if (typeof enValue !== 'string' || !enValue.trim()) return false;
|
|
37
|
-
if (shouldSkipWord(enValue)) return false;
|
|
38
|
-
|
|
39
|
-
// CRITICAL OPTIMIZATION: If enValue is a technical key (e.g. "scenario.xxx.title"),
|
|
40
|
-
// skip translating it to other languages. We only translate REAL English content.
|
|
41
|
-
const isTechnicalKey = enValue.includes('.') && !enValue.includes(' ');
|
|
42
|
-
if (isTechnicalKey) return false;
|
|
43
|
-
|
|
44
|
-
// If value is missing or same as English, it needs translation
|
|
45
|
-
if (!value || typeof value !== 'string') return true;
|
|
46
|
-
|
|
47
|
-
if (value === enValue) {
|
|
48
|
-
const isSingleWord = !enValue.includes(' ') && enValue.length < 20;
|
|
49
|
-
return !isSingleWord;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Detect outdated template patterns (e.g., {{appName}}, {{variable}})
|
|
53
|
-
if (value && typeof value === 'string') {
|
|
54
|
-
const hasTemplatePattern = value.includes('{{') && value.includes('}}');
|
|
55
|
-
if (hasTemplatePattern && !enValue.includes('{{')) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0, checked: 0, translatedKeys: [] }) {
|
|
64
|
-
const keys = Object.keys(enObj);
|
|
65
|
-
|
|
66
|
-
if (!stats.translatedKeys) stats.translatedKeys = [];
|
|
67
|
-
|
|
68
|
-
for (const key of keys) {
|
|
69
|
-
const enValue = enObj[key];
|
|
70
|
-
const targetValue = targetObj[key];
|
|
71
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
72
|
-
|
|
73
|
-
if (typeof enValue === 'object' && enValue !== null) {
|
|
74
|
-
if (!targetObj[key] || typeof targetObj[key] !== 'object') targetObj[key] = {};
|
|
75
|
-
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
76
|
-
} else if (typeof enValue === 'string') {
|
|
77
|
-
stats.checked++;
|
|
78
|
-
if (needsTranslation(targetValue, enValue)) {
|
|
79
|
-
// Show progress for translations
|
|
80
|
-
process.stdout.write(` \r Progress: ${stats.checked} keys checked, ${stats.count} translated...`);
|
|
81
|
-
|
|
82
|
-
const translated = await translateText(enValue, targetLang);
|
|
83
|
-
if (translated !== enValue) {
|
|
84
|
-
targetObj[key] = translated;
|
|
85
|
-
stats.count++;
|
|
86
|
-
stats.translatedKeys.push({ key: currentPath, from: enValue, to: translated });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|