From d27743017cd09b5281f7576f16b334b69a1f4173 Mon Sep 17 00:00:00 2001 From: vutukuri15 Date: Mon, 3 Mar 2025 16:35:46 -0800 Subject: [PATCH 1/2] Implemented Language Selector --- app/_layout.tsx | 32 +++++--- app/index.tsx | 82 +++++++++++++++---- components/BuyInSelector.tsx | 16 ++-- components/ChipDetection.tsx | 56 +++++++------ components/ChipDistributionSummary.tsx | 9 +- components/ChipsSelector.tsx | 10 ++- components/CurrencySelector.tsx | 9 +- components/__tests__/BuyInSelector.test.tsx | 26 +++--- components/__tests__/ChipDetection.test.tsx | 19 +++-- .../ChipDistributionSummary.test.tsx | 7 +- components/__tests__/ChipsSelector.test.tsx | 4 +- i18n/en.json | 50 +++++++++++ i18n/es.json | 51 ++++++++++++ i18n/i18n.ts | 22 +++++ 14 files changed, 299 insertions(+), 94 deletions(-) create mode 100644 i18n/en.json create mode 100644 i18n/es.json create mode 100644 i18n/i18n.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index d73d8d3..6ed6aef 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,11 +1,15 @@ +import i18n from "@/i18n/i18n"; import AppContext, { IAppContext } from "@/util/context"; import { MaterialIcons } from "@expo/vector-icons"; import { Stack } from "expo-router"; import React, { useMemo, useState } from "react"; +import { I18nextProvider, useTranslation } from "react-i18next"; const RootLayout: React.FC = () => { const [showSettings, setShowSettings] = useState(false); + const { t } = useTranslation(); + const ctx = useMemo( () => ({ showSettings, @@ -15,19 +19,21 @@ const RootLayout: React.FC = () => { return ( - ( - setShowSettings(!showSettings)} - size={30} - /> - ), - }} - /> + + ( + setShowSettings(!showSettings)} + size={30} + /> + ), + }} + /> + ); }; diff --git a/app/index.tsx b/app/index.tsx index 6437c19..b88823e 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -15,6 +15,8 @@ import { import styles from "@/styles/styles"; import Section from "@/containers/Section"; import AppContext from "@/util/context"; +import { Picker } from "@react-native-picker/picker"; +import i18n from "@/i18n/i18n"; const IndexScreen: React.FC = () => { const [playerCount, setPlayerCount] = useState(2); @@ -22,6 +24,7 @@ const IndexScreen: React.FC = () => { const [numberOfChips, setNumberOfChips] = useState(5); const [totalChipsCount, setTotalChipsCount] = useState([]); const [selectedCurrency, setSelectedCurrency] = useState("$"); + const [selectedLanguage, setSelectedLanguage] = useState("en"); const context = useContext(AppContext); const isSettingsVisible = useMemo(() => context.showSettings, [context]); @@ -49,7 +52,7 @@ const IndexScreen: React.FC = () => { const handleSave = async (slot: "SLOT1" | "SLOT2") => { if (buyInAmount === null) { - Alert.alert("Error", "Please select a valid buy-in amount"); + Alert.alert(i18n.t("error"), i18n.t("please_select_valid_buyin")); return; } const state = { @@ -63,10 +66,10 @@ const IndexScreen: React.FC = () => { await saveState(slot, state); await savePersistentState(state); console.log(`Game state saved to ${slot}:`, state); - Alert.alert("Success", `State saved to ${slot}`); + Alert.alert(i18n.t("success"), i18n.t("state_saved", { slot })); // Fixed interpolation } catch (error) { console.error("Error saving state:", error); - Alert.alert("Error", "Failed to save state."); + Alert.alert(i18n.t("error"), i18n.t("failed_to_save_state")); } }; @@ -81,23 +84,49 @@ const IndexScreen: React.FC = () => { setSelectedCurrency(loadedState.selectedCurrency || "$"); await savePersistentState(loadedState); console.log(`Game state loaded from ${slot}:`, loadedState); - Alert.alert("Success", `State loaded from ${slot}`); + Alert.alert(i18n.t("success"), i18n.t("state_loaded_from", { slot })); // Fixed interpolation } else { - Alert.alert("Info", "No saved state found."); + Alert.alert(i18n.t("info"), i18n.t("no_saved_state_found")); } } catch (error) { console.error("Error loading state:", error); - Alert.alert("Error", "Failed to load state."); + Alert.alert(i18n.t("error"), i18n.t("failed_to_load_state")); } }; + const handleLanguageChange = (language: string) => { + setSelectedLanguage(language); + i18n.changeLanguage(language); + }; + return ( {isSettingsVisible && ( -
+
+ + + + +
+ )} + + {isSettingsVisible && ( +
{ )}
@@ -116,14 +145,20 @@ const IndexScreen: React.FC = () => { />
-
+
-
+
{ const chipCountArray = Object.values(chipData); @@ -132,7 +167,10 @@ const IndexScreen: React.FC = () => { />
-
+
{
{ />
-
+
<>
diff --git a/components/BuyInSelector.tsx b/components/BuyInSelector.tsx index dacda85..d9356a1 100644 --- a/components/BuyInSelector.tsx +++ b/components/BuyInSelector.tsx @@ -2,10 +2,11 @@ import React, { useState } from "react"; import { View, Text, TextInput } from "react-native"; import styles, { COLORS } from "@/styles/styles"; import Button from "@/containers/Button"; +import i18n from "@/i18n/i18n"; interface BuyInSelectorProps { setBuyInAmount: React.Dispatch>; - selectedCurrency: string; // Accept selectedCurrency as a prop + selectedCurrency: string; } const defaultBuyInOptions = [10, 25, 50]; @@ -45,22 +46,25 @@ const BuyInSelector: React.FC = ({ color={buyInAmount === amount ? COLORS.PRIMARY : COLORS.SECONDARY} onPress={() => handleBuyInSelection(amount)} title={`${selectedCurrency} ${amount}`} - > + /> ))} - Or enter a custom amount: + {i18n.t("custom_buy_in")} + - Selected Buy-in:{" "} - {buyInAmount !== null ? `${selectedCurrency} ${buyInAmount}` : "None"} + {`${i18n.t("selected_buy_in")}: `} + {buyInAmount !== null + ? `${selectedCurrency} ${buyInAmount}` + : i18n.t("none")} ); diff --git a/components/ChipDetection.tsx b/components/ChipDetection.tsx index 53e6748..bcbe689 100644 --- a/components/ChipDetection.tsx +++ b/components/ChipDetection.tsx @@ -1,18 +1,20 @@ import React, { useState } from "react"; import { Image, ActivityIndicator, Text, View } from "react-native"; import Button from "@/containers/Button"; - import * as ImagePicker from "expo-image-picker"; +import i18n from "@/i18n/i18n"; const ChipDetection = ({ updateChipCount, }: { - updateChipCount: () => void; + updateChipCount: (chipData: Record) => void; }) => { - const [imageUri, setImageUri] = useState(null); + const [imageUri, setImageUri] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [lastDetectedChips, setLastDetectedChips] = useState({}); + const [error, setError] = useState(null); + const [lastDetectedChips, setLastDetectedChips] = useState< + Record + >({}); const requestCameraPermissions = async () => { const cameraPermission = await ImagePicker.requestCameraPermissionsAsync(); @@ -26,16 +28,16 @@ const ChipDetection = ({ quality: 1, }); - if (!result.canceled) { + if (!result.canceled && result.assets && result.assets.length > 0) { setImageUri(result.assets[0].uri); - await processImage(result.assets[0].base64); + await processImage(result.assets[0].base64 as string); } }; const takePhoto = async () => { const hasPermission = await requestCameraPermissions(); if (!hasPermission) { - setError("Camera permission is required to take a photo."); + setError(i18n.t("camera_permission_required")); return; } @@ -44,25 +46,25 @@ const ChipDetection = ({ quality: 1, }); - if (!result.canceled) { + if (!result.canceled && result.assets && result.assets.length > 0) { setImageUri(result.assets[0].uri); - await processImage(result.assets[0].base64); + await processImage(result.assets[0].base64 as string); } }; - const processImage = async (base64Image) => { + const processImage = async (base64Image: string) => { setLoading(true); setError(null); try { - const response = await fetch(process.env.EXPO_PUBLIC_API_URL, { + const response = await fetch(process.env.EXPO_PUBLIC_API_URL as string, { method: "POST", headers: { - Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`, // Use environment variable for API key + Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ - model: process.env.EXPO_PUBLIC_MODEL_NAME, // Use environment variable for model name + model: process.env.EXPO_PUBLIC_MODEL_NAME, messages: [ { role: "system", @@ -90,13 +92,12 @@ const ChipDetection = ({ const result = await response.json(); if (!response.ok || !result.choices || !result.choices[0].message) { - throw new Error("Invalid response from API."); + throw new Error(i18n.t("invalid_response")); // Translate invalid response error } const rawContent = result.choices[0].message.content.trim(); const cleanJSON = rawContent.replace(/```json|```/g, "").trim(); - - const parsedData = JSON.parse(cleanJSON); + const parsedData: Record = JSON.parse(cleanJSON); const filteredData = Object.fromEntries( Object.entries(parsedData).filter(([_, count]) => count > 0) @@ -105,27 +106,36 @@ const ChipDetection = ({ setLastDetectedChips(filteredData); updateChipCount(filteredData); } catch (error) { - setError("Failed to analyze the image."); + setError(i18n.t("failed_to_analyze_image")); } finally { setLoading(false); } }; return ( - <> - -