add en/vi language
This commit is contained in:
@@ -4,14 +4,16 @@ import { HapticTab } from "@/components/haptic-tab";
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { startEvents, stopEvents } from "@/services/device_events";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const segments = useSegments();
|
||||
const segments = useSegments() as string[];
|
||||
const prev = useRef<string | null>(null);
|
||||
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null; // tuỳ cấu trúc của bạn
|
||||
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
|
||||
const { t, locale } = useI18n();
|
||||
useEffect(() => {
|
||||
if (prev.current !== currentSegment) {
|
||||
// console.log("Tab changed ->", { from: prev.current, to: currentSegment });
|
||||
@@ -39,7 +41,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Giám sát",
|
||||
title: t("navigation.home"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="map.fill" color={color} />
|
||||
),
|
||||
@@ -49,7 +51,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="tripInfo"
|
||||
options={{
|
||||
title: "Chuyến Đi",
|
||||
title: t("navigation.trip"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="ferry.fill" color={color} />
|
||||
),
|
||||
@@ -58,7 +60,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="diary"
|
||||
options={{
|
||||
title: "Nhật Ký",
|
||||
title: t("navigation.diary"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="book.closed.fill" color={color} />
|
||||
),
|
||||
@@ -67,7 +69,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="sensor"
|
||||
options={{
|
||||
title: "Cảm biến",
|
||||
title: t("navigation.sensor"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol
|
||||
size={28}
|
||||
@@ -80,7 +82,7 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="setting"
|
||||
options={{
|
||||
title: "Cài đặt",
|
||||
title: t("navigation.setting"),
|
||||
tabBarIcon: ({ color }) => (
|
||||
<IconSymbol size={28} name="gear" color={color} />
|
||||
),
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function HomeScreen() {
|
||||
if (TrackPointsData && TrackPointsData.length > 0) {
|
||||
setTrackPointsData(TrackPointsData);
|
||||
} else {
|
||||
setTrackPointsData(null);
|
||||
setTrackPointsData([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
|
||||
import EnIcon from "@/assets/icons/en_icon.png";
|
||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||
import RotateSwitch from "@/components/rotate-switch";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { api } from "@/config";
|
||||
import { DOMAIN, TOKEN } from "@/constants";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { removeStorageItem } from "@/utils/storage";
|
||||
import { useState } from "react";
|
||||
|
||||
type Todo = {
|
||||
userId: number;
|
||||
id: number;
|
||||
@@ -18,23 +20,40 @@ type Todo = {
|
||||
export default function SettingScreen() {
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Todo | null>(null);
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const [isEnabled, setIsEnabled] = useState(locale === "vi");
|
||||
// Sync isEnabled state khi locale thay đổi
|
||||
useEffect(() => {
|
||||
setIsEnabled(locale === "vi");
|
||||
}, [locale]);
|
||||
|
||||
// useEffect(() => {
|
||||
// getData();
|
||||
// }, []);
|
||||
|
||||
const getData = async () => {
|
||||
try {
|
||||
const response = await api.get("/todos/1");
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
const toggleSwitch = async () => {
|
||||
const newLocale = isEnabled ? "en" : "vi";
|
||||
await setLocale(newLocale);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">Settings</ThemedText>
|
||||
<ThemedText type="title">{t("navigation.setting")}</ThemedText>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText type="default">{t("common.language")}</ThemedText>
|
||||
{/* <Switch
|
||||
trackColor={{ false: "#767577", true: "#81b0ff" }}
|
||||
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
|
||||
ios_backgroundColor="#3e3e3e"
|
||||
onValueChange={toggleSwitch}
|
||||
value={isEnabled}
|
||||
/> */}
|
||||
<RotateSwitch
|
||||
initialValue={isEnabled}
|
||||
onChange={toggleSwitch}
|
||||
size="sm"
|
||||
offImage={EnIcon}
|
||||
onImage={VnIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedView
|
||||
style={styles.button}
|
||||
onTouchEnd={async () => {
|
||||
@@ -43,8 +62,9 @@ export default function SettingScreen() {
|
||||
router.navigate("/auth/login");
|
||||
}}
|
||||
>
|
||||
<ThemedText type="defaultSemiBold">Đăng xuất</ThemedText>
|
||||
<ThemedText type="defaultSemiBold">{t("auth.logout")}</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
{data && (
|
||||
<ThemedView style={{ marginTop: 20 }}>
|
||||
<ThemedText type="default">{data.title}</ThemedText>
|
||||
@@ -62,11 +82,23 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
gap: 16,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
||||
},
|
||||
button: {
|
||||
marginTop: 20,
|
||||
padding: 10,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: "#007AFF",
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toastConfig } from "@/config";
|
||||
import { setRouterInstance } from "@/config/auth";
|
||||
import "@/global.css";
|
||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||
import { I18nProvider } from "@/hooks/use-i18n";
|
||||
import Toast from "react-native-toast-message";
|
||||
import "../global.css";
|
||||
export default function RootLayout() {
|
||||
@@ -23,38 +24,41 @@ export default function RootLayout() {
|
||||
useEffect(() => {
|
||||
setRouterInstance(router);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<GluestackUIProvider>
|
||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||
<Stack
|
||||
screenOptions={{ headerShown: false }}
|
||||
initialRouteName="auth/login"
|
||||
<I18nProvider>
|
||||
<GluestackUIProvider>
|
||||
<ThemeProvider
|
||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="auth/login"
|
||||
options={{
|
||||
title: "Login",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
screenOptions={{ headerShown: false }}
|
||||
initialRouteName="auth/login"
|
||||
>
|
||||
<Stack.Screen
|
||||
name="auth/login"
|
||||
options={{
|
||||
title: "Login",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
title: "Home",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
title: "Home",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options={{ presentation: "formSheet", title: "Modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||
</ThemeProvider>
|
||||
</GluestackUIProvider>
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options={{ presentation: "formSheet", title: "Modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||
</ThemeProvider>
|
||||
</GluestackUIProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import EnIcon from "@/assets/icons/en_icon.png";
|
||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||
import RotateSwitch from "@/components/rotate-switch";
|
||||
import ScanQRCode from "@/components/ScanQRCode";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import SliceSwitch from "@/components/ui/slice-switch";
|
||||
import { DOMAIN, TOKEN } from "@/constants";
|
||||
import { login } from "@/controller/AuthController";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
||||
import {
|
||||
getStorageItem,
|
||||
@@ -25,7 +30,6 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState("");
|
||||
@@ -33,6 +37,8 @@ export default function LoginScreen() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
||||
const { t, setLocale, locale } = useI18n();
|
||||
const [isVNLang, setIsVNLang] = useState(false);
|
||||
|
||||
const checkLogin = useCallback(async () => {
|
||||
const token = await getStorageItem(TOKEN);
|
||||
@@ -47,7 +53,6 @@ export default function LoginScreen() {
|
||||
return;
|
||||
}
|
||||
const parsed = parseJwtToken(token);
|
||||
// console.log("Parse Token: ", parsed);
|
||||
|
||||
const { exp } = parsed;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
@@ -60,6 +65,10 @@ export default function LoginScreen() {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVNLang(locale === "vi");
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
checkLogin();
|
||||
}, [checkLogin]);
|
||||
@@ -131,6 +140,15 @@ export default function LoginScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchLanguage = (isVN: boolean) => {
|
||||
if (isVN) {
|
||||
setLocale("vi");
|
||||
} else {
|
||||
setLocale("en");
|
||||
}
|
||||
setIsVNLang(isVN);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
@@ -147,10 +165,7 @@ export default function LoginScreen() {
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
Hệ thống giám sát tàu cá
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.subtitle}>
|
||||
Đăng nhập để tiếp tục
|
||||
{t("common.app_name")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
@@ -158,10 +173,10 @@ export default function LoginScreen() {
|
||||
<View style={styles.formContainer}>
|
||||
{/* Username Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<ThemedText style={styles.label}>Tài khoản</ThemedText>
|
||||
<ThemedText style={styles.label}>{t("auth.username")}</ThemedText>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Nhập tài khoản"
|
||||
placeholder={t("auth.username_placeholder")}
|
||||
placeholderTextColor="#999"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
@@ -172,11 +187,11 @@ export default function LoginScreen() {
|
||||
|
||||
{/* Password Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<ThemedText style={styles.label}>Mật khẩu</ThemedText>
|
||||
<ThemedText style={styles.label}>{t("auth.password")}</ThemedText>
|
||||
<View className="relative">
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Nhập mật khẩu"
|
||||
placeholder={t("auth.password_placeholder")}
|
||||
placeholderTextColor="#999"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
@@ -228,7 +243,7 @@ export default function LoginScreen() {
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={styles.loginButtonText}>Đăng nhập</Text>
|
||||
<Text style={styles.loginButtonText}>{t("auth.login")}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -255,18 +270,32 @@ export default function LoginScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Footer text */}
|
||||
<View style={styles.footerContainer}>
|
||||
<ThemedText style={styles.footerText}>
|
||||
Chưa có tài khoản?{" "}
|
||||
<Text style={styles.linkText}>Đăng ký ngay</Text>
|
||||
</ThemedText>
|
||||
{/* Language Switcher */}
|
||||
<View style={styles.languageSwitcherContainer}>
|
||||
<RotateSwitch
|
||||
initialValue={isVNLang}
|
||||
onChange={handleSwitchLanguage}
|
||||
size="sm"
|
||||
offImage={EnIcon}
|
||||
onImage={VnIcon}
|
||||
/>
|
||||
<SliceSwitch
|
||||
size="sm"
|
||||
leftIcon="moon"
|
||||
leftIconColor="white"
|
||||
rightIcon="sunny"
|
||||
rightIconColor="orange"
|
||||
activeBackgroundColor="black"
|
||||
inactiveBackgroundColor="white"
|
||||
inactiveOverlayColor="black"
|
||||
activeOverlayColor="white"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Copyright */}
|
||||
<View style={styles.copyrightContainer}>
|
||||
<ThemedText style={styles.copyrightText}>
|
||||
© {new Date().getFullYear()} - Sản phẩm của Mobifone
|
||||
© {new Date().getFullYear()} - {t("common.footer_text")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
@@ -364,4 +393,46 @@ const styles = StyleSheet.create({
|
||||
opacity: 0.6,
|
||||
textAlign: "center",
|
||||
},
|
||||
languageSwitcherContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
gap: 20,
|
||||
},
|
||||
languageSwitcherLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
opacity: 0.8,
|
||||
},
|
||||
languageButtonsContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
},
|
||||
languageButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
languageButtonActive: {
|
||||
backgroundColor: "#007AFF",
|
||||
borderColor: "#007AFF",
|
||||
},
|
||||
languageButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
color: "#666",
|
||||
},
|
||||
languageButtonTextActive: {
|
||||
color: "#fff",
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user