Просмотр исходного кода

Add i18n support and integrate translations

Introduced i18next and react-i18next for internationalization, added English and Vietnamese translation files, and refactored UI components (Header, Footer, CompatibilityModal, ProductCard, SearchView, etc.) to use translation keys. Updated package dependencies, added language selection logic, and improved route handling. Also updated font usage and made minor UI/UX adjustments for localization.
trunghieubui 3 недель назад
Родитель
Сommit
8a88ab4f7a
36 измененных файлов с 835 добавлено и 372 удалено
  1. BIN
      EsimLao/docs/language.xlsx
  2. BIN
      EsimLao/esim-vite/dist.zip
  3. 5 5
      EsimLao/esim-vite/index.html
  4. 88 1
      EsimLao/esim-vite/package-lock.json
  5. 2 0
      EsimLao/esim-vite/package.json
  6. 8 19
      EsimLao/esim-vite/src/App.tsx
  7. 16 1
      EsimLao/esim-vite/src/apis/authApi.ts
  8. 4 1
      EsimLao/esim-vite/src/apis/productApi.ts
  9. 23 78
      EsimLao/esim-vite/src/components/CompatibilityModal.tsx
  10. 15 13
      EsimLao/esim-vite/src/components/Footer.tsx
  11. 47 30
      EsimLao/esim-vite/src/components/Header.tsx
  12. 31 27
      EsimLao/esim-vite/src/components/ProductCard.tsx
  13. 5 3
      EsimLao/esim-vite/src/components/SearchView.tsx
  14. 2 2
      EsimLao/esim-vite/src/components/TopLoader.tsx
  15. 0 1
      EsimLao/esim-vite/src/features/account/accuntSlice.ts
  16. 19 0
      EsimLao/esim-vite/src/i18n/index.ts
  17. 146 0
      EsimLao/esim-vite/src/i18n/locales/en.json
  18. 147 0
      EsimLao/esim-vite/src/i18n/locales/vi.json
  19. 4 0
      EsimLao/esim-vite/src/index.css
  20. 9 4
      EsimLao/esim-vite/src/pages/buy-sim/BuySimView.tsx
  21. 29 25
      EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx
  22. 8 8
      EsimLao/esim-vite/src/pages/contact/ContactView.tsx
  23. 34 5
      EsimLao/esim-vite/src/pages/home/HomeView.tsx
  24. 4 21
      EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx
  25. 10 6
      EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx
  26. 23 10
      EsimLao/esim-vite/src/pages/home/components/HomeSearch.tsx
  27. 4 52
      EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx
  28. 91 34
      EsimLao/esim-vite/src/pages/login/LoginView.tsx
  29. 6 3
      EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx
  30. 6 2
      EsimLao/esim-vite/src/pages/news/NewsView.tsx
  31. 16 12
      EsimLao/esim-vite/src/pages/order-detail/OrderDetailView.tsx
  32. 4 2
      EsimLao/esim-vite/src/pages/order-history/OrderHistoryView.tsx
  33. 10 2
      EsimLao/esim-vite/src/pages/product-detail/ProductDetailView.tsx
  34. 4 5
      EsimLao/esim-vite/src/pages/support/SupportView.tsx
  35. 14 0
      EsimLao/esim-vite/src/services/auth/types.ts
  36. 1 0
      EsimLao/esim-vite/src/services/product/type.ts

BIN
EsimLao/docs/language.xlsx


BIN
EsimLao/esim-vite/dist.zip


+ 5 - 5
EsimLao/esim-vite/index.html

@@ -6,14 +6,14 @@
   <link rel="icon" type="image/svg+xml" href="./publish/assets/img/logo_color.png" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Getgo | Stay Connected Everywhere</title>
-  <link rel="preconnect" href="https://fonts.googleapis.com" />
-  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
   <link
-    href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
-    rel="stylesheet" />
+    href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
+    rel="stylesheet">
   <style>
     body {
-      font-family: "Atkinson Hyperlegible", sans-serif;
+      font-family: "Montserrat", sans-serif;
       background-color: #ffffff;
       color: #1e293b;
       margin: 0;

+ 88 - 1
EsimLao/esim-vite/package-lock.json

@@ -13,9 +13,11 @@
         "@tailwindcss/vite": "^4.1.18",
         "@tanstack/react-query": "^5.90.16",
         "axios": "^1.13.2",
+        "i18next": "^25.7.4",
         "meta": "^2.2.25",
         "react": "^19.2.3",
         "react-dom": "^19.2.3",
+        "react-i18next": "^16.5.2",
         "react-redux": "^9.2.0",
         "react-router-dom": "^7.11.0",
         "redux": "^5.0.1",
@@ -262,6 +264,15 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.27.2",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -2823,6 +2834,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/html-parse-stringify": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+      "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+      "license": "MIT",
+      "dependencies": {
+        "void-elements": "3.1.0"
+      }
+    },
     "node_modules/https-proxy-agent": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -2836,6 +2856,37 @@
         "node": ">= 14"
       }
     },
+    "node_modules/i18next": {
+      "version": "25.7.4",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
+      "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://locize.com"
+        },
+        {
+          "type": "individual",
+          "url": "https://locize.com/i18next.html"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4"
+      },
+      "peerDependencies": {
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -4103,6 +4154,33 @@
         "react": "^19.2.3"
       }
     },
+    "node_modules/react-i18next": {
+      "version": "16.5.2",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
+      "integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "html-parse-stringify": "^3.0.1",
+        "use-sync-external-store": "^1.6.0"
+      },
+      "peerDependencies": {
+        "i18next": ">= 25.6.2",
+        "react": ">= 16.8.0",
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-redux": {
       "version": "9.2.0",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -4717,7 +4795,7 @@
       "version": "5.8.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
       "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
-      "dev": true,
+      "devOptional": true,
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -4857,6 +4935,15 @@
         }
       }
     },
+    "node_modules/void-elements": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/web-streams-polyfill": {
       "version": "3.3.3",
       "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

+ 2 - 0
EsimLao/esim-vite/package.json

@@ -14,9 +14,11 @@
     "@tailwindcss/vite": "^4.1.18",
     "@tanstack/react-query": "^5.90.16",
     "axios": "^1.13.2",
+    "i18next": "^25.7.4",
     "meta": "^2.2.25",
     "react": "^19.2.3",
     "react-dom": "^19.2.3",
+    "react-i18next": "^16.5.2",
     "react-redux": "^9.2.0",
     "react-router-dom": "^7.11.0",
     "redux": "^5.0.1",

+ 8 - 19
EsimLao/esim-vite/src/App.tsx

@@ -18,9 +18,11 @@ import OrderHistoryView from "./pages/order-history/OrderHistoryView";
 import OrderDetailView from "./pages/order-detail/OrderDetailView";
 import CompatibilityModal from "./components/CompatibilityModal";
 import QRCodeModal from "./components/QRCodeModal";
