diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 086b7f76..43911341 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,6 +19,9 @@ jobs:
- run: npm --prefix webapp ci
- run: npm --prefix questiongenerator ci
- run: npm --prefix gamehistoryservice ci
+ - run: npm --prefix apis/allquestionservice ci
+ - run: npm --prefix apis/alluserservice ci
+ - run: npm --prefix perfilservice ci
- run: npm --prefix users/authservice test -- --coverage
- run: npm --prefix users/userservice test -- --coverage
- run: npm --prefix gatewayservice test -- --coverage
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c396a5d3..56c71a56 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -155,10 +155,62 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
workdir: gamehistoryservice
+ docker-push-perfilservice:
+ name: Push perfil service service Docker Image to GitHub Packages
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [e2e-tests]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_es2c/perfilservice
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: ghcr.io
+ workdir: perfilservice
+ docker-push-allquestionservice:
+ name: Push all question service service Docker Image to GitHub Packages
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [e2e-tests]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_es2c/apis/allquestionservice
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: ghcr.io
+ workdir: apis/allquestionservice
+ docker-push-alluserservice:
+ name: Push all user service service Docker Image to GitHub Packages
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [e2e-tests]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_es2c/apis/alluserservice
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: ghcr.io
+ workdir: apis/alluserservice
deploy:
name: Deploy over SSH
runs-on: ubuntu-latest
- needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp,docker-push-questiongenerator,docker-push-gamehistoryservice]
+ needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp,
+ docker-push-questiongenerator,docker-push-gamehistoryservice,docker-push-perfilservice,docker-push-allquestionservice,docker-push-alluserservice]
steps:
- name: Deploy over SSH
uses: fifsky/ssh-action@master
diff --git a/apis/allquestionservice/Dockerfile b/apis/allquestionservice/Dockerfile
index 5678a0ab..74b03e40 100644
--- a/apis/allquestionservice/Dockerfile
+++ b/apis/allquestionservice/Dockerfile
@@ -13,8 +13,11 @@ RUN npm install
# Copy the app source code to the working directory
COPY . .
-# Expose the port the app runs on
+# Expose the port the app runs on
EXPOSE 8007
+ARG API_ORIGIN_URI="http://localhost:3000"
+ENV REACT_APP_API_ORIGIN_ENDPOINT=$API_ORIGIN_URI
+
# Define the command to run your app
CMD ["node", "allquestions-api.js"]
diff --git a/apis/alluserservice/Dockerfile b/apis/alluserservice/Dockerfile
index d1060ce1..2b562cbc 100644
--- a/apis/alluserservice/Dockerfile
+++ b/apis/alluserservice/Dockerfile
@@ -16,5 +16,8 @@ COPY . .
# Expose the port the app runs on
EXPOSE 8006
+ARG API_ORIGIN_URI="http://localhost:3000"
+ENV REACT_APP_API_ORIGIN_ENDPOINT=$API_ORIGIN_URI
+
# Define the command to run your app
CMD ["node", "allusers-api.js"]
diff --git a/docker-compose.yml b/docker-compose.yml
index e4f079ac..080e477b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -77,9 +77,11 @@ services:
perfilservice:
container_name: perfilservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_es2c/perfilservice/perfil-api:latest
+ image: ghcr.io/arquisoft/wiq_es2c/perfilservice:latest
profiles: ["dev", "prod"]
build: ./perfilservice
+ volumes:
+ - mongodb_data:/data/db
depends_on:
- mongodb
ports:
@@ -91,9 +93,11 @@ services:
alluserservice:
container_name: alluserservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_es2c/apis/alluserservice/allusers-api:latest
+ image: ghcr.io/arquisoft/wiq_es2c/apis/alluserservice:latest
profiles: ["dev", "prod"]
build: ./apis/alluserservice
+ volumes:
+ - mongodb_data:/data/db
depends_on:
- mongodb
ports:
@@ -105,9 +109,11 @@ services:
allquestionservice:
container_name: allquestionservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_es2c/apis/allquestionservice/allquestions-api:latest
+ image: ghcr.io/arquisoft/wiq_es2c/apis/allquestionservice:latest
profiles: ["dev", "prod"]
build: ./apis/allquestionservice
+ volumes:
+ - mongodb_data:/data/db
depends_on:
- mongodb
ports:
diff --git a/package-lock.json b/package-lock.json
index 1ffe46df..e003d34f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2,5 +2,49 @@
"name": "wiq_es2c",
"lockfileVersion": 3,
"requires": true,
- "packages": {}
+ "packages": {
+ "": {
+ "dependencies": {
+ "i18next": "^23.11.2"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+ "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "23.11.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.2.tgz",
+ "integrity": "sha512-qMBm7+qT8jdpmmDw/kQD16VpmkL9BdL+XNAK5MNbNFaf1iQQq35ZbPrSlqmnNPOSUY4m342+c0t0evinF5l7sA==",
+ "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"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
+ }
+ }
}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..dc54a203
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "i18next": "^23.11.2"
+ }
+}
diff --git a/perfilservice/Dockerfile b/perfilservice/Dockerfile
index 997b1fe3..889973d2 100644
--- a/perfilservice/Dockerfile
+++ b/perfilservice/Dockerfile
@@ -16,5 +16,8 @@ COPY . .
# Expose the port the app runs on
EXPOSE 8005
+ARG API_ORIGIN_URI="http://localhost:3000"
+ENV REACT_APP_API_ORIGIN_ENDPOINT=$API_ORIGIN_URI
+
# Define the command to run your app
CMD ["node", "perfil-api.js"]
diff --git a/timerservice/timer.js b/timerservice/timer.js
deleted file mode 100644
index 684792ad..00000000
--- a/timerservice/timer.js
+++ /dev/null
@@ -1,29 +0,0 @@
-
-app.use((req, res, next) => {
- res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
- res.setHeader('Access-Control-Allow-Credentials', true);
- next();
-});
-
-app.get('/timer', async (req, res) => {
-
- const time = parseInt(req.params.time);
- startTimer(time,() =>{
- res.send("Time up");
- })
-
-});
-
-function startTimer(time,func){
- this.timer = setTimeout(()=>{
- func();
- },time)
-}
-
-var server = app.listen(port, () => {
- console.log(`Questions Generation Service listening at http://localhost:${port}`);
-});
-
-module.exports = server
\ No newline at end of file
diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js
index 71f0d8ec..4c8b1ff7 100644
--- a/users/userservice/user-service.test.js
+++ b/users/userservice/user-service.test.js
@@ -2,7 +2,7 @@ const request = require('supertest');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
-let
+let newString = "testadduser";
const newUser = {
username: 'testuser',
email: 'testuser@gmail.com',
diff --git a/webapp/e2e/steps/register-form.steps.js b/webapp/e2e/steps/register-form.steps.js
index d411994d..447026b0 100644
--- a/webapp/e2e/steps/register-form.steps.js
+++ b/webapp/e2e/steps/register-form.steps.js
@@ -30,16 +30,16 @@ defineFeature(feature, test => {
given('An unregistered user', async () => {
username = "pablo"
- email = "pablo@email.com"
+ email = "pablo@gmail.com"
password = "pabloasw"
await expect(page).toClick("button", { text: "¿No tienes cuenta? Registrate aquí." });
});
-
+
when('I fill the data in the form and press submit', async () => {
await expect(page).toFill('input[name="username"]', username);
await expect(page).toFill('input[name="email"]', email);
await expect(page).toFill('input[name="password"]', password);
- await expect(page).toClick('button', { text: 'REGÍSTRATE' })
+ await expect(page).toClick('button', { text: 'REGÍSTRATE' });
});
then('A confirmation message should be shown in the screen', async () => {
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 69590fe3..9664032b 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -16,8 +16,11 @@
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.6.5",
+ "i18next": "^23.11.2",
+ "i18next-browser-languagedetector": "^7.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-i18next": "^14.1.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"web-vitals": "^3.5.1"
@@ -25,11 +28,14 @@
"devDependencies": {
"axios-mock-adapter": "^1.22.0",
"expect-puppeteer": "^9.0.2",
+ "i18next": "^23.11.2",
+ "i18next-browser-languagedetector": "^7.2.1",
"jest": "^29.3.1",
"jest-cucumber": "^3.0.1",
"jest-environment-node": "^29.7.0",
"mongodb-memory-server": "^9.1.4",
"puppeteer": "^21.7.0",
+ "react-i18next": "^14.1.0",
"serve": "^14.2.1",
"start-server-and-test": "^2.0.3"
}
@@ -12137,6 +12143,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==",
+ "dev": true,
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/html-webpack-plugin": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz",
@@ -12280,6 +12295,38 @@
"node": ">=10.17.0"
}
},
+ "node_modules/i18next": {
+ "version": "23.11.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.2.tgz",
+ "integrity": "sha512-qMBm7+qT8jdpmmDw/kQD16VpmkL9BdL+XNAK5MNbNFaf1iQQq35ZbPrSlqmnNPOSUY4m342+c0t0evinF5l7sA==",
+ "dev": true,
+ "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"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz",
+ "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -22031,6 +22078,28 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "node_modules/react-i18next": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz",
+ "integrity": "sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "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": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -26647,6 +26716,15 @@
"node": ">= 0.8"
}
},
+ "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==",
+ "dev": true,
+ "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 4825bfc3..2b7867a6 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -5,14 +5,17 @@
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
- "@mui/material": "^5.15.3",
"@mui/icons-material": "^5.15.15",
+ "@mui/material": "^5.15.3",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.6.5",
+ "i18next": "^23.11.2",
+ "i18next-browser-languagedetector": "^7.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-i18next": "^14.1.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"web-vitals": "^3.5.1"
@@ -52,6 +55,9 @@
"mongodb-memory-server": "^9.1.4",
"puppeteer": "^21.7.0",
"serve": "^14.2.1",
- "start-server-and-test": "^2.0.3"
+ "start-server-and-test": "^2.0.3",
+ "i18next": "^23.11.2",
+ "i18next-browser-languagedetector": "^7.2.1",
+ "react-i18next": "^14.1.0"
}
}
diff --git a/webapp/src/App.js b/webapp/src/App.js
index 99bb223b..66589424 100644
--- a/webapp/src/App.js
+++ b/webapp/src/App.js
@@ -7,6 +7,9 @@ import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import './App.css';
import { createTheme, ThemeProvider } from '@mui/material/styles';
+import {Button, Tooltip } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
const theme = createTheme({
palette: {
@@ -16,7 +19,12 @@ const theme = createTheme({
},
});
+
+
function App() {
+
+ const [t, i18n] = useTranslation("global");
+
const [showLogin, setShowLogin] = useState(true);
const handleToggleView = () => {
@@ -39,14 +47,31 @@ function App() {
{showLogin ? (
- ¿No tienes cuenta? Registrate aquí.
+ {t("enlaceLogin")}
) : (
- ¿Ya tienes cuenta? Inicia sesión aquí.
+ {t("enlaceRegistro")}
)}
+
+ {/* Aquí coloca tus dos botones */}
+
+
+
+
+
+
+
);
diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js
index 1bfc84bf..611a3051 100644
--- a/webapp/src/App.test.js
+++ b/webapp/src/App.test.js
@@ -2,6 +2,10 @@ import { render, screen, act, fireEvent} from '@testing-library/react';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
import { UserProvider } from './components/UserContext';
+import { I18nextProvider } from "react-i18next";
+import i18n from "./translations/i18n";
+
+i18n.changeLanguage("es");
describe('renders learn react link', () => {
it('play', async () => {
@@ -10,11 +14,13 @@ describe('renders learn react link', () => {
showLogin = newState;
};
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
const button = screen.getByText("¿No tienes cuenta? Registrate aquí.");
expect(button).toBeInTheDocument();
diff --git a/webapp/src/components/AddUser.js b/webapp/src/components/AddUser.js
index 0b8e69aa..c2e43e6a 100644
--- a/webapp/src/components/AddUser.js
+++ b/webapp/src/components/AddUser.js
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
import axios from 'axios';
import { Container, Typography, TextField, Button, Snackbar } from '@mui/material';
import '../App.css';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
@@ -15,6 +16,8 @@ const AddUser = () => {
const [openSnackbar, setOpenSnackbar] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
+ const [t] = useTranslation("global");
+
const addUser = async () => {
try {
@@ -34,23 +37,23 @@ const AddUser = () => {
return (
+ sx={{
+ marginTop: 4,
+ borderRadius: '10px',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ }}>
- REGÍSTRATE
+ {t("registro")}
setUsername(e.target.value)}
sx={{ marginBottom: 4, backgroundColor: '#FFFFFF'}}
@@ -59,7 +62,7 @@ const AddUser = () => {
name="email"
margin="normal"
fullWidth
- label="Email"
+ label={t("email")}
value={email}
onChange={(e) => setEmail(e.target.value)}
sx={{ marginBottom: 4, backgroundColor: '#FFFFFF'}}
@@ -68,7 +71,7 @@ const AddUser = () => {
name="password"
margin="normal"
fullWidth
- label="Contraseña"
+ label={t("password")}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -76,7 +79,7 @@ const AddUser = () => {
/>
diff --git a/webapp/src/components/AddUser.test.js b/webapp/src/components/AddUser.test.js
index 3e8403c6..30a0658d 100644
--- a/webapp/src/components/AddUser.test.js
+++ b/webapp/src/components/AddUser.test.js
@@ -3,7 +3,10 @@ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AddUser from './AddUser';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../translations/i18n";
+i18n.changeLanguage("es");
const mockAxios = new MockAdapter(axios);
describe('AddUser component', () => {
@@ -12,7 +15,9 @@ describe('AddUser component', () => {
});
it('should add user successfully', async () => {
- render();
+ render(
+
+ );
const usernameInput = screen.getByLabelText('Usuario');
const passwordInput = screen.getByLabelText('Contraseña');
@@ -35,7 +40,9 @@ describe('AddUser component', () => {
});
it('should handle error when adding user', async () => {
- render();
+ render(
+
+ );
const usernameInput = screen.getByLabelText('Usuario');
const passwordInput = screen.getByLabelText('Contraseña');
diff --git a/webapp/src/components/AllQuestions.js b/webapp/src/components/AllQuestions.js
index d031033b..223ec1aa 100644
--- a/webapp/src/components/AllQuestions.js
+++ b/webapp/src/components/AllQuestions.js
@@ -1,11 +1,13 @@
import axios from 'axios';
import React, { useState, useEffect, useCallback } from 'react';
import { Container, Typography, TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper, Snackbar } from '@mui/material';
+import { useTranslation } from 'react-i18next';
const AllQuestions = () => {
- const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
+ const [t] = useTranslation("global");
+ const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const [questions, setQuestions] = useState([]);
const [error, setError] = useState('');
@@ -17,7 +19,7 @@ const AllQuestions = () => {
} catch (error) {
setError(error.response.data.error);
}
- })
+ }, [apiEndpoint]);
useEffect(() => {
getAllQuestions();
@@ -38,14 +40,14 @@ const AllQuestions = () => {
width: '100%',
}}>
- TODAS LAS PREGUNTAS
+ {t("textoAllQuestions")}
- Pregunta
- Respuesta
+ {t("textoPregunta")}
+ {t("textoRespuesta")}
diff --git a/webapp/src/components/AllUsers.js b/webapp/src/components/AllUsers.js
index 57dfdd92..fd69f284 100644
--- a/webapp/src/components/AllUsers.js
+++ b/webapp/src/components/AllUsers.js
@@ -1,9 +1,13 @@
import axios from 'axios';
import React, { useState, useEffect, useCallback } from 'react';
import { Container, Typography, TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper, Snackbar } from '@mui/material';
+import { useTranslation } from 'react-i18next';
const AllUsers = () => {
+ const [t] = useTranslation("global");
+
+
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
@@ -18,7 +22,7 @@ const AllUsers = () => {
} catch (error) {
setError(error.response.data.error);
}
- })
+ }, [apiEndpoint]);
useEffect(() => {
getAllUsers();
@@ -39,15 +43,15 @@ const AllUsers = () => {
width: '100%',
}}>
- TODOS LOS USUARIOS
+ {t("textoAllUsers")}
- Usuario
- Email
- Creado
+ {t("usuario")}
+ {t("email")}
+ {t("creado")}
diff --git a/webapp/src/components/EndGame.js b/webapp/src/components/EndGame.js
index 48e1a7ae..a0b2b11c 100644
--- a/webapp/src/components/EndGame.js
+++ b/webapp/src/components/EndGame.js
@@ -90,6 +90,11 @@ const EndGame = () => {
+
+ {error && (
+ setError('')} message={`Error: ${error}`} />
+ )}
+
);
};
diff --git a/webapp/src/components/Game.js b/webapp/src/components/Game.js
index 8fb89d50..dd5eb6a0 100644
--- a/webapp/src/components/Game.js
+++ b/webapp/src/components/Game.js
@@ -5,10 +5,14 @@ import { Container, Typography, Button, Snackbar } from '@mui/material';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUser } from './UserContext';
import '../App.css';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const Game = () => {
+
+ const [t] = useTranslation("global");
+
const { usernameGlobal } = useUser();
const [question, setQuestion] = useState('');
const [image, setImage] = useState('');
@@ -49,7 +53,7 @@ const Game = () => {
console.log("Error: " + error.response.data.error);
setError(error.response.data.error);
}
- }, [usernameGlobal])
+ }, [usernameGlobal, MAX_TIME, THEMATIC]);
const saveGameHistory = useCallback(async () => {
try {
@@ -88,7 +92,7 @@ const Game = () => {
return () => {
clearTimeout(timerId);
}
- }, [elapsedTime, getQuestion, answeredQuestions, navigate, isTimeRunning, saveGameHistory]);
+ }, [elapsedTime, getQuestion, answeredQuestions, navigate, isTimeRunning, saveGameHistory, MAX_PREGUNTAS]);
const handleOptionClick = async (option) => {
var isTheCorrectAnswer = false;
@@ -147,7 +151,7 @@ const Game = () => {
{answeredQuestions} / {MAX_PREGUNTAS}
- Tiempo restante: {elapsedTime} segundos
+ {t("textoTiempoRest")} {elapsedTime} {t("textoTiempoRest2")}
{
beforeEach(() => {
mockAxios.reset();
@@ -37,11 +46,13 @@ describe('Start game', () => {
{ message: "Tiempo de pregunta actualizado exitosamente",
updatedQuestion });
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
var button1;
var button2;
@@ -108,11 +119,13 @@ describe('Start game', () => {
{ message: "Tiempo de pregunta actualizado exitosamente",
updatedQuestion });
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
await waitFor(() => {
expect(screen.getByText('Error: Error al generar la pregunta')).toBeInTheDocument();
diff --git a/webapp/src/components/GameConfiguration.js b/webapp/src/components/GameConfiguration.js
index dd1d4eae..a3a7c222 100644
--- a/webapp/src/components/GameConfiguration.js
+++ b/webapp/src/components/GameConfiguration.js
@@ -2,12 +2,16 @@
import React, { useState } from 'react';
import axios from 'axios';
import { useNavigate} from 'react-router-dom';
-import { Container, Typography, TextField, Button, Snackbar, skeletonClasses } from '@mui/material';
+import { Container, Typography, TextField, Button, Snackbar } from '@mui/material';
import '../App.css';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const GameConfiguration = () => {
+
+ const [t] = useTranslation("global");
+
const navigate = useNavigate();
const [openSnackbar, setOpenSnackbar] = useState(false);
const [error, setError] = useState('');
@@ -49,6 +53,7 @@ const GameConfiguration = () => {
navigate("/Game", {state: {time: valueTime, question:valueQuestion, thematic:selectedOption}});
} catch (error) {
setError(error.response.data.error);
+ setSnackbarMessage(error);
setOpenSnackbar(true);
}
};
@@ -60,22 +65,22 @@ const GameConfiguration = () => {
return (
+ sx={{
+ marginTop: 25,
+ borderRadius: '10px',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ }}>
- Personaliza tu partida
+ {t("textoPersonalizar")}
{
name="time"
margin="normal"
fullWidth
- label="Tiempo por pregunta"
+ label={t("textoTiempoPreg")}
onChange={handleChangeTime}
value={valueTime}
type="number"
@@ -106,20 +111,20 @@ const GameConfiguration = () => {
}}
/>
- Selecciona las tematicas de la pregunta para poder jugar
+ {t("textoTematicas")}
{error && (
diff --git a/webapp/src/components/Gamehistory.js b/webapp/src/components/Gamehistory.js
index e17b3975..2dc6a406 100644
--- a/webapp/src/components/Gamehistory.js
+++ b/webapp/src/components/Gamehistory.js
@@ -3,10 +3,14 @@ import axios from 'axios';
import { useUser } from './UserContext';
import { Container, Typography, TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper, Snackbar } from '@mui/material';
import '../App.css';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const Gamehistory = () => {
+
+ const [t] = useTranslation("global");
+
const { usernameGlobal } = useUser();
const [gamehistory, setGameHistory] = useState('');
const [error, setError] = useState('');
@@ -39,18 +43,18 @@ const Gamehistory = () => {
width: '100%',
}}>
- MIS ESTADÍSTICAS
+ {t("textoHistorico")}
- Partidas Jugadas
- Preguntas respondidas
- Aciertos
- Fallos
- Ratio de Acierto
- Tiempo jugado
+ {t("textoPartJug")}
+ {t("textoPregResp")}
+ {t("textoAciertos")}
+ {t("textoFallos")}
+ {t("textoRatAc")}
+ {t("textoTiempo")}
diff --git a/webapp/src/components/Gamehistory.test.js b/webapp/src/components/Gamehistory.test.js
index 8c4d19f4..9bf6e584 100644
--- a/webapp/src/components/Gamehistory.test.js
+++ b/webapp/src/components/Gamehistory.test.js
@@ -5,7 +5,10 @@ import { UserProvider } from './UserContext';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import GameHistory from './Gamehistory';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../translations/i18n";
+i18n.changeLanguage("es");
const mockAxios = new MockAdapter(axios);
describe('Game history', () => {
@@ -24,20 +27,22 @@ describe('Game history', () => {
ratio: "60 %",
totalTime: "10 s"});
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
await waitFor(() => {
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('MIS ESTADÍSTICAS');
- expect(screen.getByText('Partidas Jugadas')).toBeInTheDocument();
+ expect(screen.getByText('Partidas jugadas')).toBeInTheDocument();
expect(screen.getByText('Preguntas respondidas')).toBeInTheDocument();
expect(screen.getByText('Aciertos')).toBeInTheDocument();
expect(screen.getByText('Fallos')).toBeInTheDocument();
- expect(screen.getByText('Ratio de Acierto')).toBeInTheDocument();
+ expect(screen.getByText('Ratio de acierto')).toBeInTheDocument();
expect(screen.getByText('Tiempo jugado')).toBeInTheDocument();
expect(screen.getByText('1')).toBeInTheDocument();
diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js
index 21214930..3dfb2508 100644
--- a/webapp/src/components/Login.js
+++ b/webapp/src/components/Login.js
@@ -5,8 +5,13 @@ import axios from 'axios';
import { Container, Typography, TextField, Button, Snackbar } from '@mui/material';
import { useUser } from './UserContext';
import '../App.css';
+import { useTranslation } from 'react-i18next';
+
const Login = () => {
+
+ const [t] = useTranslation("global");
+
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
@@ -54,28 +59,30 @@ const Login = () => {
setOpenSnackbar(false);
};
+
+
return (
+ sx={{
+ marginTop: 4,
+ borderRadius: '10px',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ }}>
{loginSuccess ? (
null
) : (
- Inicia sesión
+ {t("login")}
setUsername(e.target.value)}
sx={{ marginBottom: 4, backgroundColor: '#FFFFFF'}}
@@ -83,16 +90,16 @@ const Login = () => {
setPassword(e.target.value)}
sx={{ marginBottom: 4, backgroundColor: '#FFFFFF'}}
/>
-
+
{error && (
setError('')} message={`Error: ${error}`} />
)}
diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js
index 5e45e73f..c600e01e 100644
--- a/webapp/src/components/Login.test.js
+++ b/webapp/src/components/Login.test.js
@@ -5,8 +5,10 @@ import MockAdapter from 'axios-mock-adapter';
import Login from './Login';
import { BrowserRouter as Router } from 'react-router-dom';
import { UserProvider } from './UserContext';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../translations/i18n";
-
+i18n.changeLanguage("es");
const mockAxios = new MockAdapter(axios);
describe('Login component', () => {
@@ -15,11 +17,13 @@ describe('Login component', () => {
});
it('should log in successfully', async () => {
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
const usernameInput = screen.getByLabelText('Usuario');
const passwordInput = screen.getByLabelText('Contraseña');
@@ -38,11 +42,13 @@ describe('Login component', () => {
});
it('should handle error when logging in', async () => {
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
const usernameInput = screen.getByLabelText('Usuario');
const passwordInput = screen.getByLabelText('Contraseña');
diff --git a/webapp/src/components/PantallaInicio.js b/webapp/src/components/PantallaInicio.js
index 3140c423..344aaabc 100644
--- a/webapp/src/components/PantallaInicio.js
+++ b/webapp/src/components/PantallaInicio.js
@@ -4,10 +4,15 @@ import { useUser } from './UserContext';
import { useNavigate } from 'react-router-dom';
import NewGameIcon from '@mui/icons-material/SportsEsports';
import '../App.css';
+import { useTranslation } from 'react-i18next';
+
const PantallaInicio = () => {
+ const [t] = useTranslation("global");
+
+
const [openSnackbar, setOpenSnackbar] = useState(false);
const [error, setError] = useState('');
@@ -41,12 +46,12 @@ const PantallaInicio = () => {
alignItems: 'center'
}}>
- ¡BIENVENIDO A WIQ {usernameGlobal}!
+ {t("textoInicio")} {usernameGlobal}!
} variant="contained" color="primary" align="center" sx={{ marginTop: 4, backgroundColor: '#FCF5B8', color: '#413C3C', fontWeight: 'bold' }}
onClick={nuevaPartida}>
- NUEVA PARTIDA
+ {t("botonPartida")}
diff --git a/webapp/src/components/PantallaInicio.test.js b/webapp/src/components/PantallaInicio.test.js
index fbf59da5..9df57bcb 100644
--- a/webapp/src/components/PantallaInicio.test.js
+++ b/webapp/src/components/PantallaInicio.test.js
@@ -5,7 +5,10 @@ import MockAdapter from 'axios-mock-adapter';
import PantallaInicio from './PantallaInicio';
import { BrowserRouter as Router } from 'react-router-dom';
import { UserProvider } from './UserContext';
+import { I18nextProvider } from "react-i18next";
+import i18n from "../translations/i18n";
+i18n.changeLanguage("es");
const mockAxios = new MockAdapter(axios);
describe('PantallaInicio component', () => {
@@ -15,13 +18,15 @@ describe('PantallaInicio component', () => {
it('muestra la pantalla de inicio correctamente', async () => {
- render(
-
-
-
- );
+ render(
+
+
+
+
+
+ );
- const element = screen.getByText(/¡BIENVENIDO A WIQ/);
+ const element = screen.getByText(/BIENVENIDO/);
const nuevaPartidaButton = screen.getByRole('button', { name: 'NUEVA PARTIDA' });
// Verifica si el elemento se encuentra en el DOM
diff --git a/webapp/src/components/PantallaInicioAdmin.js b/webapp/src/components/PantallaInicioAdmin.js
index 699dbbd0..a1941388 100644
--- a/webapp/src/components/PantallaInicioAdmin.js
+++ b/webapp/src/components/PantallaInicioAdmin.js
@@ -1,10 +1,14 @@
import React, { useState } from 'react';
import { Container, Button, Box, Snackbar } from '@mui/material';
import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
const PantallaInicio = () => {
+ const [t] = useTranslation("global");
+
+
const [openSnackbar, setOpenSnackbar] = useState(false);
const [error, setError] = useState('');
@@ -43,13 +47,13 @@ const PantallaInicio = () => {
alignItems: 'center'
}}>
-
+
{error && (
setError('')} message={`Error: ${error}`} />
)}
diff --git a/webapp/src/components/Perfil.js b/webapp/src/components/Perfil.js
index c3e9be09..eb18ad12 100644
--- a/webapp/src/components/Perfil.js
+++ b/webapp/src/components/Perfil.js
@@ -2,9 +2,13 @@ import axios from 'axios';
import React, { useState, useEffect, useCallback } from 'react';
import { Container, Typography, TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper, Snackbar } from '@mui/material';
import { useUser } from './UserContext';
+import { useTranslation } from 'react-i18next';
+
const Perfil = () => {
+ const [t] = useTranslation("global");
+
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const { usernameGlobal } = useUser();
@@ -24,7 +28,7 @@ const Perfil = () => {
} catch (error) {
setError(error.response.data.error);
}
- }, [usernameGlobal])
+ }, [usernameGlobal, apiEndpoint]);
useEffect(() => {
getPerfil();
@@ -46,15 +50,15 @@ const Perfil = () => {
}}
>
- PERFIL
+ {t("textoPerfil")}
- Usuario
- Email
- Creado
+ {t("usuario")}
+ {t("email")}
+ {t("creado")}
diff --git a/webapp/src/components/Ranking.js b/webapp/src/components/Ranking.js
index b190f0cf..4a4497bb 100644
--- a/webapp/src/components/Ranking.js
+++ b/webapp/src/components/Ranking.js
@@ -1,14 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
-import { Container, Box, Typography, Grid} from '@mui/material';
-import { useUser } from './UserContext';
+import { Container, Box, Typography, Grid, Snackbar} from '@mui/material';
import '../App.css';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const Ranking = () => {
- const { usernameGlobal } = useUser();
+ const [t] = useTranslation("global");
+
const [ranking, setRanking] = useState('');
const [error, setError] = useState('');
@@ -19,7 +20,7 @@ const Ranking = () => {
} catch (error) {
setError(error.response.data.error);
}
- }, [usernameGlobal])
+ }, []);
useEffect(() => {
getRanking();
@@ -37,7 +38,7 @@ const Ranking = () => {
}}>
- Top 3 Usuarios
+ {t("textoTop")}
@@ -68,6 +69,11 @@ const Ranking = () => {
+
+ {error && (
+ setError('')} message={`Error: ${error}`} />
+ )}
+
);
};
diff --git a/webapp/src/components/fragments/NavigationBar.js b/webapp/src/components/fragments/NavigationBar.js
index 20f84362..d1093103 100644
--- a/webapp/src/components/fragments/NavigationBar.js
+++ b/webapp/src/components/fragments/NavigationBar.js
@@ -5,9 +5,13 @@ import { AppBar, Toolbar, IconButton, Menu, MenuItem, Grid, Button, Hidden} from
import Tooltip from '@mui/material/Tooltip';
import { useUser } from '../UserContext';
import MenuIcon from '@mui/icons-material/Menu';
+import { useTranslation } from 'react-i18next';
+
const NavigationBar = () => {
+ const [t] = useTranslation("global");
+
const [setError] = useState('');
const { usernameGlobal, setUsernameGlobal } = useUser();
const navigate = useNavigate();
@@ -57,6 +61,8 @@ const NavigationBar = () => {
}
};
+
+
if (isHiddenRoute) {
return null; // Si no estás en / o /App, no muestra la barra de navegación
}
@@ -88,33 +94,34 @@ const NavigationBar = () => {
-
-
-
-
+
+
-
-
-
+
+
+
-
+
+
diff --git a/webapp/src/components/fragments/NavigationBar_Game.js b/webapp/src/components/fragments/NavigationBarGame.js
similarity index 86%
rename from webapp/src/components/fragments/NavigationBar_Game.js
rename to webapp/src/components/fragments/NavigationBarGame.js
index 702ec504..ec603922 100644
--- a/webapp/src/components/fragments/NavigationBar_Game.js
+++ b/webapp/src/components/fragments/NavigationBarGame.js
@@ -1,13 +1,16 @@
import React, {useCallback, useState} from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
-import { AppBar, Toolbar, IconButton, Menu, MenuItem, Grid, Button, Hidden} from '@mui/material';
+import { AppBar, Toolbar, IconButton, Menu, MenuItem, Grid, Button, Hidden, Snackbar} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import axios from 'axios';
import Tooltip from '@mui/material/Tooltip';
+import { useTranslation } from 'react-i18next';
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
const NavigationBar_Game = () => {
+ const [t] = useTranslation("global");
+
const [error, setError] = useState('');
const navigate = useNavigate();
@@ -26,7 +29,6 @@ const NavigationBar_Game = () => {
setAnchorEl(null);
};
-
const showHome = useCallback(async () => {
try {
axios.get(`${apiEndpoint}/restartGame`);
@@ -35,7 +37,7 @@ const NavigationBar_Game = () => {
console.log("Error: " + error.response.data.error);
setError(error.response.data.error);
}
- })
+ }, [navigate]);
if (isHiddenRoute) {
return null; // Si no estás en / o /App, no muestra la barra de navegación
@@ -64,7 +66,7 @@ const NavigationBar_Game = () => {
-
+
@@ -72,6 +74,11 @@ const NavigationBar_Game = () => {
+
+ {error && (
+ setError('')} message={`Error: ${error}`} />
+ )}
+
);
};
diff --git a/webapp/src/components/images/esp.png b/webapp/src/components/images/esp.png
new file mode 100644
index 00000000..e03bcf15
Binary files /dev/null and b/webapp/src/components/images/esp.png differ
diff --git a/webapp/src/components/images/ing.png b/webapp/src/components/images/ing.png
new file mode 100644
index 00000000..82f498b4
Binary files /dev/null and b/webapp/src/components/images/ing.png differ
diff --git a/webapp/src/index.js b/webapp/src/index.js
index 5e6f76e0..28c0d5ab 100644
--- a/webapp/src/index.js
+++ b/webapp/src/index.js
@@ -1,11 +1,15 @@
-import React from 'react';
+ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import NavigationBar from './components/fragments/NavigationBar';
-import NavigationBar_Game from './components/fragments/NavigationBar_Game';
+import NavigationBarGame from './components/fragments/NavigationBarGame';
import reportWebVitals from './reportWebVitals';
import { UserProvider } from './components/UserContext';
+import { I18nextProvider } from 'react-i18next';
+import i18next from 'i18next';
+import global_es from "./translations/es/global.json";
+import global_en from "./translations/en/global.json";
import {
Route,
@@ -25,13 +29,27 @@ import AllQuestions from './components/AllQuestions';
import Ranking from './components/Ranking';
import GameConfiguration from './components/GameConfiguration';
+i18next.init( {
+ interpolation: { escapevalue: false},
+ lng:"es",
+ resources: {
+ es: {
+ global: global_es
+ },
+ en: {
+ global: global_en
+ }
+ }
+})
+
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+
-
+
}>
}>
@@ -49,6 +67,7 @@ root.render(
+
);
diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json
new file mode 100644
index 00000000..27f8f113
--- /dev/null
+++ b/webapp/src/translations/en/global.json
@@ -0,0 +1,50 @@
+{
+ "login": "Log in",
+ "usuario": "User",
+ "password": "Password",
+ "botonLogin": "ENTERS",
+ "enlaceLogin": "You do not have an account? Sign up here.",
+ "email": "Email",
+ "botonRegistro": "SIGN UP",
+ "registro": "SIGN UP",
+ "enlaceRegistro": "Do you already have an account? Sign in here.",
+ "textoInicio": "WELCOME TO WIQ",
+ "botonPartida": "NEW GAME",
+ "toolInicio":"Start",
+ "toolHistorico":"Historical",
+ "toolPerfil":"Profile",
+ "toolRanking":"Ranking",
+ "toolLogOut":"Log out",
+ "textoHistorico": "MY STATISTICS",
+ "textoPartJug": "Games played",
+ "textoPregResp": "Questions answered",
+ "textoAciertos": "Successes",
+ "textoFallos": "Failures",
+ "textoRatAc": "Success rate",
+ "textoTiempo": "Time played",
+ "textoTop": "Top 3 users",
+ "creado": "Created",
+ "textoPerfil": "PROFILE",
+ "textoPersonalizar": "Customize your game",
+ "textoNumPreg": "Number of questions",
+ "textoTiempoPreg": "Time available per question",
+ "textoTematicas": "Select the topics of the question to be able to play",
+ "tematicaTodas": "All",
+ "tematicaInf": "Computing",
+ "tematicaGeo": "Geography",
+ "tematicaCult": "Culture",
+ "tematicaPersonajes": "Characters",
+ "botonJugar": "PLAY",
+ "textoTiempoRest": "Time left: ",
+ "textoTiempoRest2": " seconds",
+ "mensajeLogin": "Successful login",
+ "textoAllUsers": "ALL USERS",
+ "textoAllQuestions": "ALL QUESTIONS",
+ "botonUsuarios": "USERS",
+ "botonPreguntas": "QUESTIONS",
+ "textoPregunta": "Question",
+ "textoRespuesta": "Answer",
+ "mensajeAddOk": "User added successfully",
+ "mensajeAddFail": "An user with that name has already registered",
+ "mensajeLogOut": "Closed session"
+}
\ No newline at end of file
diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json
new file mode 100644
index 00000000..2ff897b9
--- /dev/null
+++ b/webapp/src/translations/es/global.json
@@ -0,0 +1,51 @@
+{
+ "login": "Inicia sesión",
+ "usuario": "Usuario",
+ "password": "Contraseña",
+ "botonLogin": "ENTRA",
+ "enlaceLogin": "¿No tienes cuenta? Registrate aquí.",
+ "email": "Email",
+ "botonRegistro": "REGÍSTRATE",
+ "registro": "REGÍSTRATE",
+ "enlaceRegistro": "¿Ya tienes cuenta? Inicia sesión aquí.",
+ "textoInicio": "¡BIENVENIDO A WIQ",
+ "botonPartida": "NUEVA PARTIDA",
+ "toolInicio":"Inicio",
+ "toolHistorico":"Historico",
+ "toolPerfil":"Perfil",
+ "toolRanking":"Ranking",
+ "toolLogOut":"Cerrar sesión",
+ "textoHistorico": "MIS ESTADÍSTICAS",
+ "textoPartJug": "Partidas jugadas",
+ "textoPregResp": "Preguntas respondidas",
+ "textoAciertos": "Aciertos",
+ "textoFallos": "Fallos",
+ "textoRatAc": "Ratio de acierto",
+ "textoTiempo": "Tiempo jugado",
+ "textoTop": "Top 3 usuarios",
+ "creado": "Creado",
+ "textoPerfil": "PERFIL",
+ "textoPersonalizar": "Personaliza tu partida",
+ "textoNumPreg": "Número de preguntas",
+ "textoTiempoPreg": "Tiempo disponible por pregunta",
+ "textoTematicas": "Selecciona las tematicas de la pregunta para poder jugar",
+ "tematicaTodas": "Todas",
+ "tematicaInf": "Informática",
+ "tematicaGeo": "Geografía",
+ "tematicaCult": "Cultura",
+ "tematicaPersonajes": "Personajes",
+ "botonJugar": "JUGAR",
+ "textoTiempoRest": "Tiempo restante: ",
+ "textoTiempoRest2": " segundos",
+ "mensajeLogin": "Inicio de sesión correcto",
+ "textoAllUsers": "TODOS LOS USUARIOS",
+ "textoAllQuestions": "TODOS LOS USUARIOS",
+ "botonUsuarios": "USUARIOS",
+ "botonPreguntas": "PREGUNTAS",
+ "textoPregunta": "Pregunta",
+ "textoRespuesta": "Respuesta",
+ "mensajeAddOk": "Usuario añadido correctamente",
+ "mensajeAddFail": "Ya se ha registrado un usuario con ese nombre",
+ "mensajeLogOut": "Sesion cerrada"
+}
+
diff --git a/webapp/src/translations/i18n.js b/webapp/src/translations/i18n.js
new file mode 100644
index 00000000..40bb6b8f
--- /dev/null
+++ b/webapp/src/translations/i18n.js
@@ -0,0 +1,32 @@
+import eng from './en/global.json'
+import esp from './es/global.json'
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+i18n
+ // detect user language
+ // learn more: https://github.com/i18next/i18next-browser-languageDetector
+ .use(LanguageDetector)
+ // pass the i18n instance to react-i18next.
+ .use(initReactI18next)
+ // init i18next
+ // for all options read: https://www.i18next.com/overview/configuration-options
+ .init({
+ debug: true,
+ fallbackLng: 'en',
+ interpolation: {
+ escapeValue: false, // not needed for react as it escapes by default
+ },
+ resources: {
+ en: {
+ global: eng,
+ },
+ es: {
+ global: esp,
+ }
+ }
+ });
+ console.log(esp);
+
+export default i18n;
\ No newline at end of file