Skip to content

Commit 7e537d0

Browse files
committed
Add multilingual support with German translations and update language handling in the weather dashboard
1 parent 9749091 commit 7e537d0

8 files changed

Lines changed: 129 additions & 45 deletions

File tree

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
OPENWEATHERMAP_API_KEY=# Replace with your OpenWeatherMap API key
22
OPENWEATHERMAP_API_VERSION=3.0 # "3.0" for One Call 3.0, "2.5" for One Call 2.5, or "free" for the free Weather/Forecast API
3+
LANGUAGE=en # "en" for English, "de" for German

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ cd weather-forecast
4343
- `OPENWEATHERMAP_API_VERSION` — The API tier to use:
4444
- `3.0` (default) — [One Call API 3.0](https://openweathermap.org/api/one-call-3) (paid subscription)
4545
- `2.5`[One Call API 2.5](https://openweathermap.org/api/one-call-api) (legacy paid subscription)
46-
- `free` — Free tier using the standard [Weather](https://openweathermap.org/current) + [Forecast](https://openweathermap.org/forecast5) endpoints (no UV index, 3-hour forecast intervals instead of hourly)
46+
- `free` — Free tier using the standard [Weather](https://openweathermap.org/current) + [Forecast](https://openweathermap.org/forecast5) endpoints (no UV index, 3-hour forecast intervals instead of hourly)
47+
- `LANGUAGE` — UI language: `en` (default) for English or `de` for German. Also affects weather descriptions from the API and date/time formatting.
48+
4749
> [!Note]
4850
> Choose the API version based on your OpenWeatherMap subscription. The `free` option allows you to run the dashboard without a paid subscription, but with limited features and less granular forecast data.
4951
50-
1. Install the dependencies
52+
4. Install the dependencies
5153

5254
```shell
5355
npm install

src/components/WeatherChart.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import annotationPlugin from 'chartjs-plugin-annotation';
88

99
Chart.register(...registerables, ChartDataLabels, annotationPlugin);
1010

11-
export default function WeatherChart({ labels, temperatureData, precipitationData, uviData, sunrise, sunset }) {
11+
export default function WeatherChart({ labels, temperatureData, precipitationData, uviData, sunrise, sunset, tempLabel = "Temp °C", rainLabel = "Regen mm", uvLabel = "UV" }) {
1212
const canvasRef = useRef(null);
1313

1414
useEffect(() => {
@@ -92,7 +92,7 @@ export default function WeatherChart({ labels, temperatureData, precipitationDat
9292
labels,
9393
datasets: [
9494
{
95-
label: "Temp °C",
95+
label: tempLabel,
9696
data: temperatureData,
9797
borderColor: colors.tempLine,
9898
backgroundColor: colors.tempFill,
@@ -103,7 +103,7 @@ export default function WeatherChart({ labels, temperatureData, precipitationDat
103103
borderWidth: 1.5,
104104
},
105105
{
106-
label: "Regen mm",
106+
label: rainLabel,
107107
data: precipitationData,
108108
borderColor: colors.precipLine,
109109
backgroundColor: colors.precipFill,
@@ -114,7 +114,7 @@ export default function WeatherChart({ labels, temperatureData, precipitationDat
114114
borderWidth: 1.5,
115115
},
116116
...(uviData.length > 0 ? [{
117-
label: "UV",
117+
label: uvLabel,
118118
data: uviData,
119119
borderColor: colors.uviLine,
120120
backgroundColor: colors.uviFill,

src/lib/i18n.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const translations = {
2+
de: {
3+
// index.astro - weather details
4+
feelsLike: "Gefühlt",
5+
sunrise: "Aufgang",
6+
sunset: "Untergang",
7+
wind: "Wind",
8+
humidity: "Feuchte",
9+
pressure: "Druck",
10+
uv: "UV",
11+
visibility: "Sicht",
12+
clouds: "Wolken",
13+
weatherUnavailable: "Wetterdaten nicht verfügbar",
14+
autoReload: "Die Seite wird automatisch neu geladen.",
15+
// setup.astro
16+
dopplerRadar: "DOPPLER RADAR",
17+
configuration: "Konfiguration",
18+
location: "Standort",
19+
or: "oder",
20+
cityZip: "Stadt / PLZ",
21+
country: "Land",
22+
search: "Suchen...",
23+
start: "Starten",
24+
locationNotFound: "Standort nicht gefunden",
25+
saveError: "Fehler beim Speichern der Position",
26+
// chart
27+
tempLabel: "Temp °C",
28+
rainLabel: "Regen mm",
29+
uvLabel: "UV",
30+
// locale settings
31+
locale: "de-DE",
32+
owmLang: "DE",
33+
},
34+
en: {
35+
feelsLike: "Feels like",
36+
sunrise: "Sunrise",
37+
sunset: "Sunset",
38+
wind: "Wind",
39+
humidity: "Humidity",
40+
pressure: "Pressure",
41+
uv: "UV",
42+
visibility: "Visibility",
43+
clouds: "Clouds",
44+
weatherUnavailable: "Weather data unavailable",
45+
autoReload: "The page will reload automatically.",
46+
dopplerRadar: "DOPPLER RADAR",
47+
configuration: "Configuration",
48+
location: "Location",
49+
or: "or",
50+
cityZip: "City / ZIP",
51+
country: "Country",
52+
search: "Search...",
53+
start: "Start",
54+
locationNotFound: "Location not found",
55+
saveError: "Failed to save location",
56+
tempLabel: "Temp °C",
57+
rainLabel: "Rain mm",
58+
uvLabel: "UV",
59+
locale: "en-US",
60+
owmLang: "EN",
61+
},
62+
} as const;
63+
64+
export type Language = keyof typeof translations;
65+
export type TranslationKey = keyof (typeof translations)["de"];
66+
67+
export function getTranslations(lang?: string) {
68+
const key = (lang && lang in translations ? lang : "en") as Language;
69+
return translations[key];
70+
}

src/lib/weather-api.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,25 @@ export async function fetchWeather(
3434
lat: string,
3535
lon: string,
3636
apiKey: string,
37-
apiVersion: string
37+
apiVersion: string,
38+
owmLang: string = "EN"
3839
): Promise<{ data: NormalizedWeather | null; error: string | null }> {
3940
if (apiVersion === "free") {
40-
return fetchFreeApi(lat, lon, apiKey);
41+
return fetchFreeApi(lat, lon, apiKey, owmLang);
4142
}
42-
return fetchOneCall(lat, lon, apiKey, apiVersion);
43+
return fetchOneCall(lat, lon, apiKey, apiVersion, owmLang);
4344
}
4445

4546
async function fetchOneCall(
4647
lat: string,
4748
lon: string,
4849
apiKey: string,
49-
version: string
50+
version: string,
51+
owmLang: string
5052
): Promise<{ data: NormalizedWeather | null; error: string | null }> {
5153
const v = version === "2.5" ? "2.5" : "3.0";
5254
const res = await fetch(
53-
`https://api.openweathermap.org/data/${v}/onecall?lat=${lat}&lon=${lon}&lang=DE&units=metric&exclude=minutely,alerts&appid=${apiKey}`
55+
`https://api.openweathermap.org/data/${v}/onecall?lat=${lat}&lon=${lon}&lang=${owmLang}&units=metric&exclude=minutely,alerts&appid=${apiKey}`
5456
);
5557
const json = await res.json();
5658
if (!res.ok) {
@@ -62,14 +64,15 @@ async function fetchOneCall(
6264
async function fetchFreeApi(
6365
lat: string,
6466
lon: string,
65-
apiKey: string
67+
apiKey: string,
68+
owmLang: string
6669
): Promise<{ data: NormalizedWeather | null; error: string | null }> {
6770
const [weatherRes, forecastRes] = await Promise.all([
6871
fetch(
69-
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&lang=DE&units=metric&appid=${apiKey}`
72+
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&lang=${owmLang}&units=metric&appid=${apiKey}`
7073
),
7174
fetch(
72-
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&lang=DE&units=metric&appid=${apiKey}`
75+
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&lang=${owmLang}&units=metric&appid=${apiKey}`
7376
),
7477
]);
7578

src/pages/api/set-location.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { APIRoute } from "astro";
44
export const POST: APIRoute = async ({ request, cookies }) => {
55
const { lat, lon, other, country } = await request.json();
66
const env = import.meta.env;
7-
const countryCode = country || "DE";
7+
const countryCode = country || "US";
88

99
if (other) {
1010
const key = env.OPENWEATHERMAP_API_KEY;

src/pages/index.astro

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import {
1313
IconCloud,
1414
} from "@tabler/icons-react";
1515
import { fetchWeather } from "@/lib/weather-api";
16+
import { getTranslations } from "@/lib/i18n";
1617
1718
const env = import.meta.env;
19+
const t = getTranslations(env.LANGUAGE);
1820
const latCookie = Astro.cookies.get("lat");
1921
const lonCookie = Astro.cookies.get("lon");
2022
const cityCookie = Astro.cookies.get("city");
@@ -29,11 +31,11 @@ if (!lat || !lon) {
2931
3032
const apiVersion = env.OPENWEATHERMAP_API_VERSION || "3.0";
3133
const { data: oneCall, error: apiError } = await fetchWeather(
32-
lat, lon, env.OPENWEATHERMAP_API_KEY, apiVersion
34+
lat, lon, env.OPENWEATHERMAP_API_KEY, apiVersion, t.owmLang
3335
);
3436
3537
const time = new Date();
36-
const currentDate = time.toLocaleDateString("de-DE", {
38+
const currentDate = time.toLocaleDateString(t.locale, {
3739
timeZone: "Europe/Berlin",
3840
weekday: "long",
3941
month: "long",
@@ -52,7 +54,7 @@ const hourlyUVIndex = rawUVI.some((v) => v > 0) ? rawUVI : [];
5254
5355
function formatToBerlinTime(unixTimestamp) {
5456
const date = new Date(unixTimestamp * 1000);
55-
return date.toLocaleTimeString("de-DE", {
57+
return date.toLocaleTimeString(t.locale, {
5658
timeZone: "Europe/Berlin",
5759
hour: "2-digit",
5860
minute: "2-digit",
@@ -62,7 +64,7 @@ function formatToBerlinTime(unixTimestamp) {
6264
6365
function formatToBerlinDate(unixTimestamp) {
6466
const date = new Date(unixTimestamp * 1000);
65-
return date.toLocaleDateString("de-DE", {
67+
return date.toLocaleDateString(t.locale, {
6668
timeZone: "Europe/Berlin",
6769
weekday: "short",
6870
});
@@ -558,9 +560,9 @@ function formatToBerlinDate(unixTimestamp) {
558560
{apiError ? (
559561
<div class="flex-1 flex items-center justify-center">
560562
<div class="hud-card p-6 text-center max-w-md">
561-
<p class="text-2xl font-semibold dash-primary mb-2">Wetterdaten nicht verfügbar</p>
563+
<p class="text-2xl font-semibold dash-primary mb-2">{t.weatherUnavailable}</p>
562564
<p class="text-sm dash-muted font-mono">{apiError}</p>
563-
<p class="text-xs dash-muted mt-3">Die Seite wird automatisch neu geladen.</p>
565+
<p class="text-xs dash-muted mt-3">{t.autoReload}</p>
564566
</div>
565567
</div>
566568
) : (<Fragment>
@@ -589,7 +591,7 @@ function formatToBerlinDate(unixTimestamp) {
589591
{Math.round(oneCall.current.temp)}°
590592
</p>
591593
<p class="font-mono text-sm dash-muted -mt-2">
592-
Gefühlt {Math.round(oneCall.current.feels_like)}°
594+
{t.feelsLike} {Math.round(oneCall.current.feels_like)}°
593595
</p>
594596
</div>
595597
</div>
@@ -604,42 +606,42 @@ function formatToBerlinDate(unixTimestamp) {
604606
<div class="detail-item">
605607
<IconSunrise size={32} className="detail-icon" />
606608
<div class="detail-text">
607-
<span class="detail-label">Aufgang</span>
609+
<span class="detail-label">{t.sunrise}</span>
608610
<span class="detail-value">{formatToBerlinTime(oneCall.current.sunrise)}</span>
609611
</div>
610612
</div>
611613
<div class="detail-item">
612614
<IconSunset size={32} className="detail-icon" />
613615
<div class="detail-text">
614-
<span class="detail-label">Untergang</span>
616+
<span class="detail-label">{t.sunset}</span>
615617
<span class="detail-value">{formatToBerlinTime(oneCall.current.sunset)}</span>
616618
</div>
617619
</div>
618620
<div class="detail-item">
619621
<IconWind size={32} className="detail-icon" />
620622
<div class="detail-text">
621-
<span class="detail-label">Wind</span>
623+
<span class="detail-label">{t.wind}</span>
622624
<span class="detail-value">{oneCall.current.wind_speed}<span class="detail-unit">m/s</span></span>
623625
</div>
624626
</div>
625627
<div class="detail-item">
626628
<IconDroplet size={32} className="detail-icon" />
627629
<div class="detail-text">
628-
<span class="detail-label">Feuchte</span>
630+
<span class="detail-label">{t.humidity}</span>
629631
<span class="detail-value">{oneCall.current.humidity}<span class="detail-unit">%</span></span>
630632
</div>
631633
</div>
632634
<div class="detail-item">
633635
<IconGauge size={32} className="detail-icon" />
634636
<div class="detail-text">
635-
<span class="detail-label">Druck</span>
637+
<span class="detail-label">{t.pressure}</span>
636638
<span class="detail-value">{oneCall.current.pressure}<span class="detail-unit">hPa</span></span>
637639
</div>
638640
</div>
639641
<div class="detail-item">
640642
<IconUvIndex size={32} className="detail-icon" />
641643
<div class="detail-text">
642-
<span class="detail-label">UV</span>
644+
<span class="detail-label">{t.uv}</span>
643645
<span class:list={[
644646
"detail-value",
645647
{
@@ -654,14 +656,14 @@ function formatToBerlinDate(unixTimestamp) {
654656
<div class="detail-item">
655657
<IconEye size={32} className="detail-icon" />
656658
<div class="detail-text">
657-
<span class="detail-label">Sicht</span>
659+
<span class="detail-label">{t.visibility}</span>
658660
<span class="detail-value">{oneCall.current.visibility / 1000}<span class="detail-unit">km</span></span>
659661
</div>
660662
</div>
661663
<div class="detail-item">
662664
<IconCloud size={32} className="detail-icon" />
663665
<div class="detail-text">
664-
<span class="detail-label">Wolken</span>
666+
<span class="detail-label">{t.clouds}</span>
665667
<span class="detail-value">{oneCall.current.clouds}<span class="detail-unit">%</span></span>
666668
</div>
667669
</div>
@@ -678,6 +680,9 @@ function formatToBerlinDate(unixTimestamp) {
678680
uviData={hourlyUVIndex}
679681
sunrise={formatToBerlinTime(oneCall.current.sunrise)}
680682
sunset={formatToBerlinTime(oneCall.current.sunset)}
683+
tempLabel={t.tempLabel}
684+
rainLabel={t.rainLabel}
685+
uvLabel={t.uvLabel}
681686
/>
682687
</div>
683688

0 commit comments

Comments
 (0)