Repository: Arquisoft/wichat_en3b
Pull request #151: Localization and added SignUp link
Author: login: Daniel-RodriguezFernandez
Labels: name: enhancement
description: New feature or request
Created: 2025-04-09T16:38:39Z | Merged: 2025-04-09T21:32:08Z

Added link to sign up in the navigation bar, created the backend for localization, translated landing page, login page and sign up page. The language can be changed dynamically, but the style might be changed, please tell me if the translations are adecuate. 


Diff:
diff --git a/package-lock.json b/package-lock.json
index c96bc2d1a7..c8481253d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,9 @@
   "packages": {
     "": {
       "dependencies": {
+        "i18next": "^24.2.3",
         "lucide-react": "^0.487.0",
+        "react-i18next": "^15.4.1",
         "recharts": "^2.15.1"
       },
       "devDependencies": {
@@ -2542,6 +2544,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "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/human-signals": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -2552,6 +2563,37 @@
         "node": ">=10.17.0"
       }
     },
+    "node_modules/i18next": {
+      "version": "24.2.3",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
+      "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
+      "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.26.10"
+      },
+      "peerDependencies": {
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/import-local": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@@ -4131,6 +4173,28 @@
         "react": "^19.1.0"
       }
     },
+    "node_modules/react-i18next": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
+      "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "html-parse-stringify": "^3.0.1"
+      },
+      "peerDependencies": {
+        "i18next": ">= 23.2.3",
+        "react": ">= 16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-is": {
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -4756,6 +4820,15 @@
         "d3-timer": "^3.0.1"
       }
     },
+    "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/walker": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
diff --git a/package.json b/package.json
index 616967a945..ebcfa8d3ba 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,9 @@
     "supertest": "^7.1.0"
   },
   "dependencies": {
+    "i18next": "^24.2.3",
     "lucide-react": "^0.487.0",
+    "react-i18next": "^15.4.1",
     "recharts": "^2.15.1"
   }
 }
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 3b8a076060..ac51bf076f 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -15,8 +15,10 @@
         "@testing-library/jest-dom": "^6.6.3",
         "@testing-library/react": "^16.2.0",
         "@testing-library/user-event": "^14.6.1",
+        "i18next": "^24.2.3",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
+        "react-i18next": "^15.4.1",
         "react-router": "^7.4.0",
         "react-router-dom": "^7.4.0",
         "react-scripts": "5.0.1",
@@ -36,7 +38,7 @@
         "jest-cucumber": "^4.5.0",
         "jest-environment-node": "^29.7.0",
         "mongodb-memory-server": "^10.1.4",
-        "puppeteer": "^24.4.0",
+        "puppeteer": "^24.6.0",
         "serve": "^14.2.4",
         "start-server-and-test": "^2.0.11",
         "util": "^0.12.5"
@@ -5968,9 +5970,9 @@
       "optional": true
     },
     "node_modules/bare-fs": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
-      "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
+      "integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
       "dev": true,
       "license": "Apache-2.0",
       "optional": true,
@@ -7998,9 +8000,9 @@
       "license": "MIT"
     },
     "node_modules/devtools-protocol": {
-      "version": "0.0.1413902",
-      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1413902.tgz",
-      "integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
+      "version": "0.0.1425554",
+      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1425554.tgz",
+      "integrity": "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw==",
       "dev": true,
       "license": "BSD-3-Clause"
     },
@@ -10559,6 +10561,15 @@
         "node": ">=12"
       }
     },
+    "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/html-webpack-plugin": {
       "version": "5.6.3",
       "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz",
@@ -10713,6 +10724,37 @@
         "node": ">=10.17.0"
       }
     },
+    "node_modules/i18next": {
+      "version": "24.2.3",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
+      "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
+      "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.26.10"
+      },
+      "peerDependencies": {
+        "typescript": "^5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/iconv-lite": {
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -16601,9 +16643,9 @@
       }
     },
     "node_modules/puppeteer": {
-      "version": "24.5.0",
-      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.5.0.tgz",
-      "integrity": "sha512-3m0B48gj1A8cK01ma49WwjE8mg4i9UmnR2lP64rwBiLacJ2V20FpT67MgSUyzfz9BcHMQQweuF6Q854mnIYTqg==",
+      "version": "24.6.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.6.0.tgz",
+      "integrity": "sha512-wYTB8WkzAr7acrlsp+0at1PZjOJPOxe6dDWKOG/kaX4Zjck9RXCFx3CtsxsAGzPn/Yv6AzgJC/CW1P5l+qxsqw==",
       "dev": true,
       "hasInstallScript": true,
       "license": "Apache-2.0",
@@ -16611,8 +16653,8 @@
         "@puppeteer/browsers": "2.9.0",
         "chromium-bidi": "3.0.0",
         "cosmiconfig": "^9.0.0",
-        "devtools-protocol": "0.0.1413902",
-        "puppeteer-core": "24.5.0",
+        "devtools-protocol": "0.0.1425554",
+        "puppeteer-core": "24.6.0",
         "typed-query-selector": "^2.12.0"
       },
       "bin": {
@@ -16623,16 +16665,16 @@
       }
     },
     "node_modules/puppeteer-core": {
-      "version": "24.5.0",
-      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.5.0.tgz",
-      "integrity": "sha512-vqibSk7xGOoqOlPUk3H+Iz02b4jCEd5QxaiuXclqyyBrJ6ZK22mXkg9HBSpyZePq6vKWh5ZAqUilSnbF2bv4Jg==",
+      "version": "24.6.0",
+      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.6.0.tgz",
+      "integrity": "sha512-Cukxysy12m0v350bhl/Gzof0XQYmtON9l2VvGp3D4BOQZVgyf+y5wIpcjDZQ/896Okoi95dKRGRV8E6a7SYAQQ==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
         "@puppeteer/browsers": "2.9.0",
         "chromium-bidi": "3.0.0",
         "debug": "^4.4.0",
-        "devtools-protocol": "0.0.1413902",
+        "devtools-protocol": "0.0.1425554",
         "typed-query-selector": "^2.12.0",
         "ws": "^8.18.1"
       },
