aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/i18n/index.ts31
-rw-r--r--src/frontend/app/i18n/locales/en-GB.json46
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json46
-rw-r--r--src/frontend/app/i18n/locales/gl-ES.json46
-rw-r--r--src/frontend/app/root.css1
-rw-r--r--src/frontend/app/root.tsx2
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx2
-rw-r--r--src/frontend/app/routes/map.tsx2
-rw-r--r--src/frontend/app/routes/settings.tsx69
-rw-r--r--src/frontend/app/routes/stoplist.tsx16
10 files changed, 219 insertions, 42 deletions
diff --git a/src/frontend/app/i18n/index.ts b/src/frontend/app/i18n/index.ts
new file mode 100644
index 0000000..a7ba6aa
--- /dev/null
+++ b/src/frontend/app/i18n/index.ts
@@ -0,0 +1,31 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import esES from './locales/es-ES.json';
+import glES from './locales/gl-ES.json';
+import enGB from './locales/en-GB.json';
+
+// Add more languages as needed
+const resources = {
+ 'es-ES': { translation: esES },
+ 'gl-ES': { translation: glES },
+ 'en-GB': { translation: enGB },
+};
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources,
+ fallbackLng: 'es-ES',
+ interpolation: {
+ escapeValue: false,
+ },
+ supportedLngs: ['es-ES', 'gl-ES', 'en-GB'],
+ detection: {
+ order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
+ caches: ['localStorage', 'cookie'],
+ },
+ });
+
+export default i18n;
diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json
new file mode 100644
index 0000000..f53a802
--- /dev/null
+++ b/src/frontend/app/i18n/locales/en-GB.json
@@ -0,0 +1,46 @@
+{
+ "about": {
+ "title": "About UrbanoVigo Web",
+ "description": "Web app to find stops and arrival times for Vigo's urban buses, Spain.",
+ "credits": "Credits",
+ "github": "Code on GitHub",
+ "developed_by": "Developed by",
+ "data_source_prefix": "Data from",
+ "data_source_middle": "under license",
+ "settings": "Settings",
+ "theme": "Mode:",
+ "theme_light": "Light",
+ "theme_dark": "Dark",
+ "table_style": "Table style:",
+ "table_style_regular": "Show in order",
+ "table_style_grouped": "Group by line",
+ "map_position_mode": "Map position:",
+ "map_position_gps": "GPS position",
+ "map_position_last": "Where I left it",
+ "details_summary": "What does this mean?",
+ "details_table": "The timetable can be shown in two ways:",
+ "details_regular": "Stops are shown in the order they are visited. Apps like Infobus (Vitrasa) use this style.",
+ "details_grouped": "Stops are grouped by bus line. Apps like iTranvias (A Coruña) or Moovit (more or less) use this style."
+ },
+ "stoplist": {
+ "search_placeholder": "Search stop...",
+ "favourites": "Favourites",
+ "recents": "Recent"
+ },
+ "estimates": {
+ "minutes": "min",
+ "meters": "m",
+ "edit": "Edit name",
+ "favourite": "Favourite",
+ "not_found": "Stop not found"
+ },
+ "map": {
+ "popup_title": "Stop",
+ "lines": "Lines"
+ },
+ "common": {
+ "loading": "Loading...",
+ "error": "An unexpected error occurred.",
+ "404": "The requested page could not be found."
+ }
+}
diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json
new file mode 100644
index 0000000..814019e
--- /dev/null
+++ b/src/frontend/app/i18n/locales/es-ES.json
@@ -0,0 +1,46 @@
+{
+ "about": {
+ "title": "Sobre UrbanoVigo Web",
+ "description": "Aplicación web para encontrar paradas y tiempos de llegada de los autobuses urbanos de Vigo, España.",
+ "credits": "Créditos",
+ "github": "Código en GitHub",
+ "developed_by": "Desarrollado por",
+ "data_source_prefix": "Datos obtenidos de",
+ "data_source_middle": "bajo licencia",
+ "settings": "Ajustes",
+ "theme": "Modo:",
+ "theme_light": "Claro",
+ "theme_dark": "Oscuro",
+ "table_style": "Estilo de tabla:",
+ "table_style_regular": "Mostrar por orden",
+ "table_style_grouped": "Agrupar por línea",
+ "map_position_mode": "Posición del mapa:",
+ "map_position_gps": "Posición GPS",
+ "map_position_last": "Donde lo dejé",
+ "details_summary": "¿Qué significa esto?",
+ "details_table": "La tabla de horarios puede mostrarse de dos formas:",
+ "details_regular": "Las paradas se muestran en el orden en que se visitan. Aplicaciones como Infobus (Vitrasa) usan este estilo.",
+ "details_grouped": "Las paradas se agrupan por la línea de autobús. Aplicaciones como iTranvias (A Coruña) o Moovit (más o menos) usan este estilo."
+ },
+ "stoplist": {
+ "search_placeholder": "Buscar parada...",
+ "favourites": "Favoritas",
+ "recents": "Recientes"
+ },
+ "estimates": {
+ "minutes": "min",
+ "meters": "m",
+ "edit": "Editar nombre",
+ "favourite": "Favorito",
+ "not_found": "No se encontró la parada"
+ },
+ "map": {
+ "popup_title": "Parada",
+ "lines": "Líneas"
+ },
+ "common": {
+ "loading": "Cargando...",
+ "error": "Ha ocurrido un error inesperado.",
+ "404": "La página solicitada no se pudo encontrar."
+ }
+}
diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json
new file mode 100644
index 0000000..3c0c2fc
--- /dev/null
+++ b/src/frontend/app/i18n/locales/gl-ES.json
@@ -0,0 +1,46 @@
+{
+ "about": {
+ "title": "Sobre UrbanoVigo Web",
+ "description": "Aplicación web para atopar paradas e tempos de chegada dos autobuses urbanos de Vigo, España.",
+ "credits": "Créditos",
+ "github": "Código en GitHub",
+ "developed_by": "Desenvolvido por",
+ "data_source_prefix": "Datos obtidos de",
+ "data_source_middle": "baixo licenza",
+ "settings": "Axustes",
+ "theme": "Modo:",
+ "theme_light": "Claro",
+ "theme_dark": "Escuro",
+ "table_style": "Estilo de táboa:",
+ "table_style_regular": "Mostrar por orde",
+ "table_style_grouped": "Agrupar por liña",
+ "map_position_mode": "Posición do mapa:",
+ "map_position_gps": "Posición GPS",
+ "map_position_last": "Onde o deixei",
+ "details_summary": "Que significa isto?",
+ "details_table": "A táboa de horarios pode mostrarse de dúas formas:",
+ "details_regular": "As paradas móstranse na orde na que se visitan. Aplicacións como Infobus (Vitrasa) usan este estilo.",
+ "details_grouped": "As paradas agrúpanse pola liña de autobús. Aplicacións como iTranvias (A Coruña) ou Moovit (máis ou menos) usan este estilo."
+ },
+ "stoplist": {
+ "search_placeholder": "Buscar parada...",
+ "favourites": "Favoritas",
+ "recents": "Recentes"
+ },
+ "estimates": {
+ "minutes": "min",
+ "meters": "m",
+ "edit": "Editar nome",
+ "favourite": "Favorita",
+ "not_found": "Non se atopou a parada"
+ },
+ "map": {
+ "popup_title": "Parada",
+ "lines": "Liñas"
+ },
+ "common": {
+ "loading": "Cargando...",
+ "error": "Produciuse un erro inesperado.",
+ "404": "Non se puido atopar a páxina solicitada."
+ }
+}
diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css
index 89da67a..a5024df 100644
--- a/src/frontend/app/root.css
+++ b/src/frontend/app/root.css
@@ -45,6 +45,7 @@ body {
.main-content {
flex: 1;
overflow: auto;
+ overflow-x: hidden;
}
.navigation-bar {
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 9d59ce7..8ffbe86 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -22,6 +22,8 @@ const pmtiles = new Protocol();
maplibregl.addProtocol("pmtiles", pmtiles.tile);
//#endregion
+import "./i18n";
+
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index 761a8d4..5dbbc7d 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -6,6 +6,7 @@ import "./estimates-$id.css";
import { RegularTable } from "../components/RegularTable";
import { useApp } from "../AppContext";
import { GroupedTable } from "../components/GroupedTable";
+import { useTranslation } from "react-i18next";
export interface StopDetails {
stop: {
@@ -32,6 +33,7 @@ const loadData = async (stopId: string) => {
};
export default function Estimates() {
+ const { t } = useTranslation();
const params = useParams();
const stopIdNum = parseInt(params.id ?? "");
const [customName, setCustomName] = useState<string | undefined>(undefined);
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index a938148..ca095e2 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -8,6 +8,7 @@ import { loadStyle } from "app/maps/styleloader";
import type { Feature as GeoJsonFeature, Point } from 'geojson';
import LineIcon from "~/components/LineIcon";
import { Link } from "react-router";
+import { useTranslation } from "react-i18next";
// Default minimal fallback style before dynamic loading
const defaultStyle: StyleSpecification = {
@@ -20,6 +21,7 @@ const defaultStyle: StyleSpecification = {
// Componente principal del mapa
export default function StopMap() {
+ const { t } = useTranslation();
const [stops, setStops] = useState<GeoJsonFeature<Point, { stopId: number; name: string; lines: string[] }>[]>([]);
const [popupInfo, setPopupInfo] = useState<any>(null);
const { mapState, updateMapState, theme } = useApp();
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index b5e91f1..e657c03 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -1,64 +1,75 @@
import { useApp } from "../AppContext";
import "./settings.css";
+import { useTranslation } from "react-i18next";
export default function Settings() {
+ const { t, i18n } = useTranslation();
const { theme, setTheme, tableStyle, setTableStyle, mapPositionMode, setMapPositionMode } = useApp();
return (
<div className="page-container">
- <h1 className="page-title">Sobre UrbanoVigo Web</h1>
+ <h1 className="page-title">{t('about.title')}</h1>
<p className="about-description">
- Aplicación web para encontrar paradas y tiempos de llegada de los autobuses
- urbanos de Vigo, España.
+ {t('about.description')}
</p>
<section className="settings-section">
- <h2>Ajustes</h2>
+ <h2>{t('about.settings')}</h2>
<div className="settings-content-inline">
- <label htmlFor="theme" className="form-label-inline">Modo:</label>
- <select id="theme" className="form-select-inline" value={theme} onChange={(e) => setTheme(e.target.value as "light" | "dark")}>
- <option value="light">Claro</option>
- <option value="dark">Oscuro</option>
+ <label htmlFor="theme" className="form-label-inline">{t('about.theme')}</label>
+ <select id="theme" className="form-select-inline" value={theme} onChange={(e) => setTheme(e.target.value as "light" | "dark")}>
+ <option value="light">{t('about.theme_light')}</option>
+ <option value="dark">{t('about.theme_dark')}</option>
</select>
</div>
<div className="settings-content-inline">
- <label htmlFor="tableStyle" className="form-label-inline">Estilo de tabla:</label>
- <select id="tableStyle" className="form-select-inline" value={tableStyle} onChange={(e) => setTableStyle(e.target.value as "regular" | "grouped")}>
- <option value="regular">Mostrar por orden</option>
- <option value="grouped">Agrupar por línea</option>
+ <label htmlFor="tableStyle" className="form-label-inline">{t('about.table_style')}</label>
+ <select id="tableStyle" className="form-select-inline" value={tableStyle} onChange={(e) => setTableStyle(e.target.value as "regular" | "grouped")}>
+ <option value="regular">{t('about.table_style_regular')}</option>
+ <option value="grouped">{t('about.table_style_grouped')}</option>
</select>
</div>
<div className="settings-content-inline">
- <label htmlFor="mapPositionMode" className="form-label-inline">Posición del mapa:</label>
- <select id="mapPositionMode" className="form-select-inline" value={mapPositionMode} onChange={e => setMapPositionMode(e.target.value as 'gps' | 'last')}>
- <option value="gps">Posición GPS</option>
- <option value="last">Donde lo dejé</option>
+ <label htmlFor="mapPositionMode" className="form-label-inline">{t('about.map_position_mode')}</label>
+ <select id="mapPositionMode" className="form-select-inline" value={mapPositionMode} onChange={e => setMapPositionMode(e.target.value as 'gps' | 'last')}>
+ <option value="gps">{t('about.map_position_gps')}</option>
+ <option value="last">{t('about.map_position_last')}</option>
+ </select>
+ </div>
+ <div className="settings-content-inline">
+ <label htmlFor="language" className="form-label-inline">Idioma:</label>
+ <select
+ id="language"
+ className="form-select-inline"
+ value={i18n.language}
+ onChange={e => i18n.changeLanguage(e.target.value)}
+ >
+ <option value="es-ES">Español</option>
+ <option value="gl-ES">Galego</option>
+ <option value="en-GB">English</option>
</select>
</div>
<details className="form-details">
- <summary>¿Qué significa esto?</summary>
- <p>
- La tabla de horarios puede mostrarse de dos formas:
- </p>
+ <summary>{t('about.details_summary')}</summary>
+ <p>{t('about.details_table')}</p>
<dl>
- <dt>Mostrar por orden</dt>
- <dd>Las paradas se muestran en el orden en que se visitan. Aplicaciones como Infobus (Vitrasa) usan este estilo.</dd>
- <dt>Agrupar por línea</dt>
- <dd>Las paradas se agrupan por la línea de autobús. Aplicaciones como iTranvias (A Coruña) o Moovit (más o menos) usan este estilo.</dd>
+ <dt>{t('about.table_style_regular')}</dt>
+ <dd>{t('about.details_regular')}</dd>
+ <dt>{t('about.table_style_grouped')}</dt>
+ <dd>{t('about.details_grouped')}</dd>
</dl>
</details>
</section>
- <h2>Créditos</h2>
+ <h2>{t('about.credits')}</h2>
<p>
<a href="https://github.com/arielcostas/urbanovigo-web" className="about-link" rel="nofollow noreferrer noopener">
- Código en GitHub
+ {t('about.github')}
</a> -
- Desarrollado por <a href="https://www.costas.dev" className="about-link" rel="nofollow noreferrer noopener">
+ {t('about.developed_by')} <a href="https://www.costas.dev" className="about-link" rel="nofollow noreferrer noopener">
Ariel Costas
</a>
</p>
<p>
- Datos obtenidos de <a href="https://datos.vigo.org" className="about-link" rel="nofollow noreferrer noopener">datos.vigo.org</a> bajo
- licencia <a href="https://opendefinition.org/licenses/odc-by/" className="about-link" rel="nofollow noreferrer noopener">Open Data Commons Attribution License</a>
+ {t('about.data_source_prefix')} <a href="https://datos.vigo.org" className="about-link" rel="nofollow noreferrer noopener">datos.vigo.org</a> {t('about.data_source_middle')} <a href="https://opendefinition.org/licenses/odc-by/" className="about-link" rel="nofollow noreferrer noopener">Open Data Commons Attribution License</a>
</p>
</div>
)
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index ff1da71..0ab6d15 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -3,25 +3,15 @@ import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import StopItem from "../components/StopItem";
import Fuse from "fuse.js";
import './stoplist.css';
-
-const placeholders = [
- "Urzaiz",
- "Gran Vía",
- "Castelao",
- "García Barbón",
- "Valladares",
- "Florida",
- "Pizarro",
- "Estrada Madrid",
- "Sanjurjo Badía"
-];
+import { useTranslation } from "react-i18next";
export default function StopList() {
+ const { t } = useTranslation();
const [data, setData] = useState<Stop[] | null>(null)
const [searchResults, setSearchResults] = useState<Stop[] | null>(null);
const searchTimeout = useRef<NodeJS.Timeout | null>(null);
- const randomPlaceholder = useMemo(() => placeholders[Math.floor(Math.random() * placeholders.length)], []);
+ const randomPlaceholder = useMemo(() => t('stoplist.search_placeholder'), [t]);
const fuse = useMemo(() => new Fuse(data || [], { threshold: 0.3, keys: ['name.original'] }), [data]);
useEffect(() => {