aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/routes')
-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
4 files changed, 47 insertions, 42 deletions
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(() => {