@@ -16986,6 +17028,28 @@
       "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
       "license": "MIT"
     },
+    "node_modules/react-i18next": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
+      "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "html-parse-stringify": "^3.0.1"
+      },
+      "peerDependencies": {
+        "i18next": ">= 23.2.3",
+        "react": ">= 16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-is": {
       "version": "19.1.0",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
@@ -21294,9 +21358,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "4.9.5",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "license": "Apache-2.0",
       "peer": true,
       "bin": {
@@ -21304,7 +21368,7 @@
         "tsserver": "bin/tsserver"
       },
       "engines": {
-        "node": ">=4.2.0"
+        "node": ">=14.17"
       }
     },
     "node_modules/unbox-primitive": {
@@ -21593,6 +21657,15 @@
         "d3-timer": "^3.0.1"
       }
     },
+    "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/w3c-hr-time": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/webapp/package.json b/webapp/package.json
index f3259cd584..d4805bc783 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -17,7 +17,9 @@
     "react-scripts": "5.0.1",
     "react-simple-typewriter": "^5.0.1",
     "recharts": "^2.15.1",
-    "web-vitals": "^4.2.4"
+    "web-vitals": "^4.2.4",
+    "i18next": "^24.2.3", 
+    "react-i18next": "^15.4.1"
   },
   "scripts": {
     "start": "react-scripts start",
@@ -57,13 +59,15 @@
     "jest-cucumber": "^4.5.0",
     "jest-environment-node": "^29.7.0",
     "mongodb-memory-server": "^10.1.4",
-    "puppeteer": "^24.4.0",
+    "puppeteer": "^24.6.0",
     "serve": "^14.2.4",
     "start-server-and-test": "^2.0.11",
     "util": "^0.12.5"
   },
   "overrides": {
     "nth-check": ">=2.1.1",
-    "postcss": ">=8.4.31"
+    "postcss": ">=8.4.31",
+    "typescript": "5.8.3"
   }
+  
 }
diff --git a/webapp/src/App.js b/webapp/src/App.js
index 49b5db4001..5701fcae3d 100644
--- a/webapp/src/App.js
+++ b/webapp/src/App.js
@@ -4,6 +4,7 @@ import { Container, CssBaseline, Typography, Button, Box, Paper, Grid } from "@m
 import { styled } from "@mui/material/styles"
 import { Outlet, NavLink } from "react-router"
 import { School, Login } from "@mui/icons-material"