+import { useAppSelector } from "./hooks/useRedux";
 
 const App: React.FC = () => {
   const location = useLocation();
+  const loading = useAppSelector((state) => state.loading);
   const path = location.pathname;
 
   const isAiView = path.startsWith("/ai");
@@ -49,30 +51,14 @@ const App: React.FC = () => {
             isAiView ? "overflow-y-auto bg-slate-50" : "bg-white"
           }`}
         >
-          {isAiView && (
-            <div className="h-14 border-b border-slate-200 bg-white/50 backdrop-blur-md flex items-center justify-between px-6 shrink-0 z-10">
-              <div className="flex items-center space-x-2">
-                <span className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>
-                <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
-                  Assistant Active
-                </span>
-              </div>
-              <a
-                href="/"
-                className="text-[10px] font-bold text-[#EE0434] hover:underline"
-              >
-                Back to Store
-              </a>
-            </div>
-          )}
-
-          <TopLoader visible={true} />
+          <TopLoader />
 
           <div className="flex-1">
             <Routes>
               <Route path="/" element={<HomeView />} />
               <Route path="/buy-sim" element={<BuySimView />} />
               <Route path="/product/:country" element={<ProductDetailView />} />
+              <Route path="/product" element={<ProductDetailView />} />
               <Route path="/checkout" element={<CheckoutView />} />
               <Route path="/support" element={<SupportView />} />
               <Route path="/news" element={<NewsView />} />
@@ -84,7 +70,10 @@ const App: React.FC = () => {
                 path="/order-history-detail/:id"
                 element={<OrderDetailView />}
               />
-
+              <Route
+                path="/order-history-detail"
+                element={<OrderDetailView />}
+              />
               {/* Fallback */}
               <Route path="*" element={<Navigate to="/" replace />} />
             </Routes>

+ 16 - 1
EsimLao/esim-vite/src/apis/authApi.ts

@@ -1,4 +1,9 @@
-import { AccountInfo, RequestOtpResponse } from "../services/auth/types";
+import {
+  AccountInfo,
+  GoogleCallbackResponse,
+  GoogleLoginResponse,
+  RequestOtpResponse,
+} from "../services/auth/types";
 import { Area } from "../services/product/type";
 import { BaseApi } from "./baseApi";
 
@@ -18,6 +23,16 @@ class AuthApi extends BaseApi {
   async resendOtp({ email }) {
     return this.authPost<RequestOtpResponse>("/resend-otp", { email });
   }
+
+  async googleLogin() {
+    return this.authPost<GoogleLoginResponse>("/google-login", {});
+  }
+
+  async googleCallback({ code }) {
+    return this.authPost<AccountInfo>("/google-callback", {
+      code,
+    });
+  }
 }
 
 export const authApi = new AuthApi();

+ 4 - 1
EsimLao/esim-vite/src/apis/productApi.ts

@@ -11,7 +11,10 @@ class ProductApi extends BaseApi {
   }
 
   async loadArea({ isCountry = "-1", isPopular = "-1" }) {
-    return this.productPost("/list_area", { isCountry, isPopular });
+    return this.productPost("/list_area", {
+      isCountry,
+      isPopular,
+    });
   }
 
   async loadPackage({ areaId, isUnlimited = "-1", isDaily = "-1" }) {

+ 23 - 78
EsimLao/esim-vite/src/components/CompatibilityModal.tsx

@@ -5,6 +5,7 @@ import { useMutation } from "@tanstack/react-query";
 import { startLoading, stopLoading } from "../features/loading/loadingSlice";
 import { contentApi } from "../apis/contentApi";
 import { DeviceMeta } from "../services/content/types";
+import { useTranslation } from "react-i18next";
 
 const CompatibilityModal = () => {
   const popup = useAppSelector((state) => state.popup.compatibilityModal);
@@ -12,6 +13,7 @@ const CompatibilityModal = () => {
   const [brands, setBrands] = useState<DeviceMeta[] | []>([]);
   const [deviceList, setDeviceList] = useState<Record<string, string[]>>([]);
   const [selectedBrand, setSelectedBrand] = useState("");
+  const { t } = useTranslation();
 
   useEffect(() => {
     getDeviceMetaMutation.mutate();
@@ -51,45 +53,6 @@ const CompatibilityModal = () => {
     },
   });
 
-  // const brands = [
-  //   {
-  //     id: "apple",
-  //     label: "Apple",
-  //     icon: (
-  //       <svg className="w-8 h-8" viewBox="0 0 384 512" fill="currentColor">
-  //         <path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 52.3-11.4 69.5-34.3z" />
-  //       </svg>
-  //     ),
-  //   },
-  //   {
-  //     id: "samsung",
-  //     label: "Samsung",
-  //     icon: (
-  //       <svg className="w-12 h-8" viewBox="0 0 24 24" fill="currentColor">
-  //         <path d="M17.8 4.8C14.3 3.6 9.7 3.6 6.2 4.8 1.9 6.3 0 10.2 0 14.9s1.9 8.6 6.2 10.1c3.5 1.2 8.1 1.2 11.6 0 4.3-1.5 6.2-5.4 6.2-10.1s-1.9-8.6-6.2-10.1zM8 17.9c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm0-5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm8 5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4zm0-5.7c-1.3 0-2.4-.3-2.4-1.4 0-1.1 1.1-1.4 2.4-1.4 1.3 0 2.4.3 2.4 1.4 0 1.1-1.1 1.4-2.4 1.4z" />
-  //       </svg>
-  //     ),
-  //   },
-  //   {
-  //     id: "google",
-  //     label: "Google Pixel",
-  //     icon: (
-  //       <svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
-  //         <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
-  //       </svg>
-  //     ),
-  //   },
-  //   {
-  //     id: "others",
-  //     label: "Others",
-  //     icon: (
-  //       <svg className="w-6 h-8" viewBox="0 0 24 24" fill="currentColor">
-  //         <path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z" />
-  //       </svg>
-  //     ),
-  //   },
-  // ];
-
   if (popup.isOpen === false) {
     return null;
   }
@@ -129,8 +92,8 @@ const CompatibilityModal = () => {
         {/* Fixed Header */}
         <div className="shrink-0 p-8 md:p-10 pb-2 text-center bg-white z-10 relative">
           <h2 className="text-3xl md:text-5xl font-black text-slate-900 mb-2 tracking-tight">
-            Does my phone <br />
-            <span className="text-[#0091ff]">support eSIM ?</span>
+            {t("doesEsimWorkWithMyDevice")} <br />
+            {/* <span className="text-[#0091ff]">support eSIM ?</span> */}
           </h2>
         </div>
 
@@ -138,35 +101,19 @@ const CompatibilityModal = () => {
         <div className="flex-1 overflow-y-auto p-8 md:p-12 pt-4 custom-scrollbar">
           <div className="text-center mb-8">
             <h3 className="text-xl md:text-2xl font-black text-[#0091ff] mb-6">
-              Method 1: Quickly check using the list below
+              {t("selectTopicInstructions")}
             </h3>
 
             <div className="text-left bg-slate-50 p-6 rounded-2xl border border-slate-100 text-sm md:text-base text-slate-600 space-y-4 mb-8">
-              <p className="font-bold">
-                Important Note: Please make sure your device supports eSIM
-                before purchasing. The information below is for reference
-                purposes only. We are not responsible for any changes in device
-                information. We will not refund any order if the device does not
-                support eSIM, including the following cases:
-              </p>
+              <p className="font-bold">{t("importantNote")}</p>
               <ul className="list-disc pl-5 space-y-1 marker:text-[#0091ff]">
+                <li>{t("someDualPhysicalSimModelsDoNotSupportEsim")}</li>
+                <li>{t("carrierLockedDevicesMayNotSupportEsim")}</li>
                 <li>
-                  Some dual physical SIM phone models do not support eSIM.
-                </li>
-                <li>
-                  Carrier-locked devices (contract-based subscription devices)
-                  may not support eSIM.
-                </li>
-                <li>
-                  Some models purchased in certain countries may not support
-                  eSIM (see table below).
+                  {t("someModelsPurchasedInCertainCountriesMayNotSupportEsim")}
                 </li>
               </ul>
-              <p>
-                To make sure your device supports eSIM, please contact the
-                store/provider where you purchased the device for the most
-                accurate confirmation.
-              </p>
+              <p>{t("toMakeSureYourDeviceSupportsEsim")}</p>
             </div>
           </div>
 
@@ -255,17 +202,14 @@ const CompatibilityModal = () => {
           {/* Apple Specific Note */}
           <div className="text-left text-slate-600 space-y-4 mb-12">
             <p className="font-bold text-[#EE0434]">
-              *Note: The following apple devices do NOT support eSIM:
+              {t("noteTheFollowingAppleDevicesDoNotSupportEsim")}
             </p>
             <ul className="list-disc pl-5 space-y-2 text-sm md:text-base">
+              <li>{t("someDualPhysicalSimModelsDoNotSupportEsim")}</li>
               <li>
-                Phone models with dual physical SIMs do NOT support eSIM. Please
-                check Settings &gt; Cellular to ensure the option "Add eSIM" is
-                available.
-              </li>
-              <li>
-                All iPhones purchased from China, Hong Kong, and Macau DO NOT
-                support eSIM.
+                {t(
+                  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim"
+                )}
               </li>
             </ul>
           </div>
@@ -276,13 +220,14 @@ const CompatibilityModal = () => {
           {/* Method 2 */}
           <div className="text-center mb-12">
             <h3 className="text-xl md:text-3xl font-black text-[#0091ff] mb-4">
-              Method 2: Check directly on your device
+              {t("method2CheckDirectlyOnYourDevice")}
             </h3>
             <p className="text-slate-600 text-lg">
-              Check if your device supports eSIM and whether it is
-              carrier-locked by following{" "}
+              {t(
+                "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing"
+              )}
               <button className="text-[#0091ff] font-bold hover:underline">
-                the steps below
+                {t("theStepsBelow")}
               </button>
             </p>
           </div>
@@ -290,12 +235,12 @@ const CompatibilityModal = () => {
           {/* Not Support */}
           <div className="text-center mb-8">
             <h3 className="text-xl md:text-3xl font-black text-[#0091ff] mb-4">
-              Your device does not support eSIM?
+              {t("yourDeviceDoesNotSupportEsim")}
             </h3>
             <p className="text-slate-600 text-lg">
-              Try finding a physical SIM that is compatible with your device{" "}
+              {t("tryFindingAPhysicalSimThatIsCompatibleWithYourDevice")}{" "}
               <button className="text-[#0091ff] font-bold hover:underline">
-                here
+                {t("here")}
               </button>
             </p>
           </div>

+ 15 - 13
EsimLao/esim-vite/src/components/Footer.tsx

@@ -1,6 +1,8 @@
 import React from "react";
-import logoColor from "../assets/img/logo_color.png";
+import logo from "../assets/img/getgo.svg";
+import { useTranslation } from "react-i18next";
 const Footer: React.FC = () => {
+  const { t } = useTranslation();
   return (
     <footer className="bg-white border-t border-slate-100 pt-16 pb-8">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -16,11 +18,11 @@ const Footer: React.FC = () => {
                 <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
               </svg> */}
               <img
-                src={logoColor}
+                src={logo}
                 alt="Getgo Logo"
-                className="w-8 h-8 text-[#EE0434] mr-0"
+                className="text-[#EE0434] mr-0"
               />
-              <span className="text-xl font-black text-[#EE0434]">etgo</span>
+              {/* <span className="text-xl font-black text-[#EE0434]">etgo</span> */}
             </div>
             <p className="text-sm font-bold text-[#EE0434] leading-tight">
               Công ty TNHH Phát triển toàn cầu VIETTECH
@@ -91,39 +93,39 @@ const Footer: React.FC = () => {
             <ul className="space-y-4 text-sm text-slate-500">
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  About Us
+                  {t("aboutUs")}
                 </a>
               </li>
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  Terms Of Service
+                  {t("termsService")}
                 </a>
               </li>
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  Privacy Policy
+                  {t("privacyPolicy")}
                 </a>
               </li>
             </ul>
           </div>
           <div>
             <h3 className="text-base font-bold text-slate-900 mb-6">
-              Travel SIM
+              Travel {t("sim")}
             </h3>
             <ul className="space-y-4 text-sm text-slate-500">
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  China SIM
+                  {t("chinaSim")}
                 </a>
               </li>
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  Thailand SIM
+                  {t("thailandSim")}
                 </a>
               </li>
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  Japan SIM
+                  {t("japanSim")}
                 </a>
               </li>
             </ul>
@@ -133,12 +135,12 @@ const Footer: React.FC = () => {
             <ul className="space-y-4 text-sm text-slate-500">
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  What is eSIM
+                  {t("whatEsim")}
                 </a>
               </li>
               <li>
                 <a href="#" className="hover:text-[#EE0434] transition-colors">
-                  Support
+                  {t("supportSupport")}
                 </a>
               </li>
             </ul>

+ 47 - 30
EsimLao/esim-vite/src/components/Header.tsx

@@ -14,11 +14,13 @@ import { Area } from "../services/product/type";
 import { accountLogout } from "../features/account/accuntSlice";
 import { useSelector } from "react-redux";
 import { setAreas } from "../features/areas/areasSlice";
+import i18n from "../i18n";
+import { useTranslation } from "react-i18next";
 
 const Header: React.FC = () => {
   const navigate = useNavigate();
   const areas = useSelector((state: any) => state.areas.areas);
-
+  const { t } = useTranslation();
   const location = useLocation();
   const [isMenuOpen, setIsMenuOpen] = useState(false);
   const [isBuySimExpanded, setIsBuySimExpanded] = useState(false);
@@ -27,7 +29,9 @@ const Header: React.FC = () => {
   const [isBuySimMegaVisible, setIsBuySimMegaVisible] = useState(false);
   const [isGuideMegaVisible, setIsGuideMegaVisible] = useState(false);
   const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
-  const [selectedLang, setSelectedLang] = useState<"en" | "vi">("en");
+  const lang = localStorage.getItem("lang") || "en";
+
+  const [selectedLang, setSelectedLang] = useState<"en" | "vi">(lang);
   const [activeDesktopTab, setActiveDesktopTab] = useState<
     "popular" | "region"
   >("popular");
@@ -256,7 +260,7 @@ const Header: React.FC = () => {
                   <div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] border border-slate-100 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
                     <div className="max-h-[300px] overflow-y-auto custom-scrollbar p-2">
                       <h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 px-3 pt-2">
-                        Most Popular
+                        {t("mostPopular")}
                       </h3>
                       <div className="space-y-0.5">
                         {areasList.length > 0 ? (
@@ -280,7 +284,7 @@ const Header: React.FC = () => {
                               </div>
                               <div className="text-right flex items-center space-x-1.5">
                                 <span className="text-xs text-slate-400 font-medium">
-                                  from:
+                                  {t("from")}:
                                 </span>
                                 <span className="text-sm font-black text-[#EE0434]">
                                   {p.minSellPrice.toLocaleString()} {p.curency}
@@ -290,7 +294,7 @@ const Header: React.FC = () => {
                           ))
                         ) : (
                           <div className="text-center py-4 text-slate-400 text-sm font-medium">
-                            No matches found
+                            {t("noMatchesFound")}
                           </div>
                         )}
                       </div>
@@ -313,7 +317,7 @@ const Header: React.FC = () => {
                     : "text-slate-700 hover:text-[#EE0434]"
                 }`}
               >
-                Home
+                {t("home")}
               </Link>
 
               <div
