exceptionz-react-native-lingua 1.0.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/dist/LinguaContext.d.ts +18 -0
- package/dist/LinguaContext.d.ts.map +1 -0
- package/dist/LinguaContext.js +102 -0
- package/dist/LinguaContext.js.map +1 -0
- package/dist/components/LanguageModal.d.ts +3 -0
- package/dist/components/LanguageModal.d.ts.map +1 -0
- package/dist/components/LanguageModal.js +155 -0
- package/dist/components/LanguageModal.js.map +1 -0
- package/dist/components/LanguageSwitcher.d.ts +3 -0
- package/dist/components/LanguageSwitcher.d.ts.map +1 -0
- package/dist/components/LanguageSwitcher.js +162 -0
- package/dist/components/LanguageSwitcher.js.map +1 -0
- package/dist/components/T.d.ts +8 -0
- package/dist/components/T.d.ts.map +1 -0
- package/dist/components/T.js +27 -0
- package/dist/components/T.js.map +1 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +25 -0
- package/dist/constants.js.map +1 -0
- package/dist/hooks/useTranslate.d.ts +2 -0
- package/dist/hooks/useTranslate.d.ts.map +1 -0
- package/dist/hooks/useTranslate.js +35 -0
- package/dist/hooks/useTranslate.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/translator.d.ts +2 -0
- package/dist/utils/translator.d.ts.map +1 -0
- package/dist/utils/translator.js +51 -0
- package/dist/utils/translator.js.map +1 -0
- package/package.json +39 -0
- package/react-native-lingua-1.0.0.tgz +0 -0
- package/src/LinguaContext.tsx +72 -0
- package/src/components/LanguageModal.tsx +141 -0
- package/src/components/LanguageSwitcher.tsx +142 -0
- package/src/components/T.tsx +12 -0
- package/src/constants.ts +21 -0
- package/src/hooks/useTranslate.ts +36 -0
- package/src/index.ts +6 -0
- package/src/utils/translator.ts +37 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +10 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_LANGUAGES = exports.useTranslate = exports.T = exports.LanguageSwitcher = exports.LanguageModal = exports.useI18n = exports.LinguaProvider = void 0;
|
|
4
|
+
var LinguaContext_1 = require("./LinguaContext");
|
|
5
|
+
Object.defineProperty(exports, "LinguaProvider", { enumerable: true, get: function () { return LinguaContext_1.LinguaProvider; } });
|
|
6
|
+
Object.defineProperty(exports, "useI18n", { enumerable: true, get: function () { return LinguaContext_1.useI18n; } });
|
|
7
|
+
var LanguageModal_1 = require("./components/LanguageModal");
|
|
8
|
+
Object.defineProperty(exports, "LanguageModal", { enumerable: true, get: function () { return LanguageModal_1.LanguageModal; } });
|
|
9
|
+
var LanguageSwitcher_1 = require("./components/LanguageSwitcher");
|
|
10
|
+
Object.defineProperty(exports, "LanguageSwitcher", { enumerable: true, get: function () { return LanguageSwitcher_1.LanguageSwitcher; } });
|
|
11
|
+
var T_1 = require("./components/T");
|
|
12
|
+
Object.defineProperty(exports, "T", { enumerable: true, get: function () { return T_1.T; } });
|
|
13
|
+
var useTranslate_1 = require("./hooks/useTranslate");
|
|
14
|
+
Object.defineProperty(exports, "useTranslate", { enumerable: true, get: function () { return useTranslate_1.useTranslate; } });
|
|
15
|
+
var constants_1 = require("./constants");
|
|
16
|
+
Object.defineProperty(exports, "SUPPORTED_LANGUAGES", { enumerable: true, get: function () { return constants_1.SUPPORTED_LANGUAGES; } });
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAA0D;AAAjD,+GAAA,cAAc,OAAA;AAAE,wGAAA,OAAO,OAAA;AAChC,4DAA2D;AAAlD,8GAAA,aAAa,OAAA;AACtB,kEAAiE;AAAxD,oHAAA,gBAAgB,OAAA;AACzB,oCAAmC;AAA1B,sFAAA,CAAC,OAAA;AACV,qDAAoD;AAA3C,4GAAA,YAAY,OAAA;AACrB,yCAAkD;AAAzC,gHAAA,mBAAmB,OAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../src/utils/translator.ts"],"names":[],"mappings":"AAAA,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCzF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.translateText = translateText;
|
|
13
|
+
function translateText(text, targetLanguage) {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
if (!text || text.trim() === '')
|
|
16
|
+
return text;
|
|
17
|
+
if (!targetLanguage || targetLanguage === 'en')
|
|
18
|
+
return text;
|
|
19
|
+
try {
|
|
20
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLanguage}&dt=t&q=${encodeURIComponent(text)}`;
|
|
21
|
+
const response = yield fetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
console.warn(`Translation API returned status: ${response.status}`);
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
const contentType = response.headers.get('content-type');
|
|
27
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
28
|
+
const textResponse = yield response.text();
|
|
29
|
+
console.warn('Translation API returned non-JSON response:', textResponse.slice(0, 100));
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = yield response.json();
|
|
35
|
+
}
|
|
36
|
+
catch (parseError) {
|
|
37
|
+
console.error('Failed to parse translation JSON:', parseError);
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
if (data && data[0] && Array.isArray(data[0])) {
|
|
41
|
+
return data[0].map((item) => item[0]).join('');
|
|
42
|
+
}
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Translation network/unexpected error:', error);
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=translator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.js","sourceRoot":"","sources":["../../src/utils/translator.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,sCAoCC;AApCD,SAAsB,aAAa,CAAC,IAAY,EAAE,cAAsB;;QACtE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC7C,IAAI,CAAC,cAAc,IAAI,cAAc,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAE5D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,6EAA6E,cAAc,WAAW,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7I,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;YAElC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,OAAO,CAAC,IAAI,CAAC,oCAAoC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;gBACpE,OAAO,IAAI,CAAC;YACd,CAAC;YAED,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACzD,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC9D,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC3C,OAAO,CAAC,IAAI,CAAC,6CAA6C,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBACxF,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IAAI,IAAI,CAAC;YACT,IAAI,CAAC;gBACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC/B,CAAC;YAAC,OAAO,UAAU,EAAE,CAAC;gBACpB,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,UAAU,CAAC,CAAC;gBAC/D,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtD,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAC;YAC9D,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exceptionz-react-native-lingua",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A React Native translation library that auto-translates text nodes and displays a language selector modal.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react-native",
|
|
21
|
+
"translate",
|
|
22
|
+
"i18n",
|
|
23
|
+
"google-translate"
|
|
24
|
+
],
|
|
25
|
+
"author": "Antigravity",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=16.8.0",
|
|
29
|
+
"react-native": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.2.0",
|
|
33
|
+
"react": "^18.2.0",
|
|
34
|
+
"react-native": "*",
|
|
35
|
+
"tsup": "^8.0.2",
|
|
36
|
+
"typescript": "^5.4.3",
|
|
37
|
+
"@react-native-async-storage/async-storage": "^1.23.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
+
import { SUPPORTED_LANGUAGES } from './constants';
|
|
4
|
+
|
|
5
|
+
interface LinguaContextProps {
|
|
6
|
+
currentLanguage: string;
|
|
7
|
+
setLanguage: (lang: string) => Promise<void>;
|
|
8
|
+
availableLanguages: string[];
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LinguaContext = createContext<LinguaContextProps | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
export const useI18n = () => {
|
|
15
|
+
const context = useContext(LinguaContext);
|
|
16
|
+
if (!context) throw new Error('useI18n must be used within a LinguaProvider');
|
|
17
|
+
return context;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface LinguaProviderProps {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
defaultLang?: string;
|
|
23
|
+
languages?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const translationCache = new Map<string, string>();
|
|
27
|
+
export const getCacheKey = (text: string, lang: string) => `${lang}:${text}`;
|
|
28
|
+
|
|
29
|
+
export const LinguaProvider: React.FC<LinguaProviderProps> = ({
|
|
30
|
+
children,
|
|
31
|
+
defaultLang = 'en',
|
|
32
|
+
languages = SUPPORTED_LANGUAGES.map(l => l.code)
|
|
33
|
+
}) => {
|
|
34
|
+
const [currentLanguage, setCurrentLanguage] = useState(defaultLang);
|
|
35
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const loadLanguage = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const saved = await AsyncStorage.getItem('app-language-preference');
|
|
41
|
+
if (saved) {
|
|
42
|
+
setCurrentLanguage(saved);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('Failed to load language preference', e);
|
|
46
|
+
} finally {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
loadLanguage();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleSetLanguage = async (lang: string) => {
|
|
54
|
+
try {
|
|
55
|
+
setCurrentLanguage(lang);
|
|
56
|
+
await AsyncStorage.setItem('app-language-preference', lang);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('Failed to save language preference', e);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<LinguaContext.Provider value={{
|
|
64
|
+
currentLanguage,
|
|
65
|
+
setLanguage: handleSetLanguage,
|
|
66
|
+
availableLanguages: languages,
|
|
67
|
+
isLoading
|
|
68
|
+
}}>
|
|
69
|
+
{children}
|
|
70
|
+
</LinguaContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
FlatList,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
SafeAreaView
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useI18n } from '../LinguaContext';
|
|
12
|
+
import { SUPPORTED_LANGUAGES } from '../constants';
|
|
13
|
+
|
|
14
|
+
export const LanguageModal: React.FC = () => {
|
|
15
|
+
const { setLanguage, currentLanguage, isLoading } = useI18n();
|
|
16
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isLoading && !currentLanguage) {
|
|
20
|
+
setModalVisible(true);
|
|
21
|
+
}
|
|
22
|
+
}, [isLoading, currentLanguage]);
|
|
23
|
+
|
|
24
|
+
const handleSelectLanguage = async (code: string) => {
|
|
25
|
+
await setLanguage(code);
|
|
26
|
+
setModalVisible(false);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Modal
|
|
31
|
+
animationType="slide"
|
|
32
|
+
transparent={true}
|
|
33
|
+
visible={modalVisible}
|
|
34
|
+
onRequestClose={() => setModalVisible(false)}
|
|
35
|
+
>
|
|
36
|
+
<View style={styles.centeredView}>
|
|
37
|
+
<View style={styles.modalView}>
|
|
38
|
+
<Text style={styles.modalTitle}>Select Language</Text>
|
|
39
|
+
<Text style={styles.modalSubtitle}>Choose your preferred language.</Text>
|
|
40
|
+
|
|
41
|
+
<FlatList
|
|
42
|
+
data={SUPPORTED_LANGUAGES}
|
|
43
|
+
keyExtractor={(item) => item.code}
|
|
44
|
+
renderItem={({ item }) => (
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
style={[
|
|
47
|
+
styles.languageItem,
|
|
48
|
+
currentLanguage === item.code && styles.selectedItem
|
|
49
|
+
]}
|
|
50
|
+
onPress={() => handleSelectLanguage(item.code)}
|
|
51
|
+
>
|
|
52
|
+
<Text style={styles.icon}>{item.icon}</Text>
|
|
53
|
+
<Text style={[
|
|
54
|
+
styles.label,
|
|
55
|
+
currentLanguage === item.code && styles.selectedLabel
|
|
56
|
+
]}>
|
|
57
|
+
{item.label}
|
|
58
|
+
</Text>
|
|
59
|
+
</TouchableOpacity>
|
|
60
|
+
)}
|
|
61
|
+
style={styles.list}
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
<TouchableOpacity
|
|
65
|
+
style={styles.closeButton}
|
|
66
|
+
onPress={() => setModalVisible(false)}
|
|
67
|
+
>
|
|
68
|
+
<Text style={styles.closeButtonText}>Close</Text>
|
|
69
|
+
</TouchableOpacity>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
</Modal>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
centeredView: {
|
|
78
|
+
flex: 1,
|
|
79
|
+
justifyContent: 'center',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
82
|
+
},
|
|
83
|
+
modalView: {
|
|
84
|
+
width: '85%',
|
|
85
|
+
backgroundColor: '#1e293b',
|
|
86
|
+
borderRadius: 20,
|
|
87
|
+
padding: 20,
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
shadowColor: '#000',
|
|
90
|
+
shadowOffset: { width: 0, height: 2 },
|
|
91
|
+
shadowOpacity: 0.25,
|
|
92
|
+
shadowRadius: 4,
|
|
93
|
+
elevation: 5,
|
|
94
|
+
maxHeight: '80%',
|
|
95
|
+
},
|
|
96
|
+
modalTitle: {
|
|
97
|
+
fontSize: 20,
|
|
98
|
+
fontWeight: 'bold',
|
|
99
|
+
color: '#f8fafc',
|
|
100
|
+
marginBottom: 8,
|
|
101
|
+
},
|
|
102
|
+
modalSubtitle: {
|
|
103
|
+
fontSize: 14,
|
|
104
|
+
color: '#94a3b8',
|
|
105
|
+
marginBottom: 20,
|
|
106
|
+
},
|
|
107
|
+
list: {
|
|
108
|
+
width: '100%',
|
|
109
|
+
},
|
|
110
|
+
languageItem: {
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
padding: 15,
|
|
114
|
+
borderRadius: 12,
|
|
115
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
116
|
+
marginBottom: 8,
|
|
117
|
+
gap: 12,
|
|
118
|
+
},
|
|
119
|
+
selectedItem: {
|
|
120
|
+
backgroundColor: '#3b82f6',
|
|
121
|
+
},
|
|
122
|
+
icon: {
|
|
123
|
+
fontSize: 18,
|
|
124
|
+
},
|
|
125
|
+
label: {
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
color: '#cbd5e1',
|
|
128
|
+
fontWeight: '500',
|
|
129
|
+
},
|
|
130
|
+
selectedLabel: {
|
|
131
|
+
color: '#fff',
|
|
132
|
+
},
|
|
133
|
+
closeButton: {
|
|
134
|
+
marginTop: 20,
|
|
135
|
+
padding: 10,
|
|
136
|
+
},
|
|
137
|
+
closeButtonText: {
|
|
138
|
+
color: '#3b82f6',
|
|
139
|
+
fontWeight: '600',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ScrollView,
|
|
8
|
+
Modal,
|
|
9
|
+
TouchableWithoutFeedback
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useI18n } from '../LinguaContext';
|
|
12
|
+
import { SUPPORTED_LANGUAGES } from '../constants';
|
|
13
|
+
|
|
14
|
+
export const LanguageSwitcher: React.FC = () => {
|
|
15
|
+
const { currentLanguage, setLanguage, availableLanguages } = useI18n();
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
const langMap = SUPPORTED_LANGUAGES.reduce((acc, lang) => {
|
|
19
|
+
acc[lang.code] = { label: lang.label, icon: lang.icon };
|
|
20
|
+
return acc;
|
|
21
|
+
}, {} as Record<string, { label: string, icon: string }>);
|
|
22
|
+
|
|
23
|
+
const handleSelect = async (lang: string) => {
|
|
24
|
+
await setLanguage(lang);
|
|
25
|
+
setIsOpen(false);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={styles.container}>
|
|
30
|
+
{isOpen && (
|
|
31
|
+
<Modal
|
|
32
|
+
transparent={true}
|
|
33
|
+
visible={isOpen}
|
|
34
|
+
animationType="fade"
|
|
35
|
+
onRequestClose={() => setIsOpen(false)}
|
|
36
|
+
>
|
|
37
|
+
<TouchableWithoutFeedback onPress={() => setIsOpen(false)}>
|
|
38
|
+
<View style={styles.overlay}>
|
|
39
|
+
<View style={styles.dropdown}>
|
|
40
|
+
<ScrollView bounces={false} style={{ maxHeight: 350 }}>
|
|
41
|
+
{availableLanguages.map((lang) => (
|
|
42
|
+
<TouchableOpacity
|
|
43
|
+
key={lang}
|
|
44
|
+
onPress={() => handleSelect(lang)}
|
|
45
|
+
style={[
|
|
46
|
+
styles.menuItem,
|
|
47
|
+
currentLanguage === lang && styles.selectedMenuItem
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
<Text style={styles.icon}>{langMap[lang]?.icon || '🌐'}</Text>
|
|
51
|
+
<Text style={[
|
|
52
|
+
styles.label,
|
|
53
|
+
currentLanguage === lang && styles.selectedLabel
|
|
54
|
+
]}>
|
|
55
|
+
{langMap[lang]?.label || lang}
|
|
56
|
+
</Text>
|
|
57
|
+
</TouchableOpacity>
|
|
58
|
+
))}
|
|
59
|
+
</ScrollView>
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
</TouchableWithoutFeedback>
|
|
63
|
+
</Modal>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<TouchableOpacity
|
|
67
|
+
style={styles.floatingButton}
|
|
68
|
+
onPress={() => setIsOpen(true)}
|
|
69
|
+
>
|
|
70
|
+
<Text style={styles.buttonIcon}>
|
|
71
|
+
{langMap[currentLanguage]?.icon || '🌐'}
|
|
72
|
+
</Text>
|
|
73
|
+
</TouchableOpacity>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const styles = StyleSheet.create({
|
|
79
|
+
container: {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
bottom: 24,
|
|
82
|
+
right: 24,
|
|
83
|
+
zIndex: 99999,
|
|
84
|
+
},
|
|
85
|
+
floatingButton: {
|
|
86
|
+
width: 48,
|
|
87
|
+
height: 48,
|
|
88
|
+
borderRadius: 24,
|
|
89
|
+
backgroundColor: '#3b82f6',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
shadowColor: '#3b82f6',
|
|
93
|
+
shadowOffset: { width: 0, height: 4 },
|
|
94
|
+
shadowOpacity: 0.3,
|
|
95
|
+
shadowRadius: 12,
|
|
96
|
+
elevation: 8,
|
|
97
|
+
},
|
|
98
|
+
buttonIcon: {
|
|
99
|
+
fontSize: 20,
|
|
100
|
+
color: '#fff',
|
|
101
|
+
},
|
|
102
|
+
overlay: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
backgroundColor: 'transparent',
|
|
105
|
+
justifyContent: 'flex-end',
|
|
106
|
+
alignItems: 'flex-end',
|
|
107
|
+
paddingBottom: 80,
|
|
108
|
+
paddingRight: 24,
|
|
109
|
+
},
|
|
110
|
+
dropdown: {
|
|
111
|
+
backgroundColor: '#1e293b',
|
|
112
|
+
borderRadius: 12,
|
|
113
|
+
padding: 6,
|
|
114
|
+
width: 180,
|
|
115
|
+
shadowColor: '#000',
|
|
116
|
+
shadowOffset: { width: 0, height: 10 },
|
|
117
|
+
shadowOpacity: 0.3,
|
|
118
|
+
shadowRadius: 20,
|
|
119
|
+
elevation: 10,
|
|
120
|
+
},
|
|
121
|
+
menuItem: {
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
padding: 10,
|
|
125
|
+
borderRadius: 8,
|
|
126
|
+
gap: 10,
|
|
127
|
+
},
|
|
128
|
+
selectedMenuItem: {
|
|
129
|
+
backgroundColor: '#3b82f6',
|
|
130
|
+
},
|
|
131
|
+
icon: {
|
|
132
|
+
fontSize: 16,
|
|
133
|
+
},
|
|
134
|
+
label: {
|
|
135
|
+
fontSize: 13,
|
|
136
|
+
fontWeight: '500',
|
|
137
|
+
color: '#cbd5e1',
|
|
138
|
+
},
|
|
139
|
+
selectedLabel: {
|
|
140
|
+
color: '#fff',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, TextProps } from 'react-native';
|
|
3
|
+
import { useTranslate } from '../hooks/useTranslate';
|
|
4
|
+
|
|
5
|
+
interface TProps extends TextProps {
|
|
6
|
+
children: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const T: React.FC<TProps> = ({ children, ...props }) => {
|
|
10
|
+
const translatedText = useTranslate(children);
|
|
11
|
+
return <Text {...props}>{translatedText}</Text>;
|
|
12
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SUPPORTED_LANGUAGES = [
|
|
2
|
+
{ code: 'en', label: 'English', icon: '🇺🇸' },
|
|
3
|
+
{ code: 'hi', label: 'हिन्दी', icon: '🇮🇳' },
|
|
4
|
+
{ code: 'bn', label: 'বাংলা', icon: '🇮🇳' },
|
|
5
|
+
{ code: 'gu', label: 'ગુજરાતી', icon: '🇮🇳' },
|
|
6
|
+
{ code: 'kn', label: 'ಕನ್ನಡ', icon: '🇮🇳' },
|
|
7
|
+
{ code: 'ks', label: 'کٲشُر', icon: '🇮🇳' },
|
|
8
|
+
{ code: 'kok', label: 'कोंकणी', icon: '🇮🇳' },
|
|
9
|
+
{ code: 'ml', label: 'മലയാളം', icon: '🇮🇳' },
|
|
10
|
+
{ code: 'mr', label: 'मराठी', icon: '🇮🇳' },
|
|
11
|
+
{ code: 'ne', label: 'नेपाली', icon: '🇮🇳' },
|
|
12
|
+
{ code: 'or', label: 'ଓଡ଼ିଆ', icon: '🇮🇳' },
|
|
13
|
+
{ code: 'pa', label: 'ਪੰਜਾਬੀ', icon: '🇮🇳' },
|
|
14
|
+
{ code: 'sa', label: 'संस्कृतम्', icon: '🇮🇳' },
|
|
15
|
+
{ code: 'sat', label: 'ᱥᱟᱱᱛᱟᱲᱤ', icon: '🇮🇳' },
|
|
16
|
+
{ code: 'sd', label: 'سنڌي', icon: '🇮🇳' },
|
|
17
|
+
{ code: 'ta', label: 'தமிழ்', icon: '🇮🇳' },
|
|
18
|
+
{ code: 'te', label: 'తెలుగు', icon: '🇮🇳' },
|
|
19
|
+
{ code: 'ur', label: 'اردو', icon: '🇮🇳' },
|
|
20
|
+
{ code: 'fr', label: 'Français', icon: '🇫🇷' },
|
|
21
|
+
];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useI18n, translationCache, getCacheKey } from '../LinguaContext';
|
|
3
|
+
import { translateText } from '../utils/translator';
|
|
4
|
+
|
|
5
|
+
export function useTranslate(text: string): string {
|
|
6
|
+
const { currentLanguage } = useI18n();
|
|
7
|
+
const [translated, setTranslated] = useState<string>(text);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!text) return;
|
|
11
|
+
if (currentLanguage === 'en') {
|
|
12
|
+
setTranslated(text);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const key = getCacheKey(text, currentLanguage);
|
|
17
|
+
if (translationCache.has(key)) {
|
|
18
|
+
setTranslated(translationCache.get(key)!);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let isMounted = true;
|
|
23
|
+
translateText(text, currentLanguage).then((res) => {
|
|
24
|
+
if (isMounted) {
|
|
25
|
+
translationCache.set(key, res);
|
|
26
|
+
setTranslated(res);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
isMounted = false;
|
|
32
|
+
};
|
|
33
|
+
}, [text, currentLanguage]);
|
|
34
|
+
|
|
35
|
+
return translated;
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { LinguaProvider, useI18n } from './LinguaContext';
|
|
2
|
+
export { LanguageModal } from './components/LanguageModal';
|
|
3
|
+
export { LanguageSwitcher } from './components/LanguageSwitcher';
|
|
4
|
+
export { T } from './components/T';
|
|
5
|
+
export { useTranslate } from './hooks/useTranslate';
|
|
6
|
+
export { SUPPORTED_LANGUAGES } from './constants';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export async function translateText(text: string, targetLanguage: string): Promise<string> {
|
|
2
|
+
if (!text || text.trim() === '') return text;
|
|
3
|
+
if (!targetLanguage || targetLanguage === 'en') return text;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLanguage}&dt=t&q=${encodeURIComponent(text)}`;
|
|
7
|
+
const response = await fetch(url);
|
|
8
|
+
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
console.warn(`Translation API returned status: ${response.status}`);
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const contentType = response.headers.get('content-type');
|
|
15
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
16
|
+
const textResponse = await response.text();
|
|
17
|
+
console.warn('Translation API returned non-JSON response:', textResponse.slice(0, 100));
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = await response.json();
|
|
24
|
+
} catch (parseError) {
|
|
25
|
+
console.error('Failed to parse translation JSON:', parseError);
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (data && data[0] && Array.isArray(data[0])) {
|
|
30
|
+
return data[0].map((item: any) => item[0]).join('');
|
|
31
|
+
}
|
|
32
|
+
return text;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Translation network/unexpected error:', error);
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": ["ES6"],
|
|
7
|
+
"jsx": "react-native",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": {
|
|
18
|
+
"*": ["src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"]
|
|
22
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['cjs', 'esm'],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: false, // Fix dynamic require of "fs" error
|
|
8
|
+
external: ['react', 'react-native', '@react-native-async-storage/async-storage'],
|
|
9
|
+
minify: true,
|
|
10
|
+
});
|