add en/vi language

This commit is contained in:
Tran Anh Tuan
2025-11-15 16:58:07 +07:00
parent 1a534eccb0
commit e725819c01
31 changed files with 1843 additions and 232 deletions

View File

@@ -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} />
),

View File

@@ -91,7 +91,7 @@ export default function HomeScreen() {
if (TrackPointsData && TrackPointsData.length > 0) {
setTrackPointsData(TrackPointsData);
} else {
setTrackPointsData(null);
setTrackPointsData([]);
}
};

View File

@@ -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,
},
});

View File

@@ -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>
);
}

View File

@@ -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
</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 tài khoản?{" "}
<Text style={styles.linkText}>Đăng 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",
},
});