- }>
+ }>
{tabContent[activeTab]}
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index cc0d9e9..7745088 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -1,4 +1,6 @@
import { type ReactNode, useState, useEffect, useMemo, useRef } from "react";
+import { useTranslation } from "react-i18next";
+import type { TFunction } from "i18next";
import { ErrorBoundary } from "./ErrorBoundary";
import { useQuery } from "@tanstack/react-query";
import { useRegionSelection, useRegions } from "../hooks/useRegion";
@@ -8,6 +10,7 @@ import { useRateLimit } from "../hooks/useRateLimit";
import { useTheme } from "../hooks/useTheme";
import { Dropdown } from "./Dropdown";
import { BottomNav } from "./BottomNav";
+import { LanguagePicker } from "./LanguagePicker";
import { BeaconWordmark } from "./BeaconWordmark";
import { getIatas } from "../api/client";
import { ENABLED_TABS, ENABLED_THEME_IDS, selectableThemes, APP_NAME, GITHUB_URL } from "../lib/constants";
@@ -16,6 +19,7 @@ import type { WsManager } from "../api/ws-manager";
// header widgets: WS status, region picker, theme picker
function LiveBadge({ wsManager }: { wsManager: WsManager }) {
+ const { t } = useTranslation();
const { status } = useWsStatus(wsManager);
const [staleStr, setStaleStr] = useState("");
@@ -34,7 +38,7 @@ function LiveBadge({ wsManager }: { wsManager: WsManager }) {
return (
- LIVE
+ {t("connection.live")}
);
}
@@ -42,20 +46,21 @@ function LiveBadge({ wsManager }: { wsManager: WsManager }) {
if (status === "connecting") {
return (
- STALE {staleStr}
+ {t("connection.stale", { age: staleStr })}
);
}
return (
- OFFLINE
+ {t("connection.offline")}
);
}
// Shown while the API is throttling us; the countdown tells users the blank tables are temporary.
function RateLimitBadge() {
+ const { t } = useTranslation();
const until = useRateLimit();
const [remaining, setRemaining] = useState(0);
@@ -74,7 +79,7 @@ function RateLimitBadge() {
return (
- RATE LIMITED {remaining}s
+ {t("connection.rateLimited", { seconds: remaining })}
);
}
@@ -95,14 +100,14 @@ function CheckBox({ checked }: { checked: boolean }) {
}
// Compact header summary of the active selection, e.g. "ALL", "YVR, YYJ", "2 regions", "1 region · 3 IATA".
-function regionSummaryLabel(selection: RegionSelection): string {
- if (isAllRegions(selection)) return "ALL";
+function regionSummaryLabel(selection: RegionSelection, t: TFunction): string {
+ if (isAllRegions(selection)) return t("region.allShort");
const parts: string[] = [];
if (selection.regions.length > 0) {
- parts.push(`${selection.regions.length} region${selection.regions.length > 1 ? "s" : ""}`);
+ parts.push(t("region.count", { count: selection.regions.length }));
}
if (selection.iatas.length > 0) {
- parts.push(selection.iatas.length <= 2 ? selection.iatas.join(", ") : `${selection.iatas.length} IATA`);
+ parts.push(selection.iatas.length <= 2 ? selection.iatas.join(", ") : t("region.iataCount", { count: selection.iatas.length }));
}
return parts.join(" · ");
}
@@ -110,6 +115,7 @@ function regionSummaryLabel(selection: RegionSelection): string {
// Grouped multi-select: regions (each expands to its member IATAs) on top, then individual IATAs.
// Toggling keeps the dropdown open so several can be picked; "All Regions" clears the selection.
function RegionSelector() {
+ const { t } = useTranslation();
const { selection } = useRegionSelection();
return (
@@ -122,8 +128,8 @@ function RegionSelector() {
className="flex items-center gap-1.5 bg-bg-raised border border-border rounded px-3 py-1 text-text-bright font-mono text-xs font-semibold hover:border-text-dim/30 transition-colors"
onClick={toggle}
>
-
REGION
- {regionSummaryLabel(selection)}
+
{t("region.label")}
+ {regionSummaryLabel(selection, t)}
▾
)}
@@ -135,6 +141,7 @@ function RegionSelector() {
// Split out from RegionSelector so the filter query lives and dies with the open panel.
function RegionSelectorPanel() {
+ const { t } = useTranslation();
const { selection, setSelection } = useRegionSelection();
const { regions } = useRegions();
const [query, setQuery] = useState("");
@@ -193,7 +200,7 @@ function RegionSelectorPanel() {
);
}, [iatas, q]);
- const showAll = !q || "all regions".includes(q);
+ const showAll = !q || t("region.allRegions").toLowerCase().includes(q);
const showIataGroup = !iatas || shownIatas.length > 0; // keep the group while loading/failed
const hasRowsAbove = showAll || shownRegions.length > 0;
@@ -213,7 +220,8 @@ function RegionSelectorPanel() {
setQuery("");
}
}}
- placeholder="Filter IATA or name…"
+ aria-label={t("region.filter")}
+ placeholder={t("region.filter")}
className="w-full text-[16px] font-mono bg-bg-surface border border-border rounded px-2 py-1 text-text-bright placeholder:text-text-dim"
/>
@@ -230,14 +238,14 @@ function RegionSelectorPanel() {
>
{/* spacer matching the checkbox column so ALL/code/name align with the rows below */}