+import { useTranslation } from "react-i18next"
 
 const StyledPaper = styled(Paper)(({ theme }) => ({
   padding: theme.spacing(4),
@@ -58,6 +59,7 @@ const StyledNavLink = styled(NavLink)({
 })
 
 function App() {
+  const {t} = useTranslation();
   return (
     <Container maxWidth="sm">
       <CssBaseline />
@@ -78,7 +80,7 @@ function App() {
             WICHAT
           </Typography>
           <Typography variant="h5" color="textSecondary" gutterBottom>
-            Welcome to the 2025 edition of the Software Architecture course
+            {t("welcomeMsg")}
           </Typography>
         </Box>
         
@@ -86,7 +88,7 @@ function App() {
           <Grid item xs={12} sx={{ display: "flex", justifyContent: "center" }}>
             <StyledNavLink to="/login">
               <LoginButton variant="contained" startIcon={<Login />}>
-                LOGIN
+                {t("login")}
               </LoginButton>
             </StyledNavLink>
           </Grid>
diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js
index bf72009363..a7359f1239 100644
--- a/webapp/src/App.test.js
+++ b/webapp/src/App.test.js
@@ -1,7 +1,9 @@
 import React from "react";
-import { render, screen } from "@testing-library/react";
-import { MemoryRouter } from "react-router";
-import App from "./App";
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router';
+import App from './App';
+import { I18nextProvider } from "react-i18next";
+import i18n from "./utils/i18n";
 
 // Global mock for Material UI icons using a Proxy to return a simple component
 jest.mock("@mui/icons-material", () => {
@@ -10,12 +12,14 @@ jest.mock("@mui/icons-material", () => {
   });
 });
 
-test("renders welcome message", () => {
+test('renders welcome message', () => {
   render(
+    <I18nextProvider i18n={i18n}>
     <MemoryRouter>
       <App />
     </MemoryRouter>
+    </I18nextProvider>
   );
-  const welcomeMessage = screen.getByText(/Welcome to the 2025 edition of the Software Architecture course/i);
+  const welcomeMessage = screen.getByText(i18n.t("welcomeMsg"));
   expect(welcomeMessage).toBeInTheDocument();
 });
diff --git a/webapp/src/components/AddUser.js b/webapp/src/components/AddUser.js
index 56f927b2b5..4dc5c01e6d 100644
--- a/webapp/src/components/AddUser.js
+++ b/webapp/src/components/AddUser.js
@@ -1,7 +1,7 @@
 // src/components/AddUser.js
 import React, { useState } from 'react';
-import axios from '../api/axios';
-
+import axios from '../utils/axios';
+import {useTranslation} from 'react-i18next';
 
 import { Container, Typography, TextField, Button, Snackbar, Box, Paper, Alert, FormHelperText } from '@mui/material';
 import { NavLink, useNavigate } from 'react-router';
@@ -15,6 +15,36 @@ const AddUser = () => {
   const [passwordError, setPasswordError] = useState('');
   const [openSnackbar, setOpenSnackbar] = useState(false);
 
+  const {t} = useTranslation();
+
+  const labels = {
+    createAccount: "createAccount",
+    username: "username",
+    password: "password",
+    passwordReq: "passwordReq",
+    signUp: "signUp",
+    alreadyAccount: "alreadyAccount",
+    loginHere: "loginHere",
+    registerTxtBubble: "registerTxtBubble",
+  };
+
+  const CustomTextField = ({ name, labelKey, value, onChange, error, helperText }) => {
+
+    return (
+      <TextField
+        name={name}
+        margin="normal"
+        fullWidth
+        label={t(labelKey)}
+        value={value}
+        onChange={onChange}
+        error={!!error}
+        helperText={helperText}
+        sx={{ mb: 2 }}
+    />
+  );
+};
+
   const navigate = useNavigate();
 
   const addUser = async () => {
@@ -53,48 +83,40 @@ const AddUser = () => {
   const handleCloseSnackbar = () => {
     setOpenSnackbar(false);
   };
-
   return (
     <Container maxWidth="md" sx={{ mt: 6 }}>
       <Paper elevation={4} sx={{ display: 'flex', borderRadius: 4, overflow: 'hidden' }}>
         {/* Left Panel - Add User Form */}
         <Box sx={{ width: '50%', p: 4, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
           <Typography component="h1" variant="h5" textAlign="center" fontWeight="bold" mb={3}>
-            Register here
+            {t(labels.createAccount)}
           </Typography>
 
-          <TextField
+          <CustomTextField
             name="username"
-            margin="normal"
-            fullWidth
-            label="Username"
+            labelKey={labels.username}
             value={username}
             onChange={(e) => {
               setUsername(e.target.value);
               setUsernameError('');
             }}
-            error={!!usernameError}
+            error={usernameError}
             helperText={usernameError}
-            sx={{ mb: 2 }}
           />
-          <TextField
+          <CustomTextField
             name="password"
-            margin="normal"
-            fullWidth
-            label="Password"
-            type="password"
+            labelKey={labels.password}
             value={password}
             onChange={(e) => {
               setPassword(e.target.value);
               setPasswordError('');
             }}
-            error={!!passwordError}
+            error={passwordError}
             helperText={passwordError}
-            sx={{ mb: 2 }}
           />
 
           <FormHelperText sx={{ mb: 2, mx: 1 }}>
-            Password must contain at least 8 characters, including a capital letter, a number, and a special character.
+            {t(labels.passwordReq)}
           </FormHelperText>
 
           <Button
@@ -105,18 +127,18 @@ const AddUser = () => {
             sx={{
               mt: 1,
               fontWeight: 'bold',
-              backgroundColor: '#5254bc', 
+              backgroundColor: '#5254bc',
               '&:hover': {
                 backgroundColor: '#3f47a3',
               },
             }}
           >
-            Add User
+            {t(labels.signUp)}
           </Button>
 
           <Typography component="div" align="center" sx={{ marginTop: 3 }}>
             <NavLink to={"/login"}>
-              Already have an account? Login here.
+              {t(labels.alreadyAccount)} <strong>{t(labels.loginHere)}</strong>
             </NavLink>
           </Typography>
         </Box>
@@ -137,7 +159,7 @@ const AddUser = () => {
           {/* Placeholder for animation or image */}
           <Box sx={{ mb: 3 }}>
             <img
-              src={logInPic} 
+              src={logInPic}
               alt="Presenter"
               style={{
                 maxWidth: '100%',
@@ -162,7 +184,7 @@ const AddUser = () => {
               maxWidth: '100%',
             }}
           >
-            Nice to see you are on board! Fill in the details and add a user to get started!
+            {t(labels.registerTxtBubble)}
           </Box>
         </Box>
       </Paper>
@@ -175,9 +197,12 @@ const AddUser = () => {
       )}
 
       {/* Success Notification */}
-      <Snackbar open={openSnackbar} autoHideDuration={6000} onClose={handleCloseSnackbar} message="User added successfully" />
-
-
+      <Snackbar
+        open={openSnackbar}
+        autoHideDuration={6000}
+        onClose={handleCloseSnackbar}
+        message={t("userAddedSuccess")}
+      />
     </Container>
   );
 };
diff --git a/webapp/src/components/AddUser.test.js b/webapp/src/components/AddUser.test.js
index d130bdef6d..5aa8bf6e5e 100644
--- a/webapp/src/components/AddUser.test.js
+++ b/webapp/src/components/AddUser.test.js
@@ -1,27 +1,40 @@
 import React from 'react';
 import { render, fireEvent, screen, waitFor } from '@testing-library/react';
 import { MemoryRouter } from 'react-router';
-import axios from '../api/axios';
+import axios from '../utils/axios';
 import MockAdapter from 'axios-mock-adapter';
 import AddUser from './AddUser';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../utils/i18n";
 
 const mockAxios = new MockAdapter(axios);
 
 describe('AddUser component', () => {
+  const renderLayout = () => {
+    render(
+      <I18nextProvider i18n={i18n}>
+        <MemoryRouter>
+          <AddUser />
+        </MemoryRouter>
+      </I18nextProvider>
+    );
+  };
+
+  const getInputsAndButton = () => {
+    const usernameInput = screen.getByLabelText(i18n.t("username"));
+    const passwordInput = screen.getByLabelText(i18n.t("password"));
+    const addUserButton = screen.getByRole('button', { name: i18n.t("signUp") });
+    return { usernameInput, passwordInput, addUserButton };
+  };
+
   beforeEach(() => {
     mockAxios.reset();
   });
 
   it('should add user successfully', async () => {
-    render(
-      <MemoryRouter>
-        <AddUser />
-      </MemoryRouter>
-    );
+    renderLayout();
 
-    const usernameInput = screen.getByLabelText(/Username/i);
-    const passwordInput = screen.getByLabelText(/Password/i);
-    const addUserButton = screen.getByRole('button', { name: /Add User/i });
+    const { usernameInput, passwordInput, addUserButton } = getInputsAndButton();
 
     mockAxios.onPost("/adduser").reply(200);
 
@@ -31,20 +44,14 @@ describe('AddUser component', () => {
     fireEvent.click(addUserButton);
 
     await waitFor(() => {
-      expect(screen.getByText(/User added successfully/i)).toBeInTheDocument();
+      expect(screen.getByText(i18n.t("userAddedSuccess"))).toBeInTheDocument();
     });
   });
 
   it('should handle error when adding user', async () => {
-    render(
-      <MemoryRouter>
-        <AddUser />
-      </MemoryRouter>
-    );
+    renderLayout();
 
-    const usernameInput = screen.getByLabelText(/Username/i);
-    const passwordInput = screen.getByLabelText(/Password/i);
-    const addUserButton = screen.getByRole('button', { name: /Add User/i });
+    const { usernameInput, passwordInput, addUserButton } = getInputsAndButton();
 
     mockAxios.onPost("/adduser").reply(500, { error: 'Internal Server Error' });
 
@@ -59,4 +66,4 @@ describe('AddUser component', () => {
       ).toBeInTheDocument();
     });
   });
-});
+});
\ No newline at end of file
diff --git a/webapp/src/components/LanguageSelect.js b/webapp/src/components/LanguageSelect.js
new file mode 100644
index 0000000000..dfdd2c9ac6
--- /dev/null
+++ b/webapp/src/components/LanguageSelect.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
+
+const LanguageSelect = () => {
+  const { i18n } = useTranslation();
+
+  const handleChange = (event) => {
+    const selectedLanguage = event.target.value;
+    i18n.changeLanguage(selectedLanguage); // This triggers the language change
+  };
+
+  return (
+    <FormControl variant="outlined" size="small" style={{ minWidth: 120 }}>
+      <InputLabel id="language-select-label">Language</InputLabel>
+      <Select
+        labelId="language-select-label"
+        id="language-select"
+        value={i18n.language} // Current language
+        onChange={handleChange}
+        label="Language"
+      >
+        <MenuItem value="en">English</MenuItem>
+        <MenuItem value="es">Español</MenuItem>
+      </Select>
+    </FormControl>
+  );
+};
+
+export default LanguageSelect;
\ No newline at end of file
diff --git a/webapp/src/components/Layout.js b/webapp/src/components/Layout.js
index 33b75f196f..baf7b31cf4 100644
--- a/webapp/src/components/Layout.js
+++ b/webapp/src/components/Layout.js
@@ -2,7 +2,9 @@ import { AppBar, Box, Button, Container } from "@mui/material";
 import HomeIcon from '@mui/icons-material/Home';
 import { Outlet, NavLink } from 'react-router';
 import useAuth from "../hooks/useAuth";
-import axios from "../api/axios";
+import axios from "../utils/axios";
+import { useTranslation } from "react-i18next";
+import LanguageSelect from "./LanguageSelect";
 
 const StyledNavlink = ({ to, label, icon }) => {
     return (
@@ -27,6 +29,8 @@ const Layout = () => {
         }
     }
 
+    const  { t } = useTranslation();
+
     return (
         <Container component="main" disableGutters sx={{
             minWidth: "100vw",
@@ -40,16 +44,29 @@ const Layout = () => {
                 padding: "0.5rem",
                 background: "linear-gradient(to right,rgb(70, 80, 180),rgb(100, 90, 200))"
             }}>
-                <StyledNavlink to="/home" label="Home" icon={<HomeIcon />} />
+                <StyledNavlink to="/home" label={t("home")} icon={<HomeIcon />} />
+                <LanguageSelect />
                 <Box sx={{ ml: "auto" }}>
-                    <>
-                        {auth.username
-                            ? <Button onClick={handleLogout} sx={{ color: "white", gap: "0.5rem", "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" } }}>
-                                Log Out
-                            </Button>
-                            : <StyledNavlink to="/login" label="Login" />}
-                    </>
-                </Box>
+                <>
+                {auth.username ? (
+                <Button
+                    onClick={handleLogout}
+                    sx={{
+                    color: "white",
+                    gap: "0.5rem",
+                    "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
+                    }}
+                >
+                    {t("logout")}
+                </Button>
+                ) : (
+                <>
+                    <StyledNavlink to="/login" label={t("login")} />
+                    <StyledNavlink to="/signup" label={t("signUp")} /> 
+                </>
+                )}
+            </>
+            </Box>
             </AppBar>
 
             <Outlet />
diff --git a/webapp/src/components/Layout.test.js b/webapp/src/components/Layout.test.js
index eca33280e0..42d93189ad 100644
--- a/webapp/src/components/Layout.test.js
+++ b/webapp/src/components/Layout.test.js
@@ -3,61 +3,67 @@ import userEvent from "@testing-library/user-event";
 import { MemoryRouter } from "react-router";
 import Layout from "./Layout";
 import useAuth from "../hooks/useAuth";
-import axios from "../api/axios";
+import axios from "../utils/axios";
+import { I18nextProvider } from "react-i18next";
+import i18n from "../utils/i18n";
 
 jest.mock("../hooks/useAuth");
-jest.mock("../api/axios", () => ({
+jest.mock("../utils/axios", () => ({
   post: jest.fn(),
 }));
 
 describe("Layout Component", () => {
-  test("renders Layout component correctly", () => {
-    useAuth.mockReturnValue({ auth: {} });
+  const renderLayout = () => {
     render(
-      <MemoryRouter>
-        <Layout />
-      </MemoryRouter>
+      <I18nextProvider i18n={i18n}>
+        <MemoryRouter>
+          <Layout />
+        </MemoryRouter>
+      </I18nextProvider>
     );
-    
-    expect(screen.getByText("Home")).toBeInTheDocument();
+  };
+
+  const mockAuth = (authState) => {
+    useAuth.mockReturnValue(authState);
+  };
+
+  const getButtonByText = (key) => screen.getByText(i18n.t(key));
+
+  beforeEach(() => {
+    jest.clearAllMocks(); // Clear mocks before each test
   });
 
-  test("shows Login button when user is not authenticated", () => {
-    useAuth.mockReturnValue({ auth: {} });
-    render(
-      <MemoryRouter>
-        <Layout />
-      </MemoryRouter>
-    );
-    
-    expect(screen.getByText("Login")).toBeInTheDocument();
+  test("renders Layout component correctly", () => {
+    mockAuth({ auth: {} });
+    renderLayout();
+
+    expect(getButtonByText("home")).toBeInTheDocument();
+  });
+
+  test("shows Login and Sign up buttons when user is not authenticated", () => {
+    mockAuth({ auth: {} });
+    renderLayout();
+
+    expect(getButtonByText("login")).toBeInTheDocument();
+    expect(getButtonByText("signUp")).toBeInTheDocument();
   });
 
   test("shows Log Out button when user is authenticated", () => {
-    useAuth.mockReturnValue({ auth: { username: "testuser" } });
-    render(
-      <MemoryRouter>
-        <Layout />
-      </MemoryRouter>
-    );
-    
-    expect(screen.getByText("Log Out")).toBeInTheDocument();
+    mockAuth({ auth: { username: "testuser" } });
+    renderLayout();
+
+    expect(getButtonByText("logout")).toBeInTheDocument();
   });
 
   test("calls logout API when Log Out button is clicked", async () => {
     const setAuthMock = jest.fn();
-    useAuth.mockReturnValue({ auth: { username: "testuser" }, setAuth: setAuthMock });
+    mockAuth({ auth: { username: "testuser" }, setAuth: setAuthMock });
     axios.post.mockResolvedValue({});
-    
-    render(
-      <MemoryRouter>
-        <Layout />
-      </MemoryRouter>
-    );
-    
-    const logoutButton = screen.getByText("Log Out");
+    renderLayout();
+
+    const logoutButton = getButtonByText("logout");
     await userEvent.click(logoutButton);
-    
+
     expect(axios.post).toHaveBeenCalledWith("/logout", {}, { withCredentials: true });
     expect(setAuthMock).toHaveBeenCalledWith({});
   });
@@ -65,21 +71,17 @@ describe("Layout Component", () => {
   test("logs an error to the console when the logout API fails", async () => {
     const setAuthMock = jest.fn();
     const errorMock = new Error("Logout failed");
-    useAuth.mockReturnValue({ auth: { username: "testuser" }, setAuth: setAuthMock });
-    axios.post.mockRejectedValue(errorMock); 
-  
-    const consoleErrorMock = jest.spyOn(console, "error").mockImplementation(() => {}); 
-  
-    render(
-      <MemoryRouter>
-        <Layout />
-      </MemoryRouter>
-    );
-  
-    const logoutButton = screen.getByText("Log Out");
+    mockAuth({ auth: { username: "testuser" }, setAuth: setAuthMock });
+    axios.post.mockRejectedValue(errorMock);
+
+    const consoleErrorMock = jest.spyOn(console, "error").mockImplementation(() => {});
+
+    renderLayout();
+
+    const logoutButton = getButtonByText("logout");
     await userEvent.click(logoutButton);
-  
-    expect(consoleErrorMock).toHaveBeenCalledWith(errorMock); 
+
+    expect(consoleErrorMock).toHaveBeenCalledWith(errorMock);
     consoleErrorMock.mockRestore();
   });
-});
+});
\ No newline at end of file
diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js
index 18644e07bc..471eeee785 100644
--- a/webapp/src/components/Login.js
+++ b/webapp/src/components/Login.js
@@ -1,11 +1,11 @@
-// src/components/Login.js
 import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 import { Container, Typography, TextField, Button, Snackbar, Checkbox, FormControlLabel, Box, Paper } from '@mui/material';
 import { NavLink, useLocation, useNavigate } from 'react-router';
 import logInPic from './photos/logInPic.png';
 
 import useAuth from "../hooks/useAuth";
-import axios from "../api/axios";
+import axios from "../utils/axios";
 
 const Login = () => {
   const { setAuth, persist, setPersist } = useAuth();
@@ -17,12 +17,38 @@ const Login = () => {
   const [error, setError] = useState('');
   const [openSnackbar, setOpenSnackbar] = useState(false);
 
+  const { t } = useTranslation();
+
+  // Centralized labels for consistency
+  const labels = {
+    loginStartMsg: "loginStartMsg",
+    username: "username",
+    password: "password",
+    rememberMe: "rememberMe",
+    login: "login",
+    noAccount: "noAccount",
+    registerHere: "registerHere",
+    loginQuestion: "loginQuestion",
+  };
+
+  // Reusable CustomTextField component
+  const CustomTextField = ({ name, labelKey, value, onChange, type = "text" }) => (
+    <TextField
+      name={name}
+      margin="normal"
+      fullWidth
+      label={t(labelKey)}
+      value={value}
+      onChange={onChange}
+      type={type}
+      sx={{ mb: 2 }}
+    />
+  );
+
   const loginUser = async () => {
     try {
       const response = await axios.post("/login", { username, password }, { withCredentials: true });
-      // store username and token in memory, where it is safest
       setAuth({ username, accessToken: response.data.accessToken });
-      // redirect to the page the user was trying to access before logging in
       navigate(from, { replace: true });
     } catch (error) {
       const errorMessage = error.response?.data?.error || "Unknown error";
@@ -45,39 +71,32 @@ const Login = () => {
     localStorage.setItem("persist", persist);
   }, [persist]);
 
-
   return (
     <Container maxWidth="md" sx={{ mt: 6 }}>
       <Paper elevation={4} sx={{ display: 'flex', borderRadius: 4, overflow: 'hidden' }}>
         {/* Left Panel - Login Form */}
         <Box sx={{ width: '50%', p: 4, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
           <Typography component="h1" variant="h5" textAlign="center" fontWeight="bold" mb={3}>
-            Login to Start Playing
+            {t(labels.loginStartMsg)}
           </Typography>
 
-          <TextField
+          <CustomTextField
             name="username"
-            margin="normal"
-            fullWidth
-            label="Username"
+            labelKey={labels.username}
             value={username}
             onChange={(e) => setUsername(e.target.value)}
-            sx={{ mb: 2 }}
           />
-          <TextField
+          <CustomTextField
             name="password"
-            margin="normal"
-            fullWidth
-            label="Password"
-            type="password"
+            labelKey={labels.password}
             value={password}
             onChange={(e) => setPassword(e.target.value)}
-            sx={{ mb: 2 }}
+            type="password"
           />
 
           <FormControlLabel
             control={<Checkbox onChange={togglePersist} checked={persist} />}
-            label="Remember me"
+            label={t(labels.rememberMe)}
             sx={{ mb: 2 }}
           />
 
@@ -89,18 +108,18 @@ const Login = () => {
             sx={{
               mt: 1,
               fontWeight: 'bold',
-              backgroundColor: '#5254bc', 
+              backgroundColor: '#5254bc',
               '&:hover': {
-                backgroundColor: '#3f47a3', 
+                backgroundColor: '#3f47a3',
               },
             }}
           >
-            🔓 Login
+            🔓 {t(labels.login)}
           </Button>
 
           <Typography component="div" align="center" sx={{ marginTop: 3 }}>
             <NavLink to="/signup">
-              Don’t have an account? <strong>Register here.</strong>
+              {t(labels.noAccount)} <strong>{t(labels.registerHere)}</strong>
             </NavLink>
           </Typography>
         </Box>
@@ -115,19 +134,19 @@ const Login = () => {
             flexDirection: 'column',
             justifyContent: 'center',
             alignItems: 'center',
-            p: 3
+            p: 3,
           }}
         >
           {/* Placeholder for animation */}
-          <Box sx={{ mb: 3}}>
+          <Box sx={{ mb: 3 }}>
             <img
-              src={logInPic} 
+              src={logInPic}
               alt="Presenter"
               style={{
                 maxWidth: '100%',
                 maxHeight: '100%',
                 objectFit: 'contain',
-                borderRadius: '20px'
+                borderRadius: '20px',
               }}
             />
           </Box>
@@ -143,10 +162,10 @@ const Login = () => {
               fontWeight: 500,
               boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
               textAlign: 'center',
-              maxWidth: '100%'
+              maxWidth: '100%',
             }}
           >
-            Ready to test your knowledge? Log in and let's go!
+            {t(labels.loginQuestion)}
           </Box>
         </Box>
       </Paper>
@@ -160,4 +179,4 @@ const Login = () => {
   );
 };
 
-export default Login;
+export default Login;
\ No newline at end of file
diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js
index 8c391f18fd..9e94b2c8d3 100644
--- a/webapp/src/components/Login.test.js
+++ b/webapp/src/components/Login.test.js
@@ -4,7 +4,9 @@ import { MemoryRouter } from 'react-router';
 import axios from 'axios';
 import MockAdapter from 'axios-mock-adapter';
 import Login from './Login';
-import useAuth from '../hooks/useAuth';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../utils/i18n";
+
 
 const mockAxios = new MockAdapter(axios);
 
@@ -18,6 +20,22 @@ jest.mock('../hooks/useAuth', () => ({
 }));
 
 describe('Login component', () => {
+  const renderLayout = () => {
+    render(
+      <I18nextProvider i18n={i18n}>
+        <MemoryRouter>
+          <Login />
+        </MemoryRouter>
+      </I18nextProvider>
+    );
+  };
+  const getInputsAndButton = () => {
+    const usernameInput = screen.getByLabelText(i18n.t("username"));
+    const passwordInput = screen.getByLabelText(i18n.t("password"));
+    const loginButton = screen.getByTestId('login-submit');
+    return { usernameInput, passwordInput, loginButton };
+  };
+
   beforeAll(() => {
     jest.spyOn(console, "error").mockImplementation(() => {});
   });
@@ -27,15 +45,8 @@ describe('Login component', () => {
   });
 
   it('should log in successfully', async () => {
-    render(
-      <MemoryRouter>
-        <Login />
-      </MemoryRouter>
-    );
-
-    const usernameInput = screen.getByLabelText(/Username/i);
-    const passwordInput = screen.getByLabelText(/Password/i);
-    const loginButton = screen.getByRole('button', { name: /Login/i });
+    renderLayout();
+    const {usernameInput, passwordInput, loginButton} = getInputsAndButton();
 
     mockAxios.onPost('/login').reply(200, { accessToken: 'fakeAccessToken' });
 
@@ -51,15 +62,9 @@ describe('Login component', () => {
   });
 
   it('should handle error when logging in', async () => {
-    render(
-      <MemoryRouter>
-        <Login />
-      </MemoryRouter>
-    );
+    renderLayout();
   
-    const usernameInput = screen.getByLabelText(/Username/i);
-    const passwordInput = screen.getByLabelText(/Password/i);
-    const loginButton = screen.getByRole('button', { name: /Login/i });
+    const {usernameInput, passwordInput, loginButton} = getInputsAndButton();
   
     mockAxios.onPost('/login').replyOnce(401, { error: 'Unauthorized' });
   
diff --git a/webapp/src/hooks/useAxios.js b/webapp/src/hooks/useAxios.js
index b27d999526..7cd38024c6 100644
--- a/webapp/src/hooks/useAxios.js
+++ b/webapp/src/hooks/useAxios.js
@@ -1,5 +1,5 @@
 import { useEffect } from "react";
-import { axiosPrivate } from "../api/axios";
+import { axiosPrivate } from "../utils/axios";
 import useRefreshToken from "./useRefreshToken";
 import useAuth from "./useAuth";
 
@@ -41,7 +41,7 @@ const useAxios = () => {
             axiosPrivate.interceptors.request.eject(requestIntercept);
             axiosPrivate.interceptors.response.eject(responseIntercept);
         }
-    }, [auth, refresh]);
+    }, [auth, refresh, setAuth]);
 
     return axiosPrivate;
 }
diff --git a/webapp/src/hooks/useRefreshToken.js b/webapp/src/hooks/useRefreshToken.js
index d6b42902ec..c9f565a4db 100644
--- a/webapp/src/hooks/useRefreshToken.js
+++ b/webapp/src/hooks/useRefreshToken.js
@@ -1,4 +1,4 @@
-import axios from "../api/axios";
+import axios from "../utils/axios";
 import useAuth from "./useAuth";
 
 /**
diff --git a/webapp/src/index.js b/webapp/src/index.js
index de8214f9de..211658555a 100644
--- a/webapp/src/index.js
+++ b/webapp/src/index.js
@@ -1,8 +1,9 @@
+import './utils/i18n';
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 import { BrowserRouter, Routes, Route } from 'react-router-dom';
 import reportWebVitals from './reportWebVitals';
-import styles from './index.css';
+
 
 import { AuthProvider } from './context/AuthContext';
 import PrivateRoute from './components/PrivateRoute';
@@ -17,6 +18,7 @@ import Layout from './components/Layout';
 import GameTopicSelection from './components/GameTopicSelection';
 import PersistentLogin from './components/PersistentLogin';
 
+
 const root = ReactDOM.createRoot(document.getElementById('root'));
 root.render(
   <BrowserRouter>
diff --git a/webapp/src/locales/translation_en.json b/webapp/src/locales/translation_en.json
new file mode 100644
index 0000000000..74d69c1f82
--- /dev/null
+++ b/webapp/src/locales/translation_en.json
@@ -0,0 +1,22 @@
+{
+    "welcomeMsg": "Welcome to the 2025 edition of the Software Architecture course",
+    "login": "Login",
+    "logout": "Logout",
+    "home": "Home",
+    "loginStartMsg": "Login to Start Playing",
+    "username": "Username",
+    "password": "Password",
+    "rememberMe": "Remember Me",
+    "noAccount": "Don’t have an account?",
+    "registerHere": "Sign up here",
+    "loginQuestion": "Ready to test your knowledge? Log in and let's go!",
+    "createAccount": "Create Account",
+    "passwordReq": "Password must contain at least 8 characters, including a capital letter, a number, and a special character.",
+    "signUp": "Sign Up",
+    "alreadyAccount": "Already have an account?",
+    "loginHere": "Login here",
+    "registerTxtBubble": "Nice to see you are on board! Fill in the details and sign up to get started!",
+    "userAddedSuccess": "User added successfully"
+    
+    
+}
\ No newline at end of file
diff --git a/webapp/src/locales/translation_es.json b/webapp/src/locales/translation_es.json
new file mode 100644
index 0000000000..dc8ba3536c
--- /dev/null
+++ b/webapp/src/locales/translation_es.json
@@ -0,0 +1,20 @@
+{
+    "welcomeMsg": "Bienvenido a la edición 2025 del curso de Arquitectura de Software" ,
+    "login": "Iniciar sesión",
+    "logout": "Cerrar sesión",
+    "home": "Inicio",
+    "loginStartMsg": "Inicie sesión para comenzar",
+    "username": "Usuario",
+    "password": "Contraseña",
+    "rememberMe": "Recuérdame",
+    "noAccount": "¿No tienes cuenta?",
+    "registerHere": "Regístrate aquí",
+    "loginQuestion": "¿Listo para probar tus conocimientos? Inicia sesión y ¡comencemos!",
+    "createAccount": "Crear cuenta",
+    "passwordReq": "La contraseña debe contener al menos 8 caracteres, una mayúscula, un dígito y un carácter especial.",
+    "signUp": "Registrarse",
+    "alreadyAccount": "¿Ya tienes una cuenta?",
+    "loginHere": "Inicia sesión aquí",
+    "registerTxtBubble": "¡Qué bien verte aquí! ¡Rellena el formulario y regístrate para comenzar!",
+    "userAddedSuccess": "Usuario añadido con éxito"
+}
\ No newline at end of file
diff --git a/webapp/src/api/axios.js b/webapp/src/utils/axios.js
similarity index 100%
rename from webapp/src/api/axios.js
rename to webapp/src/utils/axios.js
diff --git a/webapp/src/utils/i18n.js b/webapp/src/utils/i18n.js
new file mode 100644
index 0000000000..410f83900d
--- /dev/null
+++ b/webapp/src/utils/i18n.js
@@ -0,0 +1,25 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+
+import translation_en from '../locales/translation_en.json';
+import translation_es from '../locales/translation_es.json';
+
+//Translations, this will be imported from a json file to make it easier to manage
+const resources = {
+    en: {translation: translation_en},
+    es: {translation: translation_es},
+};
+
+i18n
+    .use(initReactI18next) // passes i18n down to react-i18next
+    .init({
+        resources,
+        lng: 'en', // default language
+        fallbackLng: 'en', // fallback language
+
+        interpolation: {
+            escapeValue: false, // react already safes from xss
+        },
+    });
+
+export default i18n;
\ No newline at end of file


Reviews:
user: login: Vicxami
body: Everything looks fine, but I commented about some minor details.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T20:21:37Z
user: login: Daniel-RodriguezFernandez
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T20:34:35Z
user: login: Daniel-RodriguezFernandez
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T20:35:54Z
user: login: Vicxami
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-09T20:39:36Z

Review comments:
diff_hunk: @@ -45,21 +46,22 @@ const Login = () => {
     localStorage.setItem("persist", persist);
   }, [persist]);
 
+  const { t } = useTranslation();
 
   return (
     <Container maxWidth="md" sx={{ mt: 6 }}>
       <Paper elevation={4} sx={{ display: 'flex', borderRadius: 4, overflow: 'hidden' }}>
         {/* Left Panel - Login Form */}
         <Box sx={{ width: '50%', p: 4, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
           <Typography component="h1" variant="h5" textAlign="center" fontWeight="bold" mb={3}>
-            Login to Start Playing
+            {t("loginStartMsg")}
           </Typography>
 
           <TextField
             name="username"
             margin="normal"
             fullWidth
-            label="Username"
+            label={t("username")}
path: webapp/src/components/Login.js
user: login: Vicxami
body: On this file, you simply used the t method in the TextField, but in AddUser you created a custom component. It is inconsistent, so maybe we should stick to one or the other. Personally, I prefer using the translation directly as we dont need to change as much.
created_at: 2025-04-09T20:19:56Z
author_association: NONE
path: webapp/src/i18n.js
user: login: Vicxami
body: I think this file belongs in the api folder, with the custom axios instance I created a while back.
created_at: 2025-04-09T20:20:56Z
author_association: NONE
diff_hunk: @@ -45,21 +46,22 @@ const Login = () => {
     localStorage.setItem("persist", persist);
   }, [persist]);
 
+  const { t } = useTranslation();
 
   return (
     <Container maxWidth="md" sx={{ mt: 6 }}>
       <Paper elevation={4} sx={{ display: 'flex', borderRadius: 4, overflow: 'hidden' }}>
         {/* Left Panel - Login Form */}
         <Box sx={{ width: '50%', p: 4, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
           <Typography component="h1" variant="h5" textAlign="center" fontWeight="bold" mb={3}>
-            Login to Start Playing
+            {t("loginStartMsg")}
           </Typography>
 
           <TextField
             name="username"
             margin="normal"
             fullWidth
-            label="Username"
+            label={t("username")}
path: webapp/src/components/Login.js
user: login: Daniel-RodriguezFernandez
body: I refactored the addUser to reduce duplication, I may need to go with the custom field in both in order to pass the quality gate, it will also make the code easier to change when possibly reworking the translations in the future

created_at: 2025-04-09T20:34:35Z
author_association: NONE
path: webapp/src/i18n.js
user: login: Daniel-RodriguezFernandez
body: For what I've read online, this file usually goes in a utils folder with the rest of utilities, however since the project didn't have a dedicated utilities folder, the next recommendation was to leave it in the root of the src folder
created_at: 2025-04-09T20:35:54Z
author_association: NONE
path: webapp/src/i18n.js
user: login: Vicxami
body: Maybe instead of api like I called it at the time, we can rename the folder as "utils" if you want, and keep both files there. I say this because the axios.js file and the i18n.js file have very similar structure.
created_at: 2025-04-09T20:39:36Z
author_association: NONE

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-09T21:29:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Arquisoft_wichat_en3b&pullRequest=151) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13 New issues](https://sonarcloud.io/project/issues?id=Arquisoft_wichat_en3b&pullRequest=151&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Arquisoft_wichat_en3b&pullRequest=151&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Arquisoft_wichat_en3b&pullRequest=151&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [89.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=Arquisoft_wichat_en3b&pullRequest=151&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1.3% Duplication on New Code](https://sonarcloud.io/component_measures?id=Arquisoft_wichat_en3b&pullRequest=151&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Arquisoft_wichat_en3b&pullRequest=151)


author_association: NONE