@@ -329,7 +333,7 @@ const Header: React.FC = () => {
                       : "text-slate-700 hover:text-[#EE0434]"
                   }`}
                 >
-                  Buy SIM{" "}
+                  {t("buySim")}{" "}
                   <svg
                     className={`ml-1 w-4 h-4 transition-transform ${
                       isBuySimMegaVisible ? "rotate-180" : ""
@@ -351,7 +355,7 @@ const Header: React.FC = () => {
                   <div className="absolute top-full left-1/2 -translate-x-1/2 w-[950px] bg-white rounded-[32px] shadow-[0_30px_60px_-15px_rgba(0,0,0,0.15)] border border-slate-100 mt-0 overflow-hidden flex animate-in fade-in slide-in-from-top-2 duration-300">
                     <div className="w-[280px] bg-red-50 p-10 flex flex-col">
                       <h3 className="text-4xl font-black text-slate-900 mb-4">
-                        Buy SIM
+                        {t("buySim")}
                       </h3>
                       <button
                         onClick={() => {
@@ -360,7 +364,7 @@ const Header: React.FC = () => {
                         }}
                         className="text-[#EE0434] font-bold text-xl flex items-center group mb-8"
                       >
-                        View all{" "}
+                        {t("viewAll")}{" "}
                         <svg
                           className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
                           fill="none"
@@ -386,7 +390,7 @@ const Header: React.FC = () => {
                               : "bg-slate-50 text-slate-900 hover:bg-slate-100"
                           }`}
                         >
-                          Most Popular
+                          {t("mostPopular")}
                         </button>
                         <button
                           onClick={() => setActiveDesktopTab("region")}
@@ -396,7 +400,7 @@ const Header: React.FC = () => {
                               : "bg-slate-50 text-slate-900 hover:bg-slate-100"
                           }`}
                         >
-                          Region
+                          {t("region")}
                         </button>
                       </div>
                       <div className="grid grid-cols-4 gap-y-8 gap-x-4">
@@ -437,7 +441,7 @@ const Header: React.FC = () => {
                       : "text-slate-700 hover:text-[#EE0434]"
                   }`}
                 >
-                  Guide{" "}
+                  {t("guide")}{" "}
                   <svg
                     className={`ml-1 w-4 h-4 transition-transform ${
                       isGuideMegaVisible ? "rotate-180" : ""
@@ -483,7 +487,7 @@ const Header: React.FC = () => {
                     : "text-slate-700 hover:text-[#EE0434]"
                 }`}
               >
-                News
+                {t("news")}
               </Link>
 
               <Link
@@ -494,7 +498,7 @@ const Header: React.FC = () => {
                     : "text-slate-700 hover:text-[#EE0434]"
                 }`}
               >
-                Contact
+                {t("contact")}
               </Link>
             </nav>
 
@@ -546,15 +550,17 @@ const Header: React.FC = () => {
                               d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
                             />
                           </svg>
-                          <span className="font-bold text-sm">Orders</span>
+                          <span className="font-bold text-sm">
+                            {t("orders")}
+                          </span>
                         </button>
                         <div className="h-px bg-slate-50 mx-4 my-1"></div>
 
                         <button
                           onClick={() => {
+                            setIsUserMenuOpen(false);
                             dispatch(accountLogout());
                             navigate("/login");
-                            setIsUserMenuOpen(false);
                           }}
                           className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
                         >
@@ -571,7 +577,9 @@ const Header: React.FC = () => {
                               d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
                             />
                           </svg>
-                          <span className="font-bold text-sm">Logout</span>
+                          <span className="font-bold text-sm">
+                            {t("logout")}
+                          </span>
                         </button>
                       </div>
                     )}
@@ -579,9 +587,9 @@ const Header: React.FC = () => {
                       <div className="flex flex-col py-2">
                         <button
                           onClick={() => {
+                            setIsUserMenuOpen(false);
                             dispatch(accountLogout());
                             navigate("/login");
-                            setIsUserMenuOpen(false);
                           }}
                           className="flex items-center space-x-3 px-6 py-3.5 w-full text-left hover:bg-slate-50 text-slate-700 hover:text-[#EE0434] transition-colors"
                         >
@@ -598,7 +606,9 @@ const Header: React.FC = () => {
                               d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
                             />
                           </svg>
-                          <span className="font-bold text-sm">Login</span>
+                          <span className="font-bold text-sm">
+                            {t("login")}
+                          </span>
                         </button>
                       </div>
                     )}
@@ -647,6 +657,8 @@ const Header: React.FC = () => {
                           onClick={() => {
                             setSelectedLang(lang.code as "en" | "vi");
                             setIsLangMenuOpen(false);
+                            i18n.changeLanguage(lang.code);
+                            localStorage.setItem("lang", lang.code);
                           }}
                           className={`flex items-center space-x-3 px-5 py-4 w-full text-left transition-colors ${
                             selectedLang === lang.code
@@ -763,7 +775,7 @@ const Header: React.FC = () => {
                     : "text-slate-800 hover:bg-slate-50"
                 }`}
               >
-                Home
+                {t("home")}
               </Link>
 
               <div className="w-full">
@@ -775,7 +787,7 @@ const Header: React.FC = () => {
                       : "text-slate-800 hover:bg-slate-50"
                   }`}
                 >
-                  <span>Buy SIM</span>
+                  <span>{t("buySim")}</span>
                   <svg
                     className={`w-6 h-6 transition-transform duration-300 ${
                       isBuySimExpanded ? "rotate-180" : ""
@@ -807,7 +819,7 @@ const Header: React.FC = () => {
                       }}
                       className="col-span-2 text-center py-4 bg-slate-50 rounded-2xl text-[#EE0434] font-black text-sm uppercase tracking-wider shadow-sm"
                     >
-                      View All Destinations
+                      {t("viewAllDestinations")}
                     </button>
                     {products.map((c) => (
                       <button
@@ -838,7 +850,7 @@ const Header: React.FC = () => {
                       : "text-slate-800 hover:bg-slate-50"
                   }`}
                 >
-                  <span>Guide</span>
+                  <span>{t("guide")}</span>
                   <svg
                     className={`w-6 h-6 transition-transform duration-300 ${
                       isGuideExpanded ? "rotate-180" : ""
@@ -888,7 +900,7 @@ const Header: React.FC = () => {
                     : "text-slate-800 hover:bg-slate-50"
                 }`}
               >
-                News
+                {t("news")}
               </Link>
 
               <Link
@@ -900,7 +912,7 @@ const Header: React.FC = () => {
                     : "text-slate-800 hover:bg-slate-50"
                 }`}
               >
-                Contact
+                {t("contact")}
               </Link>
 
               {account !== null && (
@@ -915,19 +927,24 @@ const Header: React.FC = () => {
                       : "text-slate-800 hover:bg-slate-50"
                   }`}
                 >
-                  Transaction History
+                  {t("transactionHistory")}
                 </button>
               )}
 
               <div className="w-full pt-4">
                 <p className="text-center text-slate-400 font-bold text-xs uppercase tracking-widest mb-4">
-                  Select Language
+                  {t("selectLanguage")}
                 </p>
                 <div className="flex justify-center space-x-4">
                   {languages.map((lang) => (
                     <button
                       key={lang.code}
-                      onClick={() => setSelectedLang(lang.code as "en" | "vi")}
+                      onClick={() => {
+                        setSelectedLang(lang.code as "en" | "vi");
+                        setIsLangMenuOpen(false);
+                        i18n.changeLanguage(lang.code);
+                        localStorage.setItem("lang", lang.code);
+                      }}
                       className={`flex items-center space-x-2 px-4 py-2 rounded-2xl transition-all border ${
                         selectedLang === lang.code
                           ? "bg-red-50 border-[#EE0434] text-[#EE0434]"
@@ -957,7 +974,7 @@ const Header: React.FC = () => {
                 }}
                 className="w-full bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white py-5 rounded-[40px] font-black text-2xl shadow-xl active:scale-[0.98] transition-all flex justify-center"
               >
-                Login / Register
+                {t("login")} / {t("register")}
               </Link>
             )}
             {account !== null && (
@@ -969,7 +986,7 @@ const Header: React.FC = () => {
                 }}
                 className="w-full bg-gradient-to-r from-[#E21c34] to-[#500B28] text-white py-5 rounded-[40px] font-black text-2xl shadow-xl active:scale-[0.98] transition-all flex justify-center"
               >
-                Logout
+                {t("logout")}
               </Link>
             )}
           </div>

+ 31 - 27
EsimLao/esim-vite/src/components/ProductCard.tsx

@@ -2,37 +2,41 @@ import React from "react";
 
 import { SimProduct } from "../services/types";
 import { Area } from "../services/product/type";
+import { useTranslation } from "react-i18next";
 
 const ProductCard: React.FC<{
   p: Area;
   onClick: (p: Area) => void;
-}> = ({ p, onClick }) => (
-  <div
-    onClick={() => onClick(p)}
-    className="bg-white border border-slate-100 rounded-[20px] p-3 md:p-6 shadow-sm hover:shadow-lg transition-all relative flex flex-col md:flex-row items-center md:space-x-4 space-y-2 md:space-y-0 group cursor-pointer text-center md:text-left"
-  >
-    <div className="absolute top-0 right-0 bg-[#EE0434] text-white text-[10px] font-black px-2 py-1 md:px-3 md:py-1.5 rounded-tr-[19px] rounded-bl-[14px] absolute-right-18">
-      {p.promotionPercent}%
+}> = ({ p, onClick }) => {
+  const { t } = useTranslation();
+  return (
+    <div
+      onClick={() => onClick(p)}
+      className="bg-white border border-slate-100 rounded-[20px] p-3 md:p-6 shadow-sm hover:shadow-lg transition-all relative flex flex-col md:flex-row items-center md:space-x-4 space-y-2 md:space-y-0 group cursor-pointer text-center md:text-left"
+    >
+      <div className="absolute top-0 right-0 bg-[#EE0434] text-white text-[10px] font-black px-2 py-1 md:px-3 md:py-1.5 rounded-tr-[19px] rounded-bl-[14px] absolute-right-18">
+        {p.promotionPercent}%
+      </div>
+      <div className="w-10 h-10 md:w-12 md:h-12 rounded-full overflow-hidden shrink-0 border border-slate-50 bg-slate-50">
+        <img
+          src={`${p.iconUrl}`}
+          alt={p.areaName1}
+          className="w-full h-full object-cover scale-150"
+        />
+      </div>
+      <div className="flex-1 w-full">
+        <h3 className="text-slate-900 font-black text-sm md:text-lg mb-0.5 group-hover:text-[#EE0434] transition-colors truncate px-1">
+          {p.areaName1}
+        </h3>
+        <p className="text-[#EE0434] font-bold text-xs md:text-lg">
+          {t("from")}:{" "}
+          <span className="font-black">
+            {p.minDisplayPrice} {p.curency}
+          </span>
+        </p>
+      </div>
     </div>
-    <div className="w-10 h-10 md:w-12 md:h-12 rounded-full overflow-hidden shrink-0 border border-slate-50 bg-slate-50">
-      <img
-        src={`${p.iconUrl}`}
-        alt={p.areaName1}
-        className="w-full h-full object-cover scale-150"
-      />
-    </div>
-    <div className="flex-1 w-full">
-      <h3 className="text-slate-900 font-black text-sm md:text-lg mb-0.5 group-hover:text-[#EE0434] transition-colors truncate px-1">
-        {p.areaName1}
-      </h3>
-      <p className="text-[#EE0434] font-bold text-xs md:text-lg">
-        from:{" "}
-        <span className="font-black">
-          {p.minDisplayPrice} {p.curency}
-        </span>
-      </p>
-    </div>
-  </div>
-);
+  );
+};
 
 export default ProductCard;

+ 5 - 3
EsimLao/esim-vite/src/components/SearchView.tsx

@@ -1,6 +1,7 @@
 import React, { useState } from "react";
 import { searchGrounding } from "../services/gemini";
 import { GroundingSource } from "../services/types";
+import { useTranslation } from "react-i18next";
 
 const SearchView: React.FC = () => {
   const [query, setQuery] = useState("");
@@ -9,6 +10,7 @@ const SearchView: React.FC = () => {
     text: string;
     sources: GroundingSource[];
   } | null>(null);
+  const { t } = useTranslation();
 
   const handleSearch = async () => {
     if (!query.trim() || loading) return;
@@ -30,10 +32,10 @@ const SearchView: React.FC = () => {
       <div className="p-12 max-w-4xl mx-auto w-full">
         <div className="text-center mb-12">
           <h2 className="text-4xl font-extrabold mb-4 bg-gradient-to-r from-blue-400 to-emerald-400 bg-clip-text text-transparent">
-            Insight Engine
+            {t("insightEngine")}
           </h2>
           <p className="text-gray-400 text-lg">
-            Real-time web search powered by Google Grounding.
+            {t("realTimeWebSearchPoweredByGoogleGrounding")}
           </p>
         </div>
 
@@ -63,7 +65,7 @@ const SearchView: React.FC = () => {
               <div className="w-3 h-3 bg-blue-500 rounded-full animate-ping [animation-delay:0.4s]"></div>
             </div>
             <p className="text-gray-500 animate-pulse font-medium">
-              Scouring the web for real-time information...
+              {t("scouringTheWebForRealTimeInformation")}
             </p>
           </div>
         )}

+ 2 - 2
EsimLao/esim-vite/src/components/TopLoader.tsx

@@ -1,13 +1,13 @@
 import React from "react";
 import { useAppSelector } from "../hooks/useRedux";
 
-const TopLoader: React.FC<{ visible: boolean }> = ({ visible }) => {
+const TopLoader = () => {
   const isLoading = useAppSelector((state) => state.loading.isLoading);
   return (
     isLoading && (
       <div
         className={`fixed top-0 left-0 w-full h-[3px] z-[9999] pointer-events-none transition-opacity duration-500 ${
-          visible ? "opacity-100" : "opacity-0"
+          isLoading ? "opacity-100" : "opacity-0"
         }`}
       >
         {/* Primary loading bar */}

+ 0 - 1
EsimLao/esim-vite/src/features/account/accuntSlice.ts

@@ -8,7 +8,6 @@ const accountSlice = createSlice({
   },
   reducers: {
     accountLogin: (state, action: PayloadAction<AccountInfo>) => {
-      console.log("data in slice: ", action.payload);
       localStorage.setItem("token", action.payload.accessToken);
       localStorage.setItem("refreshToken", action.payload.refreshToken);
       localStorage.setItem("accountInfo", JSON.stringify(action.payload));

+ 19 - 0
EsimLao/esim-vite/src/i18n/index.ts

@@ -0,0 +1,19 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+
+import en from "./locales/en.json";
+import vi from "./locales/vi.json";
+
+i18n.use(initReactI18next).init({
+  resources: {
+    en: { translation: en },
+    vi: { translation: vi },
+  },
+  lng: "vi", // default
+  fallbackLng: "en",
+  interpolation: {
+    escapeValue: false,
+  },
+});
+
+export default i18n;

+ 146 - 0
EsimLao/esim-vite/src/i18n/locales/en.json

@@ -0,0 +1,146 @@
+{
+  "buySim": "Buy a SIM",
+  "viewAll": "View all",
+  "mostPopular": "Most Popular",
+  "region": "Region",
+  "guide": "Guide",
+  "whatEsim": "What is eSIM",
+  "installationInstructions": "Installation instructions",
+  "supportSupport": "Support",
+  "orderSearch": "Order Tracking Search",
+  "seeMore": "See more",
+  "whatUs": "What our customers say about us",
+  "whyGetgo": "Why choose Getgo",
+  "fastCost": "Fast – Convenient – Reasonable Cost",
+  "globalAnywhere": "Global connection, Stable Anywhere",
+  "customerSupport": "24/7 Customer Support",
+  "customerPolicy": "Customer-first Policy",
+  "getgoWith": "Getgo has partnered with",
+  "buyExplore": "Buy a SIM easily, ready to explore",
+  "compatibleDevice": "Compatible Device",
+  "pickPlan": "Pick Your Plan",
+  "instantActivation": "Instant Activation",
+  "frequentlyQuestions": "Frequently Asked Questions",
+  "refundDefective": "100% refund policy if product is defective",
+  "returnGuide": "Return and Refund Guide",
+  "chooseTo": "Choose country you're going to",
+  "doesEsim?": "Does my phone support eSIM?",
+  "ordersOrders": "Orders",
+  "logoutLogout": "Logout",
+  "viettechLimited": "VIETTECH Global Development Company Limited",
+  "taxCode": "Tax Code: 0901210362",
+  "address": "218 Dao Dua 1, Vinhomes Ocean Park 2 Urban Area, Nghia Tru Commune, Hung Yen Province, Vietnam",
+  "getgoGetgo": "Getgo",
+  "aboutUs": "About Us",
+  "termsService": "Terms of Service",
+  "privacyPolicy": "Privacy Policy",
+  "travelSim": "Travel SIM",
+  "chinaSim": "China SIM",
+  "thailandSim": "Thailand SIM",
+  "japanSim": "Japan SIM",
+  "emailAsia": "Email: sale.admin@viettech.asia",
+  "zaloWhatsapp": "Zalo/WhatsApp: +84 972 978 296",
+  "verifiedSpeed": "Verified: High Speed",
+  "numberDays": "Number of days",
+  "dataData": "Data",
+  "unlimitedUnlimited": "Unlimited",
+  "simType": "SIM Type",
+  "quantityQuantity": "Quantity",
+  "esimEsim": "eSIM",
+  "days": "days",
+  "discountDiscount": "Discount",
+  "pricePrice": "Price",
+  "vnd": "VND",
+  "continueShopping": "Continue shopping",
+  "chooseProduct": "Choose Product",
+  "orderInformation": "Order Information",
+  "paymentPayment": "Payment",
+  "customerInformation": "Customer Information",
+  "lastName": "Last Name",
+  "firstName": "First Name",
+  "emailEmail": "Email",
+  "confirmEmail": "Confirm Email",
+  "phoneNumber": "Phone number",
+  "orderInfo": "Order Info",
+  "esim": "eSIM",
+  "paymentMethod": "Payment method",
+  "onepayOnepay": "OnePay",
+  "iService": "I have read and agree to the Terms of Service",
+  "iUnlocked": "I confirm that my device supports eSIM and has been network unlocked",
+  "iInvoice": "I want to issue a VAT Invoice",
+  "overviewOverview": "Overview",
+  "totalTotal": "Total",
+  "paymentStatus": "Payment Status",
+  "paymentSuccessful": "Payment successful!",
+  "viewOrder": "View Order",
+  "closeClose": "Close",
+  "transactionHistory": "Transaction History",
+  "enterCode": "Enter order code",
+  "fromDate": "From date",
+  "toDate": "To date",
+  "statusStatus": "Status",
+  "successSuccess": "Success",
+  "pendingPayment": "Pending Payment",
+  "transactionNo.": "Transaction ID / Order No.",
+  "dateTime": "Date & Time",
+  "amountAmount": "Amount",
+  "detailDetail": "Detail",
+  "manageManage": "Manage",
+  "fullName": "Full Name",
+  "simNam": "SIM Viet Nam",
+  "validityPeriod": "Validity period",
+  "daysDays": "days",
+  "qrCode": "QR Code",
+  "getUs": "Get in touch with us",
+  "weArePossible": "We're always ready to listen and support you with any questions. Leave your information, and our team will get back to you as soon as possible.",
+  "enterName": "Enter your full name",
+  "specificRequirements": "Specific Requirements",
+  "describeRequirements": "Describe your requirements",
+  "submitSubmit": "Submit",
+  "from": "from",
+  "noMatchesFound": "No matches found",
+  "viewAllDestinations": "View All Destinations",
+  "selectLanguage": "Select Language",
+  "weAreAlwaysHereToSupportYou": "We are always here to support you.",
+  "frequently": "Frequently",
+  "askedQuestions": "Asked Questions",
+  "fastestData": "Fastest Data",
+  "bestPrices": "Best Prices",
+  "connectInstantlyInOver200Destinations": "Connect instantly in over 200 destinations",
+  "countryList": "Country List",
+  "regionList": "Regions",
+  "noCountryMatchesFound": "No countries found matching \"{searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "Does my phone support eSIM?",
+  "sayAboutUs": "say about us",
+  "overMillionSatisfiedCustomers": "Over 1,000,000 satisfied customers have used our services",
+  "simCountries": "SIM Countries",
+  "apply": "Apply",
+  "subtotal": "Subtotal",
+  "checkout": "Checkout",
+  "getInTouch": "Get in touch",
+  "withUs": "with us",
+  "backToNews": "Back to News",
+  "myEsim": "My eSIM",
+  "back": "Back",
+  "failure": "Failure",
+  "guideAndHelp": "Guide & Help",
+  "selectTopicInstructions": "Select a topic from the left to view detailed instructions for your travel SIM or eSIM. Our support team is also available 24/7 via chat.",
+  "importantNote": "Important Note: Please make sure your device supports eSIM before purchasing. The information below is for reference purposes only. We are not responsible for any changes in device information. We will not refund any order if the device does not support eSIM, including the following cases:",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "Some dual physical SIM phone models do not support eSIM.",
+  "carrierLockedDevicesMayNotSupportEsim": "Carrier-locked devices (contract-based subscription devices) may not support eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "Some models purchased in certain countries may not support eSIM (see table below).",
+  "toMakeSureYourDeviceSupportsEsim": "To make sure your device supports eSIM, please contact the store/provider where you purchased the device for the most accurate confirmation.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "*Note: The following apple devices do NOT support eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "All iPhones purchased from China, Hong Kong, and Macau DO NOT support eSIM.",
+  "method2CheckDirectlyOnYourDevice": "Method 2: Check directly on your device",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "Check if your device supports eSIM and whether it is carrier-locked by following",
+  "theStepsBelow": "the steps below",
+  "yourDeviceDoesNotSupportEsim": "Your device does not support eSIM?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "Try finding a physical SIM that is compatible with your device",
+  "here": "here",
+  "insightEngine": "Insight Engine",
+  "realTimeWebSearchPoweredByGoogleGrounding": "Real-time web search powered by Google Grounding",
+  "scouringTheWebForRealTimeInformation": "Scouring the web for real-time information...",
+  "news": "News",
+  "contact": "Contact"
+}

+ 147 - 0
EsimLao/esim-vite/src/i18n/locales/vi.json

@@ -0,0 +1,147 @@
+{
+  "buySim": "Mua SIM",
+  "viewAll": "Xem tất cả",
+  "mostPopular": "Phổ biến nhất",
+  "region": "Khu vực",
+  "guide": "Hướng dẫn",
+  "whatEsim": "eSIM là gì",
+  "installationInstructions": "Hướng dẫn cài đặt",
+  "supportSupport": "Hỗ trợ",
+  "orderSearch": "Tra cứu đơn hàng",
+  "seeMore": "Xem thêm",
+  "whatUs": "Khách hàng nói gì về chúng tôi",
+  "whyGetgo": "Vì sao chọn Getgo",
+  "fastCost": "Nhanh – Tiện lợi – Chi phí hợp lý",
+  "globalAnywhere": "Kết nối toàn cầu, ổn định mọi nơi",
+  "customerSupport": "Hỗ trợ khách hàng 24/7",
+  "customerPolicy": "Chính sách ưu tiên khách hàng",
+  "getgoWith": "Getgo hợp tác cùng",
+  "buyExplore": "Mua SIM dễ dàng, sẵn sàng khám phá",
+  "compatibleDevice": "Thiết bị tương thích",
+  "pickPlan": "Chọn gói cước",
+  "instantActivation": "Kích hoạt ngay lập tức",
+  "frequentlyQuestions": "Câu hỏi thường gặp",
+  "100% refundDefective": "Hoàn tiền 100% nếu sản phẩm bị lỗi",
+  "returnGuide": "Hướng dẫn đổi trả & hoàn tiền",
+  "chooseTo": "Chọn quốc gia bạn sắp đến",
+  "doesEsim?": "Điện thoại của tôi có hỗ trợ eSIM không?",
+  "ordersOrders": "Thông tin đơn hàng",
+  "logoutLogout": "Đăng xuất",
+  "viettechLimited": "Công ty TNHH Phát triển toàn cầu VIETTECH",
+  "taxCode": "Mã số thuế: 0901210362",
+  "address": "218 Đảo Dừa 1, Khu đô thị Vinhomes Ocean Park 2, Xã Nghĩa Trụ, Tỉnh Hưng Yên, Việt Nam",
+  "getgoGetgo": "Getgo",
+  "aboutUs": "Giới thiệu",
+  "termsService": "Điều khoản dịch vụ",
+  "privacyPolicy": "Chính sách bảo mật",
+  "travelSim": "SIM du lịch",
+  "chinaSim": "SIM Trung Quốc",
+  "thailandSim": "SIM Thái Lan",
+  "japanSim": "SIM Nhật Bản",
+  "emailAsia": "Email: sale.admin@viettech.asia",
+  "zaloWhatsapp": "Zalo/Whatsapp: +84972.978.296",
+  "verifiedSpeed": "Đã xác thực: Tốc độ cao",
+  "numberDays": "Số ngày",
+  "dataData": "Dữ liệu",
+  "unlimitedUnlimited": "Không giới hạn",
+  "simType": "Loại SIM",
+  "quantityQuantity": "Số lượng",
+  "esimEsim": "eSIM",
+  "days": "ngày",
+  "discountDiscount": "Giảm giá",
+  "pricePrice": "Giá",
+  "vnd": "đ (VNĐ)",
+  "continueShopping": "Tiếp tục mua sắm",
+  "chooseProduct": "Chọn sản phẩm",
+  "orderInformation": "Thông tin đơn hàng",
+  "paymentPayment": "Thanh toán",
+  "customerInformation": "Thông tin khách hàng",
+  "lastName": "Họ",
+  "firstName": "Tên",
+  "emailEmail": "Email",
+  "confirmEmail": "Xác nhận Email",
+  "phoneNumber": "Số điện thoại",
+  "orderInfo": "Thông tin đơn hàng",
+  "esim": "eSIM",
+  "paymentMethod": "Phương thức thanh toán",
+  "onepayOnepay": "OnePay",
+  "iService": "Tôi đã đọc và đồng ý với Điều khoản dịch vụ",
+  "iUnlocked": "Tôi xác nhận thiết bị của mình có hỗ trợ eSIM và đã được mở khóa mạng",
+  "iInvoice": "Tôi muốn xuất hóa đơn VAT",
+  "overviewOverview": "Tổng quan",
+  "totalTotal": "Tổng cộng",
+  "paymentStatus": "Trạng thái thanh toán",
+  "paymentSuccessful": "Thanh toán thành công!",
+  "viewOrder": "Xem đơn hàng",
+  "closeClose": "Đóng",
+  "transactionHistory": "Lịch sử giao dịch",
+  "enterCode": "Nhập mã đơn hàng",
+  "fromDate": "Từ ngày",
+  "toDate": "Đến ngày",
+  "statusStatus": "Trạng thái",
+  "successSuccess": "Thành công",
+  "pendingPayment": "Chờ thanh toán",
+  "transactionNo.": "Mã giao dịch / Mã đơn hàng",
+  "dateTime": "Ngày & giờ",
+  "amountAmount": "Số tiền",
+  "detailDetail": "Chi tiết",
+  "manageManage": "Quản lý",
+  "fullName": "Họ và tên",
+  "simNam": "SIM Việt Nam",
+  "validityPeriod": "Thời hạn sử dụng",
+  "daysDays": "ngày",
+  "qrCode": "Mã QR",
+  "getUs": "Liên hệ với chúng tôi",
+  "weArePossible": "Chúng tôi luôn sẵn sàng lắng nghe và hỗ trợ bạn với mọi thắc mắc. Vui lòng để lại thông tin, đội ngũ của chúng tôi sẽ liên hệ lại với bạn trong thời gian sớm nhất.",
+  "enterName": "Nhập họ và tên của bạn",
+  "specificRequirements": "Yêu cầu cụ thể",
+  "describeRequirements": "Mô tả yêu cầu của bạn",
+  "submitSubmit": "Gửi",
+  "from": "từ",
+  "noMatchesFound": "Không tìm thấy kết quả",
+  "viewAllDestinations": "Xem tất cả điểm đến",
+  "selectLanguage": "Chọn ngôn ngữ",
+  "weAreAlwaysHereToSupportYou": "Chúng tôi luôn ở đây để hỗ trợ bạn.",
+  "frequently": "Thường gặp",
+  "askedQuestions": "Câu hỏi được hỏi",
+  "fastestData": "Dữ liệu nhanh nhất",
+  "bestPrices": "Giá tốt nhất",
+  "connectInstantlyInOver200Destinations": "Kết nối ngay lập tức ở hơn 200 điểm đến",
+  "countryList": "Danh sách quốc gia",
+  "regionList": "Khu vực",
+  "noCountryMatchesFound": "Không tìm thấy quốc gia nào phù hợp với \"{searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "Điện thoại của tôi có hỗ trợ eSIM không?",
+  "sayAboutUs": "nói về chúng tôi",
+  "overMillionSatisfiedCustomers": "Hơn 1.000.000 khách hàng hài lòng đã sử dụng dịch vụ của chúng tôi",
+  "simCountries": "Quốc gia SIM",
+  "apply": "Áp dụng",
+  "subtotal": "Tạm tính",
+  "checkout": "Thanh toán",
+  "getInTouch": "Liên hệ",
+  "withUs": "với chúng tôi",
+  "backToNews": "Quay lại Tin tức",
+  "myEsim": "eSIM của tôi",
+  "back": "Quay lại",
+  "failure": "Thất bại",
+  "guideAndHelp": "Hướng dẫn & Trợ giúp",
+  "selectTopicInstructions": "Chọn một chủ đề từ bên trái để xem hướng dẫn chi tiết cho SIM du lịch hoặc eSIM của bạn. Đội ngũ hỗ trợ của chúng tôi cũng sẵn sàng 24/7 qua trò chuyện.",
+  "importantNote": "Lưu ý quan trọng: Vui lòng đảm bảo thiết bị của bạn hỗ trợ eSIM trước khi mua hàng. Thông tin dưới đây chỉ mang tính tham khảo. Chúng tôi không chịu trách nhiệm về bất kỳ thay đổi nào trong thông tin thiết bị. Chúng tôi sẽ không hoàn tiền cho bất kỳ đơn hàng nào nếu thiết bị không hỗ trợ eSIM, bao gồm các trường hợp sau:",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "Một số mẫu điện thoại có hai SIM vật lý không hỗ trợ eSIM.",
+  "carrierLockedDevicesMayNotSupportEsim": "Các thiết bị bị khóa nhà mạng (thiết bị đăng ký theo hợp đồng) có thể không hỗ trợ eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "Một số mẫu mua ở các quốc gia nhất định có thể không hỗ trợ eSIM (xem bảng bên dưới).",
+  "toMakeSureYourDeviceSupportsEsim": "Để chắc chắn thiết bị của bạn hỗ trợ eSIM, vui lòng liên hệ cửa hàng/nhà cung cấp nơi bạn đã mua thiết bị để có xác nhận chính xác nhất.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "*Lưu ý: Các thiết bị Apple sau KHÔNG hỗ trợ eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "Tất cả iPhone mua từ Trung Quốc, Hồng Kông và Ma Cao KHÔNG hỗ trợ eSIM.",
+  "method2CheckDirectlyOnYourDevice": "Phương pháp 2: Kiểm tra trực tiếp trên thiết bị của bạn",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "Kiểm tra xem thiết bị của bạn có hỗ trợ eSIM và liệu nó có bị khóa nhà mạng hay không bằng cách làm theo",
+  "theStepsBelow": "các bước dưới đây",
+  "yourDeviceDoesNotSupportEsim": "Thiết bị của bạn không hỗ trợ eSIM?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "Hãy thử tìm một SIM vật lý tương thích với thiết bị của bạn",
+  "here": "tại đây",
+  "insightEngine": "Công cụ Thấu hiểu",
+  "realTimeWebSearchPoweredByGoogleGrounding": "Tìm kiếm web thời gian thực được hỗ trợ bởi Google Grounding",
+  "scouringTheWebForRealTimeInformation": "Đang tìm kiếm thông tin thời gian thực trên web...",
+  "news": "Tin tức",
+  "contact": "Liên hệ",
+  "home": "Trang chủ"
+}

+ 4 - 0
EsimLao/esim-vite/src/index.css

@@ -9,4 +9,8 @@
   .absolute-right-18 {
     right: 0px;
   }
+}
+
+.font-black{
+  font-weight: 700 !important;
 }

+ 9 - 4
EsimLao/esim-vite/src/pages/buy-sim/BuySimView.tsx

@@ -9,6 +9,7 @@ import { Area } from "../../services/product/type";
 import { DataCacheKey, staleTime } from "../../global/constants";
 import { useNavigate } from "react-router-dom";
 import HomeSearch from "../home/components/HomeSearch";
+import { useTranslation } from "react-i18next";
 
 interface BuySimViewProps {
   onProductSelect: (product: SelectedProduct) => void;
@@ -21,6 +22,8 @@ const BuySimView: React.FC<BuySimViewProps> = ({
 }) => {
   const dispatch = useAppDispatch();
   const navigate = useNavigate();
+  const { t } = useTranslation();
+
   const [countries, setCountries] = React.useState<Area[]>([]);
   const [populars, setPopulars] = React.useState<Area[]>([]);
 
@@ -66,7 +69,7 @@ const BuySimView: React.FC<BuySimViewProps> = ({
             onClick={() => onViewChange(ViewMode.HOME)}
             className="hover:text-[#EE0434] text-[18px]"
           >
-            Home
+            {t("home")}
           </button>
           <svg
             className="w-3 h-3"
@@ -81,7 +84,9 @@ const BuySimView: React.FC<BuySimViewProps> = ({
               strokeLinejoin="round"
             />
           </svg>
-          <span className="text-slate-900 font-bold text-[18px]">Buy Sim</span>
+          <span className="text-slate-900 font-bold text-[18px]">
+            {t("buySim")}
+          </span>
         </nav>
       </div>
 
@@ -91,7 +96,7 @@ const BuySimView: React.FC<BuySimViewProps> = ({
         </div>
         <section className="mb-12">
           <h2 className="text-xl md:text-[32px] font-black text-slate-900 mb-6 md:mb-8 tracking-tight">
-            SIM Countries
+            {t("simCountries")}
           </h2>
           <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
             {loadArea
@@ -103,7 +108,7 @@ const BuySimView: React.FC<BuySimViewProps> = ({
         </section>
         <section className="mb-12">
           <h2 className="text-xl md:text-[32px] font-black text-slate-900 mb-6 md:mb-8 tracking-tight">
-            SIM Regions
+            SIM {t("regionList")}
           </h2>
           <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
             {loadArea

+ 29 - 25
EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx

@@ -19,17 +19,20 @@ import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
 import { productApi } from "../../apis/productApi";
 import { openPopup } from "../../features/popup/popupSlice";
 import { formatNumber } from "../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
 
 const CheckoutView = () => {
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
+  const { t } = useTranslation();
   const loading = useAppSelector((state) => state.loading);
+  const accountInfo = localStorage.getItem("accountInfo");
   const [paymentMethod, setPaymentMethod] = useState("card");
   const [form, setForm] = useState({
     firstName: "",
     lastName: "",
-    email: "",
-    confirmEmail: "",
+    email: accountInfo != null ? JSON.parse(accountInfo).email : "",
+    confirmEmail: accountInfo != null ? JSON.parse(accountInfo).email : "",
     phone: "",
   });
   const [agreements, setAgreements] = useState({
@@ -151,7 +154,7 @@ const CheckoutView = () => {
                 d="M15 19l-7-7 7-7"
               />
             </svg>
-            Continue shopping
+            {t("continueShopping")}
           </button>
         </div>
       </div>
@@ -176,7 +179,9 @@ const CheckoutView = () => {
                   />
                 </svg>
               </div>
-              <span className="text-[#00c087] text-[14px]">Choose Product</span>
+              <span className="text-[#00c087] text-[14px]">
+                {t("chooseProduct")}
+              </span>
             </div>
             <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
             <div className="flex flex-col items-center gap-2">
@@ -184,7 +189,7 @@ const CheckoutView = () => {
                 2
               </div>
               <span className="text-[#0071e3] text-center text-[14px]">
-                Order Information
+                {t("orderInformation")}
               </span>
             </div>
             <div className="flex-1 h-0.5 bg-slate-200 mx-2 mb-6"></div>
@@ -192,7 +197,9 @@ const CheckoutView = () => {
               <div className="w-8 h-8 rounded-full bg-white border border-slate-200 text-slate-300 flex items-center justify-center">
                 3
               </div>
-              <span className="text-slate-300 text-[14px]">Payment</span>
+              <span className="text-slate-300 text-[14px]">
+                {t("paymentPayment")}
+              </span>
             </div>
           </div>
         </div>
@@ -201,12 +208,12 @@ const CheckoutView = () => {
           {/* Customer Information */}
           <div>
             <h2 className="text-xl font-black text-[#003459] mb-6">
-              Customer Information
+              {t("customerInformation")}
             </h2>
             <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
               <div className="space-y-1">
                 <label className={labelClass}>
-                  Last Name <span className="text-red-500">*</span>
+                  {t("lastName")} <span className="text-red-500">*</span>
                 </label>
                 <input
                   type="text"
@@ -219,7 +226,7 @@ const CheckoutView = () => {
               </div>
               <div className="space-y-1">
                 <label className={labelClass}>
-                  First Name <span className="text-red-500">*</span>
+                  {t("firstName")} <span className="text-red-500">*</span>
                 </label>
                 <input
                   type="text"
@@ -232,7 +239,7 @@ const CheckoutView = () => {
               </div>
               <div className="space-y-1">
                 <label className={labelClass}>
-                  Email <span className="text-red-500">*</span>
+                  {t("emailEmail")} <span className="text-red-500">*</span>
                 </label>
                 <input
                   type="email"
@@ -245,7 +252,7 @@ const CheckoutView = () => {
               </div>
               <div className="space-y-1">
                 <label className={labelClass}>
-                  Confirm Email <span className="text-red-500">*</span>
+                  {t("confirmEmail")} <span className="text-red-500">*</span>
                 </label>
                 <input
                   type="email"
@@ -257,7 +264,7 @@ const CheckoutView = () => {
                 />
               </div>
               <div className="md:col-span-2 space-y-1">
-                <label className={labelClass}>Phone Number</label>
+                <label className={labelClass}>{t("phoneNumber")}</label>
                 <input
                   type="tel"
                   className={inputClass}
@@ -273,7 +280,7 @@ const CheckoutView = () => {
           {/* Order Info */}
           <div className="bg-slate-50/50 rounded-3xl p-6 border border-slate-100">
             <h2 className="text-xl font-black text-[#003459] mb-6">
-              Order Info
+              {t("orderInformation")}
             </h2>
             <div className="flex justify-between items-center">
               <div className="flex items-center gap-4">
@@ -307,7 +314,7 @@ const CheckoutView = () => {
           {/* Payment Method */}
           <div>
             <h2 className="text-xl font-black text-[#003459] mb-6">
-              Payment Method
+              {t("paymentPayment")}
             </h2>
             <div className="space-y-3">
               {loadPaymentChannel.map((method) => (
@@ -348,12 +355,12 @@ const CheckoutView = () => {
                 className="mt-1 w-4 h-4 rounded border-slate-300 text-[#0071e3] focus:ring-[#0071e3]"
               />
               <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
-                I have read and agree to the{" "}
+                {t("iService")}{" "}
                 <a
                   href="#"
                   className="text-[#0071e3] font-bold hover:underline"
                 >
-                  Terms of Service
+                  {t("termsService")}
                 </a>
               </span>
             </label>
@@ -366,8 +373,7 @@ const CheckoutView = () => {
                 onChange={handleCheckboxChange}
               />
               <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
-                I confirm that my device supports eSIM and has been network
-                unlocked
+                {t("iUnlocked")}
               </span>
             </label>
             <label className="flex items-start cursor-pointer group">
@@ -379,7 +385,7 @@ const CheckoutView = () => {
                 onChange={handleCheckboxChange}
               />
               <span className="ml-3 text-sm text-slate-500 font-medium group-hover:text-slate-700 transition-colors">
-                I want to issue a VAT Invoice
+                {t("iInvoice")}
               </span>
             </label>
           </div>
@@ -395,13 +401,13 @@ const CheckoutView = () => {
                 className="w-full bg-white border-2 border-transparent focus:border-[#0071e3]/20 rounded-2xl py-4 px-6 focus:outline-none transition-all text-slate-700 font-bold placeholder:text-slate-300"
               />
               <button className="absolute right-3 top-2 bottom-2 text-[#0071e3] font-black text-sm px-4 hover:bg-blue-50 rounded-xl transition-colors">
-                Apply
+                {t("apply")}
               </button>
             </div>
 
             <div className="space-y-4 mb-8">
               <div className="flex justify-between text-slate-600 font-bold">
-                <span>Subtotal:</span>
+                <span>{t("subtotal")}:</span>
                 <span>
                   {formatNumber(
                     state.checkoutDetails.totalMoney * state.quantity
@@ -418,7 +424,7 @@ const CheckoutView = () => {
               </div>
               <div className="flex justify-between items-center pt-4 border-t border-slate-200/50">
                 <span className="text-[#0071e3] font-black text-xl">
-                  Total ({state.quantity}):
+                  {t("totalTotal")} ({state.quantity}):
                 </span>
                 <span className="text-[#0071e3] font-black text-3xl">
                   {formatNumber(state.checkoutDetails.paymentMoney)}{" "}
@@ -434,7 +440,6 @@ const CheckoutView = () => {
                 form.email &&
                 agreements.terms &&
                 agreements.esim &&
-                agreements.vatInvoice &&
                 !loading.isSmallLoading
                   ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
                   : "bg-slate-100 text-slate-300 cursor-not-allowed"
@@ -445,7 +450,6 @@ const CheckoutView = () => {
                 !form.email ||
                 !agreements.terms ||
                 !agreements.esim ||
-                !agreements.vatInvoice ||
                 loading.isSmallLoading
               }
               onClick={handleSubmit}
@@ -453,7 +457,7 @@ const CheckoutView = () => {
               {loading.isSmallLoading && (
                 <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
               )}
-              <span>Checkout</span>
+              <span>{t("checkout")}</span>
             </button>
           </div>
         </div>

+ 8 - 8
EsimLao/esim-vite/src/pages/contact/ContactView.tsx

@@ -1,18 +1,18 @@
 import React from "react";
+import { useTranslation } from "react-i18next";
 
 const ContactView: React.FC = () => {
+  const { t } = useTranslation();
   return (
     <div className="bg-white">
       <section className="max-w-7xl mx-auto px-6 md:px-12 py-16 md:py-24">
         <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
           <div className="space-y-8">
             <h1 className="text-5xl md:text-7xl font-black text-[#EE0434] tracking-tight leading-tight">
-              Get in touch <br /> with us
+              {t("getInTouch")} <br /> {t("withUs")}
             </h1>
             <p className="text-slate-500 text-lg md:text-xl font-medium leading-relaxed max-w-lg">
-              We're always ready to listen and support you with any questions.
-              Leave your information, and our team will get back to you as soon
-              as possible.
+              {t("weAreAlwaysHereToSupportYou")}
             </p>
             <div className="space-y-6 pt-4">
               <div className="space-y-2">
@@ -45,17 +45,17 @@ const ContactView: React.FC = () => {
             <form className="space-y-8" onSubmit={(e) => e.preventDefault()}>
               <div className="space-y-3">
                 <label className="block text-slate-800 font-bold text-lg">
-                  Full Name
+                  {t("fullName")}
                 </label>
                 <input
                   type="text"
-                  placeholder="Enter your full name"
+                  placeholder={t("enterName")}
                   className="w-full bg-white border border-slate-200 rounded-xl py-4 px-6 focus:outline-none focus:ring-2 focus:ring-red-50 focus:border-[#EE0434] transition-all"
                 />
               </div>
               <div className="space-y-3">
                 <label className="block text-slate-800 font-bold text-lg">
-                  Specific Requirements
+                  {t("specificRequirements")}
                 </label>
                 <textarea
                   rows={6}
@@ -67,7 +67,7 @@ const ContactView: React.FC = () => {
                 type="submit"
                 className="w-full bg-[#EE0434] text-white py-5 rounded-[40px] font-black text-2xl shadow-lg hover:shadow-xl transition-all"
               >
-                Submit
+                {t("submitSubmit")}
               </button>
             </form>
           </div>

+ 34 - 5
EsimLao/esim-vite/src/pages/home/HomeView.tsx

@@ -1,5 +1,5 @@
 import React, { useState, useEffect, useCallback } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
 import { SimProduct } from "../../services/types";
 import HomeBanner from "./components/HomeBanner";
 import HomeTestimonial from "./components/HomeTestimonial";
@@ -10,16 +10,28 @@ import { useAppDispatch } from "../../hooks/useRedux";
 import { openPopup } from "../../features/popup/popupSlice";
 import partner1 from "../../assets/img/partner1.png";
 import partner2 from "../../assets/img/partner2.png";
+import { authApi } from "../../apis/authApi";
+import { accountLogin } from "../../features/account/accuntSlice";
+import { useTranslation } from "react-i18next";
 
 const HomeView: React.FC = () => {
   const [simType, setSimType] = useState<"eSIM" | "Physical">("eSIM");
-
+  const [searchParams] = useSearchParams();
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
+  const { t } = useTranslation();
 
   useEffect(() => {
     const params = new URLSearchParams(window.location.search);
+
+    console.log("URL Params: ", params.toString());
     const status = params.get("vpc_TxnResponseCode");
+
+    console.log("searchParams: ", searchParams.toString());
+
+    // google callback
+    const code = searchParams.get("code");
+
     if (status) {
       console.log("URL Params:", params);
       if (status === "0") {
@@ -44,23 +56,40 @@ const HomeView: React.FC = () => {
           })
         );
       }
+    } else if (code) {
+      console.log("Handling Google callback with code:", code);
+      handleGoogleCallback(code);
     }
   }, []);
 
+  const handleGoogleCallback = async (code: string) => {
+    try {
+      const response = await authApi.googleCallback({ code });
+      if (response.errorCode === "0") {
+        dispatch(accountLogin(response.data));
+        navigate("/");
+      } else {
+        console.error("Google callback failed:", response.message);
+      }
+    } catch (error) {
+      console.error("Google callback error:", error);
+    }
+  };
+
   const steps = [
     {
       number: "1",
-      title: "Compatible Device",
+      title: t("compatibleDevice"),
       description: "Check if your phone supports eSIM technology.",
     },
     {
       number: "2",
-      title: "Pick Your Plan",
+      title: t("pickPlan"),
       description: "Select a data package for your destination.",
     },
     {
       number: "3",
-      title: "Instant Activation",
+      title: t("instantActivation"),
       description: "Scan the QR code and connect to high-speed data.",
     },
   ];

+ 4 - 21
EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx

@@ -8,29 +8,12 @@ import {
   stopLoading,
 } from "../../../features/loading/loadingSlice";
 import { useAppDispatch } from "../../../hooks/useRedux";
+import { useTranslation } from "react-i18next";
 
 const HomeFaq = () => {
   const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
   const dispatch = useAppDispatch();
-  // const faqs = [
-  //   {
-  //     question:
-  //       "1. Does using an eSIM/ SIM incur any additional fees or services?",
-  //     answer:
-  //       "No. The price shown in the order already includes all costs, and no additional fees will arise during usage.",
-  //   },
-  //   {
-  //     question: "2. I received the email but there is no QR code.",
-  //     answer:
-  //       "Please check your spam or junk folder first. If it's not there, contact our support team via Zalo OA or WhatsApp with your order ID, and we will assist you in resending the QR code manually.",
-  //   },
-  //   {
-  //     question: "3. I lost my SIM while traveling abroad. Can it be reissued?",
-  //     answer:
-  //       "For eSIMs, we can easily resend the digital profile to your registered email. For physical SIMs, reissuance while abroad may be difficult due to shipping; we recommend switching to an eSIM if your device supports it.",
-  //   },
-  // ];
-
+  const { t } = useTranslation();
   const { data: loadFaqsData = [] } = useQuery<Faq[]>({
     queryKey: [DataCacheKey.FAQS],
     queryFn: async (): Promise<Faq[]> => {
@@ -61,10 +44,10 @@ const HomeFaq = () => {
         <div className="max-w-4xl mx-auto">
           <div className="text-center mb-12 md:mb-20">
             <h2 className="text-3xl md:text-6xl font-black tracking-tight text-[#EE0434] mb-4">
-              Frequently <span className="text-slate-900">Asked Questions</span>
+              {t("frequently")} <span className="text-slate-900">{t("askedQuestions")}</span>
             </h2>
             <p className="text-slate-500 text-sm md:text-xl font-medium">
-              We are always here to support you.
+              {t("weAreAlwaysHereToSupportYou")}
             </p>
           </div>
 

+ 10 - 6
EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx

@@ -11,12 +11,13 @@ import {
 import { DataCacheKey, staleTime } from "../../../global/constants";
 import { Area } from "../../../services/product/type";
 import { formatNumber } from "../../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
 
 const HomeProduct = () => {
   const [activeTab, setActiveTab] = useState<"country" | "region">("country");
   const navigate = useNavigate();
   const [areasList, setAreasList] = useState<Area[]>([]);
-
+  const { t } = useTranslation();
   const dispatch = useAppDispatch();
 
   useEffect(() => {
@@ -68,10 +69,11 @@ const HomeProduct = () => {
   return (
     <section className="pt-12 pb-20 text-center px-4">
       <h2 className="text-3xl md:text-5xl lg:text-6xl font-black text-slate-900 tracking-tight mb-4">
-        Fastest Data, <span className="text-[#EE0434]">Best Prices.</span>
+        {t("fastestData")},{" "}
+        <span className="text-[#EE0434]">{t("bestPrices")}</span>
       </h2>
       <p className="text-slate-500 font-bold mb-10 md:mb-12">
-        Connect instantly in over 200 destinations.
+        {t("connectInstantlyInOver200Destinations")}
       </p>
 
       <div className="inline-flex p-1.5 bg-slate-100 rounded-full mb-12 md:mb-16 shadow-inner">
@@ -86,7 +88,7 @@ const HomeProduct = () => {
               : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
           }`}
         >
-          Country List
+          {t("countryList")}
         </button>
         <button
           onClick={() => {
@@ -99,7 +101,7 @@ const HomeProduct = () => {
               : "bg-[#f0f0f0] text-[#8b8e96] hover:text-slate-800"
           }`}
         >
-          Regions
+          {t("regionList")}
         </button>
       </div>
 
@@ -154,7 +156,9 @@ const HomeProduct = () => {
               />
             </svg>
           </div>
-          <span className="ml-6 text-white text-2xl font-black">See More</span>
+          <span className="ml-6 text-white text-2xl font-black">
+            {t("seeMore")}
+          </span>
         </button>
       </div>
     </section>

+ 23 - 10
EsimLao/esim-vite/src/pages/home/components/HomeSearch.tsx

@@ -10,11 +10,13 @@ import { productApi } from "../../../apis/productApi";
 import { setAreas } from "../../../features/areas/areasSlice";
 import { Area } from "../../../services/product/type";
 import { useNavigate } from "react-router";
+import { useTranslation } from "react-i18next";
 
 const HomeSearch = () => {
   const areas = useSelector((state: any) => state.areas.areas);
   const dispatch = useDispatch();
   const navigate = useNavigate();
+  const { t } = useTranslation();
   const [searchQuery, setSearchQuery] = useState("");
   const [isDropdownOpen, setIsDropdownOpen] = useState(false);
   const dropdownRef = useRef<HTMLDivElement>(null);
@@ -67,12 +69,20 @@ const HomeSearch = () => {
   const handleSearch = (query: string) => {
     setSearchQuery(query);
     if (query.trim() === "") {
-      setAreasList(areas);
+      // set 10 most popular areas when input is empty
+      setAreasList(areas.slice(0, 10));
     } else {
-      const filtered = areas.filter((area: Area) =>
-        area.areaName1.toLowerCase().includes(query.toLowerCase())
-      );
-      setAreasList(filtered);
+      // set time 500ms to wait for user to stop typing
+      const timer = setTimeout(() => {
+        // logic sau khi user ngừng gõ
+        const filtered = areas.filter((area: Area) =>
+          area.coverageArea.toLowerCase().includes(query.toLowerCase())
+        );
+        setAreasList(filtered);
+      }, 500);
+      return () => {
+        clearTimeout(timer); // clear nếu user gõ tiếp
+      };
     }
   };
 
@@ -98,7 +108,7 @@ const HomeSearch = () => {
           <div className="w-full max-w-3xl relative group z-30">
             <input
               type="text"
-              placeholder="Choose country you're going to"
+              placeholder={t("chooseTo")}
               value={searchQuery}
               onChange={(e) => {
                 handleSearch(e.target.value);
@@ -128,7 +138,7 @@ const HomeSearch = () => {
               <div className="absolute top-full left-0 right-0 mt-3 bg-white rounded-[24px] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.15)] border border-slate-100 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
                 <div className="max-h-[400px] overflow-y-auto custom-scrollbar p-4">
                   <h3 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4 px-2 sticky top-0 bg-white z-10 py-2">
-                    Most Popular
+                    {t("mostPopular")}
                   </h3>
                   <div className="space-y-1">
                     {areasList.length > 0 ? (
@@ -152,7 +162,7 @@ const HomeSearch = () => {
                           </div>
                           <div className="text-right flex items-center space-x-2">
                             <span className="text-sm text-slate-400 font-medium">
-                              from:
+                              {t("from")}:
                             </span>
                             <span className="text-lg font-black text-[#EE0434]">
                               {p.minSellPrice.toLocaleString()} {p.curency}
@@ -162,7 +172,10 @@ const HomeSearch = () => {
                       ))
                     ) : (
                       <div className="text-center py-8 text-slate-400 font-medium">
-                        No countries found matching "{searchQuery}"
+                        {t("noCountryMatchesFound").replace(
+                          "{searchQuery}",
+                          searchQuery
+                        )}
                       </div>
                     )}
                   </div>
@@ -178,7 +191,7 @@ const HomeSearch = () => {
               dispatch(openCompatibilityModal());
             }}
           >
-            Does my phone support eSIM?
+            {t("doesEsimWorkWithMyDevice")}
           </a>
           {/* ,
           <button className="hover:text-[#EE0434] transition-colors">

+ 4 - 52
EsimLao/esim-vite/src/pages/home/components/HomeTestimonial.tsx

@@ -8,59 +8,11 @@ import {
 } from "../../../features/loading/loadingSlice";
 import { contentApi } from "../../../apis/contentApi";
 import { useAppDispatch } from "../../../hooks/useRedux";
+import { useTranslation } from "react-i18next";
 
 const HomeTestimonial = () => {
   const dispatch = useAppDispatch();
-  // const testimonials = [
-  //   {
-  //     name: "Quang Huy",
-  //     location: "Nghệ An",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=huy",
-  //     content:
-  //       "Trong suốt hành trình đi phượt bên Mỹ, Huy dò đường bằng Google Map phát trực tiếp từ sim data tốc độ cao mua sẵn ở Việt Nam tại shop infigate. Huy chỉ cần gắn sim vô là xài thôi, rất dễ dàng.",
-  //   },
-  //   {
-  //     name: "Yan Lin",
-  //     location: "Trung Quốc",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=yan",
-  //     content:
-  //       "不用下载APP,不用注册账号,只要插入SIM卡就能上网。对不太懂技术的人来说非常友好,客服还可以用中文沟通,太贴心了!",
-  //   },
-  //   {
-  //     name: "Hồng Mây",
-  //     location: "Hồ Chí Minh",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=may",
-  //     content:
-  //       "Dùng thích nên lần nào đi công tác mình cũng mua 😂 chất lượng dùng mạng bên TQ nhanh, vượt được hết tường lửa để truy cập zalo, fb thoải mái, giá rẻ mỗi tội không gọi được điện thôi.",
-  //   },
-  //   {
-  //     name: "Min-jun",
-  //     location: "Hàn Quốc",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=minjun",
-  //     content:
-  //       "현지 SIM 카드인데 데이터 무제한 요금제가 있어서 너무 좋아요. 가격도 저렴하고 속도도 빠름! 카카오톡, 인스타, 유튜브 다 문제없이 사용 중이에요.",
-  //   },
-  //   {
-  //     name: "David",
-  //     location: "Hoa Kỳ",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=david",
-  //     content:
-  //       "As a traveler from the U.S., I was worried about staying online during my trip. This SIM card solved everything. It was delivered to my hotel and activated in minutes.",
-  //   },
-  //   {
-  //     name: "Thùy Linh",
-  //     location: "Hà Nội",
-  //     rating: 5,
-  //     avatar: "https://i.pravatar.cc/150?u=linh",
-  //     content:
-  //       "Dịch vụ tuyệt vời ở Phuket, Thái Lan nhờ eSIM infigate. Kết nối mạnh ổn định, đủ để cả nhà xem phim, lướt web, sử dụng mạng xã hội một cách thoải mái.",
-  //   },
-  // ];
+  const { t } = useTranslation();
 
   const { data: loadReviewData = [] } = useQuery<Review[]>({
     queryKey: [DataCacheKey.REVIEWS],
@@ -92,10 +44,10 @@ const HomeTestimonial = () => {
         <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-center">
           <div className="lg:col-span-4 text-white space-y-4 md:space-y-6 text-center lg:text-left">
             <h2 className="text-2xl md:text-4xl lg:text-[36px] font-black leading-tight tracking-tight">
-              What our customers <br className="hidden lg:block" /> say about us
+              {t("whatUs")} <br className="hidden lg:block" /> {t("sayAboutUs")}
             </h2>
             <p className="text-sm md:text-xl font-medium text-white/80 max-w-sm mx-auto lg:mx-0">
-              Over 1,000,000 satisfied customers have used our services
+              {t("overMillionSatisfiedCustomers")}
             </p>
           </div>
 

+ 91 - 34
EsimLao/esim-vite/src/pages/login/LoginView.tsx

@@ -26,6 +26,7 @@ const LoginView: React.FC = () => {
   const inputRef = useRef<HTMLInputElement>(null);
   const [errorMessage, setErrorMessage] = useState("");
   let interval;
+
   useEffect(() => {
     // Focus input on mount
     inputRef.current?.focus();
@@ -158,6 +159,22 @@ const LoginView: React.FC = () => {
     getOtpMutation.mutate();
   };
 
+  const onGoogleLogin = async () => {
+    dispatch(startLoading({ message: "Redirecting to Google..." }));
+    try {
+      const res = await authApi.googleLogin();
+      if (res && res.errorCode === "0") {
+        window.location.href = res.data.url;
+      } else {
+        console.error("Google login failed:", res);
+        dispatch(stopLoading());
+      }
+    } catch (error) {
+      console.error("Google login error:", error);
+      dispatch(stopLoading());
+    }
+  };
+
   return (
     <div className="min-h-screen bg-white flex flex-col lg:flex-row overflow-hidden">
       <div className="w-full lg:w-1/2 flex flex-col justify-center items-center p-6 md:p-12 lg:p-20 relative z-10 bg-white">
@@ -208,41 +225,81 @@ const LoginView: React.FC = () => {
             </p>
           </div>
           {step === "email" && (
-            <form className="space-y-8" onSubmit={handleGetOtp}>
-              <div className="space-y-3">
-                <label className="block text-slate-500 font-black text-[10px] uppercase tracking-[0.2em] pl-1">
-                  Email Address
-                </label>
-                <input
-                  type="email"
-                  value={email}
-                  onChange={(e) => setEmail(e.target.value)}
-                  placeholder="Enter your email"
-                  className="w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-4 md:py-5 px-6 focus:outline-none focus:bg-white transition-all text-slate-700 font-semibold"
-                />
-              </div>
-              <div>
-                {errorMessage && (
-                  <p className="text-red-500 text-sm font-medium text-base md:text-lg text-center lg:text-left mb-4">
-                    {errorMessage}
-                  </p>
-                )}
+            <>
+              <form className="space-y-8" onSubmit={handleGetOtp}>
+                <div className="space-y-3">
+                  <label className="block text-slate-500 font-black text-[10px] uppercase tracking-[0.2em] pl-1">
+                    Email Address
+                  </label>
+                  <input
+                    type="email"
+                    value={email}
+                    onChange={(e) => setEmail(e.target.value)}
+                    placeholder="Enter your email"
+                    className="w-full bg-slate-50 border-2 border-transparent focus:border-[#EE0434]/20 rounded-2xl py-4 md:py-5 px-6 focus:outline-none focus:bg-white transition-all text-slate-700 font-semibold"
+                  />
+                </div>
+                <div>
+                  {errorMessage && (
+                    <p className="text-red-500 text-sm font-medium text-base md:text-lg text-center lg:text-left mb-4">
+                      {errorMessage}
+                    </p>
+                  )}
+                </div>
+                <button
+                  type="submit"
+                  disabled={loading.isSmallLoading}
+                  className={`w-full py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg transition-all flex items-center justify-center space-x-3 ${
+                    !loading.isSmallLoading
+                      ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
+                      : "bg-slate-100 text-slate-300 cursor-not-allowed"
+                  }`}
+                >
+                  {loading.isSmallLoading && (
+                    <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
+                  )}
+                  <span>Login</span>
+                </button>
+              </form>
+              <div className="space-y-6">
+                <div className="relative">
+                  <div className="absolute inset-0 flex items-center">
+                    <div className="w-full border-t border-slate-100"></div>
+                  </div>
+                  <div className="relative flex justify-center text-sm">
+                    <span className="px-4 bg-white text-slate-400 font-bold uppercase tracking-wider text-[10px]">
+                      Or continue with
+                    </span>
+                  </div>
+                </div>
+
+                <button
+                  type="button"
+                  onClick={onGoogleLogin}
+                  className="w-full bg-white border border-slate-200 text-slate-700 py-4 md:py-5 rounded-2xl font-bold text-lg md:text-xl shadow-sm hover:bg-slate-50 hover:border-slate-300 transition-all flex items-center justify-center space-x-3 active:scale-[0.98]"
+                >
+                  <svg className="w-6 h-6" viewBox="0 0 24 24">
+                    <path
+                      d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
+                      fill="#4285F4"
+                    />
+                    <path
+                      d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
+                      fill="#34A853"
+                    />
+                    <path
+                      d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
+                      fill="#FBBC05"
+                    />
+                    <path
+                      d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
+                      fill="#EA4335"
+                    />
+                  </svg>
+                  <span>Google</span>
+                </button>
               </div>
-              <button
-                type="submit"
-                disabled={loading.isSmallLoading}
-                className={`w-full py-4 md:py-5 rounded-2xl font-black text-xl md:text-2xl shadow-lg transition-all flex items-center justify-center space-x-3 ${
-                  !loading.isSmallLoading
-                    ? "bg-[#EE0434] text-white hover:scale-[1.01] active:scale-[0.98]"
-                    : "bg-slate-100 text-slate-300 cursor-not-allowed"
-                }`}
-              >
-                {loading.isSmallLoading && (
-                  <div className="w-5 h-5 border-3 border-white/30 border-t-red-500 rounded-full animate-spin"></div>
-                )}
-                <span>Login</span>
-              </button>
-            </form>
+            </>
           )}
           {step === "otp" && (
             <>

+ 6 - 3
EsimLao/esim-vite/src/pages/news/NewsDetailView.tsx

@@ -6,11 +6,14 @@ import { useAppDispatch } from "../../hooks/useRedux";
 import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
 import { articleApi } from "../../apis/articleApi";
 import { Article } from "../../services/article/types";
+import { useTranslation } from "react-i18next";
 
 const ArticleDetailView: React.FC = () => {
   const location = useLocation();
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
+  const { t } = useTranslation();
+
   const article = location.state?.article as Article;
   const [articleDetail, setArticleDetail] = useState<Article | null>(null);
 
@@ -61,7 +64,7 @@ const ArticleDetailView: React.FC = () => {
       <div className="max-w-7xl mx-auto px-4 py-4 md:py-6">
         <nav className="flex items-center flex-wrap gap-2 text-xs md:text-sm text-slate-500 font-medium">
           <Link to="/" className="hover:text-blue-500">
-            Home
+            {t("home")}
           </Link>
           <svg
             className="w-4 h-4"
@@ -77,7 +80,7 @@ const ArticleDetailView: React.FC = () => {
             />
           </svg>
           <Link to="/news" className="hover:text-blue-500">
-            News
+            {t("news")}
           </Link>
           <svg
             className="w-4 h-4"
@@ -165,7 +168,7 @@ const ArticleDetailView: React.FC = () => {
                   d="M15 19l-7-7 7-7"
                 />
               </svg>
-              <span>Back to News</span>
+              <span>{t("backToNews")}</span>
             </button>
           </div>
         </div>

+ 6 - 2
EsimLao/esim-vite/src/pages/news/NewsView.tsx

@@ -7,10 +7,12 @@ import { Article, Category } from "@/src/services/article/types";
 import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
 import { useAppDispatch } from "../../hooks/useRedux";
 import { articleApi } from "../../apis/articleApi";
+import { useTranslation } from "react-i18next";
 
 const NewsView: React.FC = () => {
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
+  const { t } = useTranslation();
 
   const { data: loadArticleData = [] } = useQuery<Article[]>({
     queryKey: [DataCacheKey.ARTICLES],
@@ -45,7 +47,7 @@ const NewsView: React.FC = () => {
       <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
         <nav className="flex items-center space-x-2 text-xs md:text-sm text-slate-500 font-medium">
           <Link to="/" className="hover:text-blue-500 text-[18px]">
-            Home
+            {t("home")}
           </Link>
           <svg
             className="w-4 h-4"
@@ -60,7 +62,9 @@ const NewsView: React.FC = () => {
               strokeLinejoin="round"
             />
           </svg>
-          <span className="text-slate-900 font-bold text-[18px]">News</span>
+          <span className="text-slate-900 font-bold text-[18px]">
+            {t("news")}
+          </span>
         </nav>
       </div>
 

+ 16 - 12
EsimLao/esim-vite/src/pages/order-detail/OrderDetailView.tsx

@@ -11,11 +11,14 @@ import React, { useState, useEffect } from "react";
 import { useLocation, useNavigate } from "react-router-dom";
 import { openQRModal } from "../../features/popup/popupSlice";
 import { formatNumber } from "../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
 
 const OrderDetailView = () => {
   const location = useLocation();
   const dispatch = useAppDispatch();
   const navigate = useNavigate();
+  const { t } = useTranslation();
+
   const [activeTab, setActiveTab] = useState<"detail" | "manage">("detail");
   const [orderDetails, setOrderDetails] = useState<OrderDetail[] | []>([]);
   const [dataUsage, setDataUsage] = useState<DataUsage[] | []>([]);
@@ -168,7 +171,7 @@ const OrderDetailView = () => {
             // onClick={() => onViewChange(ViewMode.HOME)}
             className="hover:text-[#EE0434] text-[16px]"
           >
-            Home
+            {t("home")}
           </button>
           <svg
             className="w-3 h-3"
@@ -187,7 +190,7 @@ const OrderDetailView = () => {
             // onClick={() => onViewChange(ViewMode.ORDER_HISTORY)}
             className="hover:text-[#EE0434] text-[16px] font-bold"
           >
-            My eSIM
+            {t("myEsim")}
           </button>
         </nav>
       </div>
@@ -211,7 +214,7 @@ const OrderDetailView = () => {
               d="M10 19l-7-7 7-7m8 14l-7-7 7-7"
             />
           </svg>
-          <span>Back</span>
+          <span>{t("back")}</span>
         </button>
 
         {/* Main Info Card */}
@@ -245,7 +248,7 @@ const OrderDetailView = () => {
                     state.orderHistory?.status
                   )}`}
                 >
-                  Success
+                  {t("paymentSuccessful")}
                 </span>
               ) : state.orderHistory?.status === 1 ? (
                 <span
@@ -253,7 +256,7 @@ const OrderDetailView = () => {
                     state.orderHistory?.status
                   )}`}
                 >
-                  Pending Payment
+                  {t("pendingPayment")}
                 </span>
               ) : (
                 <span
@@ -261,7 +264,7 @@ const OrderDetailView = () => {
                     state.orderHistory?.status
                   )}`}
                 >
-                  Failure
+                  {t("failure")}
                 </span>
               )}
             </div>
@@ -276,7 +279,7 @@ const OrderDetailView = () => {
                     : "text-[#EE0434] hover:bg-[#EE0434]/10"
                 }`}
               >
-                Detail
+                {t("detailDetail")}
               </button>
               <button
                 onClick={() => handleTabChange("manage")}
@@ -286,14 +289,15 @@ const OrderDetailView = () => {
                     : "text-[#EE0434] hover:bg-[#EE0434]/10"
                 }`}
               >
-                Manage
+                {t("manageManage")}
               </button>
             </div>
 
             {/* Divider with Total */}
             <div className="relative flex items-center justify-center border-t border-slate-100 py-8">
               <span className="bg-white px-4 text-lg md:text-xl font-bold text-slate-800 absolute">
-                Total: {formatNumber(state.orderHistory?.totalMoney)}{" "}
+                {t("totalTotal")}:{" "}
+                {formatNumber(state.orderHistory?.totalMoney)}{" "}
                 <span className="text-slate-500 font-normal">
                   ({state.orderHistory?.curency})
                 </span>
@@ -318,7 +322,7 @@ const OrderDetailView = () => {
                     />
                   </svg>
                   <span className="text-xs font-bold text-[16px]">
-                    Full name
+                    {t("fullName")}
                   </span>
                 </div>
                 <span className="text-slate-800 font-semibold text-sm text-[16px]">
@@ -342,7 +346,7 @@ const OrderDetailView = () => {
                     />
                   </svg>
                   <span className="text-xs font-bold text-[16px]">
-                    Phone number
+                    {t("phoneNumber")}
                   </span>
                 </div>
                 <span className="text-slate-800 font-semibold text-sm text-[16px]">
@@ -386,7 +390,7 @@ const OrderDetailView = () => {
                     />
                   </svg>
                   <span className="text-xs font-bold text-[16px]">
-                    Payment method
+                    {t("paymentMethod")}
                   </span>
                 </div>
                 <span className="text-slate-800 font-semibold text-sm text-[16px]">

+ 4 - 2
EsimLao/esim-vite/src/pages/order-history/OrderHistoryView.tsx

@@ -6,6 +6,7 @@ import { OrderHistory } from "../../services/product/type";
 import { useMutation } from "@tanstack/react-query";
 import React, { useState, useEffect } from "react";
 import { formatNumber } from "../../logic/loigicUtils";
+import { useTranslation } from "react-i18next";
 
 const OrderHistoryView = () => {
   const [searchOrder, setSearchOrder] = useState("");
@@ -14,6 +15,7 @@ const OrderHistoryView = () => {
   const [fromDate, setFromDate] = useState("");
   const [toDate, setToDate] = useState("");
   const [orders, setOrders] = useState<OrderHistory[]>([]);
+  const { t } = useTranslation();
 
   const dispatch = useAppDispatch();
   const navigate = useNavigate();
@@ -78,7 +80,7 @@ const OrderHistoryView = () => {
             // onClick={() => onViewChange(ViewMode.HOME)}
             className="hover:text-[#EE0434] text-[18px]"
           >
-            Home
+            {t("home")}
           </button>
           <svg
             className="w-3 h-3"
@@ -94,7 +96,7 @@ const OrderHistoryView = () => {
             />
           </svg>
           <span className="text-slate-900 font-bold text-[18px]">
-            Transaction History
+            {t("transactionHistory")}
           </span>
         </nav>
       </div>

+ 10 - 2
EsimLao/esim-vite/src/pages/product-detail/ProductDetailView.tsx

@@ -145,6 +145,9 @@ const ProductDetailView: React.FC = () => {
       }
     });
     setSelectedDays(day);
+    setSelectedData(
+      dataSet.has(selectedData) ? selectedData : Array.from(dataSet)[0]
+    );
     setDataActiveOptions(Array.from(dataSet));
   };
 
@@ -157,6 +160,9 @@ const ProductDetailView: React.FC = () => {
       }
     });
     setSelectedData(data);
+    setSelectedDays(
+      daysSet.has(selectedDays) ? selectedDays : Array.from(daysSet)[0]
+    );
     setDaysActiveOptions(Array.from(daysSet));
   };
 
@@ -314,7 +320,8 @@ const ProductDetailView: React.FC = () => {
                 <button
                   key={day}
                   onClick={() =>
-                    daysActiveOptions.includes(day) && handleSelectDay(day)
+                    // daysActiveOptions.includes(day) &&
+                    handleSelectDay(day)
                   }
                   className={`min-w-[50px] md:min-w-[70px] h-10 md:h-14 rounded-xl md:rounded-2xl font-bold text-base md:text-xl transition-all border-2 ${
                     selectedDays === day
@@ -339,7 +346,8 @@ const ProductDetailView: React.FC = () => {
                 <button
                   key={data}
                   onClick={() =>
-                    dataActiveOptions.includes(data) && handleSelectData(data)
+                    // dataActiveOptions.includes(data) &&
+                    handleSelectData(data)
                   }
                   className={`h-10 md:h-14 rounded-xl md:rounded-2xl font-bold text-sm md:text-xl transition-all border-2 ${
                     selectedData === data

+ 4 - 5
EsimLao/esim-vite/src/pages/support/SupportView.tsx

@@ -1,9 +1,10 @@
 import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
 import { Link } from "react-router-dom";
 
 const SupportView: React.FC = () => {
   const [activeItem, setActiveItem] = useState("What is travel eSIM/SIM?");
-
+  const { t } = useTranslation();
   const categories = [
     {
       title: "What is travel eSIM/SIM?",
@@ -127,12 +128,10 @@ const SupportView: React.FC = () => {
           </aside>
           <main className="lg:col-span-8">
             <h1 className="text-4xl md:text-5xl font-black text-[#EE0434] mb-10">
-              Guide & Help
+              {t("guideAndHelp")}
             </h1>
             <p className="text-lg leading-relaxed text-slate-600">
-              Select a topic from the left to view detailed instructions for
-              your travel SIM or eSIM. Our support team is also available 24/7
-              via chat.
+              {t("selectTopicInstructions")}
             </p>
           </main>
         </div>

+ 14 - 0
EsimLao/esim-vite/src/services/auth/types.ts

@@ -11,3 +11,17 @@ export interface AccountInfo {
 export interface RequestOtpResponse {
   expireInSeconds: number;
 }
+
+export interface GoogleLoginResponse {
+  url: string;
+}
+
+export interface GoogleCallbackResponse {
+  userId: number;
+  email: string;
+  fullName: string;
+  avatarUrl: string;
+  accessToken: string;
+  refreshToken: string;
+  expiresAt: string;
+}

+ 1 - 0
EsimLao/esim-vite/src/services/product/type.ts

@@ -12,6 +12,7 @@ export interface Area {
   minSellPrice: number;
   promotionPercent: number;
   curency: string;
+  coverageArea: string;
 }
 
 export interface Package {