diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-24 16:14:28 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-24 16:14:28 +0200 |
| commit | ecb73e1684b42265af3f8d93541600e4d0f9c414 (patch) | |
| tree | 26e413973b32de0367aa06cfc0df329c67733821 /src/frontend/app/routes | |
| parent | f65b4e1e0d5648038823962349279be4badc68ed (diff) | |
Implement i18n
Closes #18
Diffstat (limited to 'src/frontend/app/routes')
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.tsx | 2 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 2 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 69 | ||||
| -rw-r--r-- | src/frontend/app/routes/stoplist.tsx | 16 |
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(() => { |
