last translations

This commit is contained in:
Vincent van der Wal
2026-08-01 18:10:36 +02:00
parent 578ee3c8ce
commit 3fda55ddd2
11 changed files with 1735 additions and 1683 deletions
+4 -1
View File
@@ -332,5 +332,8 @@
"cadence_every_hours": "alle {hours} h", "cadence_every_hours": "alle {hours} h",
"cadence_daily": "täglich", "cadence_daily": "täglich",
"cadence_monthly": "monatlich", "cadence_monthly": "monatlich",
"cadence_varies": "variiert" "cadence_varies": "variiert",
"model_group_automatic": "Automatisch",
"model_group_reanalysis": "ECMWF-Reanalyse",
"model_group_regional_reanalysis": "Regionale Reanalyse"
} }
+4 -1
View File
@@ -332,5 +332,8 @@
"cadence_every_hours": "every {hours} h", "cadence_every_hours": "every {hours} h",
"cadence_daily": "daily", "cadence_daily": "daily",
"cadence_monthly": "monthly", "cadence_monthly": "monthly",
"cadence_varies": "varies" "cadence_varies": "varies",
"model_group_automatic": "Automatic",
"model_group_reanalysis": "ECMWF reanalysis",
"model_group_regional_reanalysis": "Regional reanalysis"
} }
+4 -1
View File
@@ -332,5 +332,8 @@
"cadence_every_hours": "cada {hours} h", "cadence_every_hours": "cada {hours} h",
"cadence_daily": "a diario", "cadence_daily": "a diario",
"cadence_monthly": "cada mes", "cadence_monthly": "cada mes",
"cadence_varies": "variable" "cadence_varies": "variable",
"model_group_automatic": "Automático",
"model_group_reanalysis": "Reanálisis del ECMWF",
"model_group_regional_reanalysis": "Reanálisis regional"
} }
+4 -1
View File
@@ -332,5 +332,8 @@
"cadence_every_hours": "toutes les {hours} h", "cadence_every_hours": "toutes les {hours} h",
"cadence_daily": "chaque jour", "cadence_daily": "chaque jour",
"cadence_monthly": "chaque mois", "cadence_monthly": "chaque mois",
"cadence_varies": "variable" "cadence_varies": "variable",
"model_group_automatic": "Automatique",
"model_group_reanalysis": "Réanalyse ECMWF",
"model_group_regional_reanalysis": "Réanalyse régionale"
} }
+4 -1
View File
@@ -332,5 +332,8 @@
"cadence_every_hours": "ogni {hours} h", "cadence_every_hours": "ogni {hours} h",
"cadence_daily": "ogni giorno", "cadence_daily": "ogni giorno",
"cadence_monthly": "ogni mese", "cadence_monthly": "ogni mese",
"cadence_varies": "variabile" "cadence_varies": "variabile",
"model_group_automatic": "Automatico",
"model_group_reanalysis": "Rianalisi ECMWF",
"model_group_regional_reanalysis": "Rianalisi regionale"
} }
+5 -1
View File
@@ -45,7 +45,11 @@
<SupporterIcon class="h-7 w-7" /> <SupporterIcon class="h-7 w-7" />
</div> </div>
<h2 class="text-xl font-bold tracking-tight">{m.supporter_gate_title({ feature })}</h2> <!-- the feature name comes from a page subtitle ("météo historique"), which is
lowercase mid-sentence and has to be lifted at the start of this one -->
<h2 class="text-xl font-bold tracking-tight first-letter:uppercase">
{m.supporter_gate_title({ feature })}
</h2>
<p class="mx-auto mt-1.5 max-w-sm text-sm text-muted-foreground"> <p class="mx-auto mt-1.5 max-w-sm text-sm text-muted-foreground">
{m.supporter_gate_body({ price })} {m.supporter_gate_body({ price })}
</p> </p>
+18 -5
View File
@@ -84,28 +84,41 @@ async function loadTile(key: string): Promise<CityRow[]> {
*/ */
const SEARCH_RADII_KM = [200, 400, 1500]; const SEARCH_RADII_KM = [200, 400, 1500];
/**
* How far out still counts as "the place you are already looking at". A village
* ends where its fields start; London's own boroughs sit 20 km from its centre
* and share its weather, so the radius grows with the size of the location.
*/
function samePlaceKm(population: number): number {
return 10 + 15 * Math.min(1, population / 5_000_000);
}
/** /**
* Returns up to `count` cities around the given point, ordered by distance. * Returns up to `count` cities around the given point, ordered by distance.
* *
* Candidates are ranked by population damped by distance, so a town up the * Candidates are ranked by population damped by distance, so a town up the
* valley can outrank a metropolis three hours away, and picks have to keep * valley can outrank a metropolis three hours away, and picks have to keep
* their distance from each other - otherwise a place like New York fills the * their distance from each other - otherwise a place like New York fills the
* whole list with its own boroughs. * list with its own boroughs, and a big city fills it with commuter suburbs
* that share its weather anyway.
*/ */
export async function findNearbyCities( export async function findNearbyCities(
latitude: number, latitude: number,
longitude: number, longitude: number,
count = 10 count = 10,
population = 0
): Promise<NearbyCity[]> { ): Promise<NearbyCity[]> {
const rows = await loadTile(tileKey(latitude, longitude)); const rows = await loadTile(tileKey(latitude, longitude));
if (rows.length === 0) return []; if (rows.length === 0) return [];
const ownFootprintKm = samePlaceKm(population);
let best: NearbyCity[] = []; let best: NearbyCity[] = [];
for (const radius of SEARCH_RADII_KM) { for (const radius of SEARCH_RADII_KM) {
const halfWeightKm = radius / 4; const halfWeightKm = radius / 2;
// far-apart picks in a wide search, tight ones when everything is close // far-apart picks in a wide search, tight ones when everything is close
const minSeparationKm = Math.max(15, radius / 20); const minSeparationKm = Math.max(25, radius / 20);
const scored = rows const scored = rows
.map(([id, name, countryCode, lat, lon, popK]) => { .map(([id, name, countryCode, lat, lon, popK]) => {
@@ -123,7 +136,7 @@ export async function findNearbyCities(
score: popK / (1 + (dist / halfWeightKm) ** 2) score: popK / (1 + (dist / halfWeightKm) ** 2)
}; };
}) })
.filter(({ city }) => city.distanceKm >= minSeparationKm && city.distanceKm <= radius) .filter(({ city }) => city.distanceKm >= ownFootprintKm && city.distanceKm <= radius)
.sort((a, b) => b.score - a.score); .sort((a, b) => b.score - a.score);
const picked: NearbyCity[] = []; const picked: NearbyCity[] = [];
+2 -2
View File
@@ -1,6 +1,6 @@
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { deLocalizeHref, getLocale, localizeHref } from '$lib/paraglide/runtime'; import { deLocalizeHref, localizeHref } from '$lib/paraglide/runtime';
import type { GeoLocation } from '$lib/stores/settings'; import type { GeoLocation } from '$lib/stores/settings';
@@ -115,7 +115,7 @@ export async function resolveLocationFromRoute({
location = candidate; location = candidate;
} else { } else {
const res = await event.fetch( const res = await event.fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(urlLocationName)}&count=1&language=${encodeURIComponent(getLocale())}&format=json` `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(urlLocationName)}&count=1&language=en&format=json`
); );
if (!res.ok) error(404, 'Location not found'); if (!res.ok) error(404, 'Location not found');
const geocodingResponse = await res.json(); const geocodingResponse = await res.json();
@@ -541,6 +541,8 @@
<NearbyCities <NearbyCities
latitude={location.latitude} latitude={location.latitude}
longitude={location.longitude} longitude={location.longitude}
population={location.population}
countryCode={location.country_code}
{selectedDayKey} {selectedDayKey}
units={$storedUnits} units={$storedUnits}
/> />
@@ -24,6 +24,16 @@
groups.flatMap((group) => group.models).find((mo) => mo.value === selectedModel) groups.flatMap((group) => group.models).find((mo) => mo.value === selectedModel)
); );
let modelLabel = $derived(model?.label ?? selectedModel); let modelLabel = $derived(model?.label ?? selectedModel);
// Provider groups are brand names and stay as they are; the few descriptive
// ones are the only headings that need translating.
const GROUP_LABELS: Record<string, () => string> = {
auto: m.model_group_automatic,
era5: m.model_group_reanalysis,
regional: m.model_group_regional_reanalysis
};
const groupLabel = (group: { value: string; label: string }) =>
GROUP_LABELS[group.value]?.() ?? group.label;
// The catalogue stores update cadences as English shorthand ("every 6 h"); // The catalogue stores update cadences as English shorthand ("every 6 h");
// map the handful of forms onto messages instead of translating the data. // map the handful of forms onto messages instead of translating the data.
function updateLabel(update: string): string { function updateLabel(update: string): string {
@@ -99,7 +109,7 @@
<Select.GroupHeading <Select.GroupHeading
class="text-[10.5px] font-bold tracking-wider text-primary/80 uppercase" class="text-[10.5px] font-bold tracking-wider text-primary/80 uppercase"
> >
{group.label} {groupLabel(group)}
</Select.GroupHeading> </Select.GroupHeading>
{#each group.models as mo (mo.value)} {#each group.models as mo (mo.value)}
<Select.Item class="cursor-pointer" value={mo.value} label={mo.label}> <Select.Item class="cursor-pointer" value={mo.value} label={mo.label}>
@@ -16,12 +16,16 @@
interface Props { interface Props {
latitude: number; latitude: number;
longitude: number; longitude: number;
/** of the location itself: sets how far out counts as "still here" */
population: number | undefined;
/** of the location itself: only foreign countries are worth naming */
countryCode: string | undefined;
/** "yyyy-MM-dd" of the day the rest of the page is showing */ /** "yyyy-MM-dd" of the day the rest of the page is showing */
selectedDayKey: string; selectedDayKey: string;
units: UnitPrefs; units: UnitPrefs;
} }
let { latitude, longitude, selectedDayKey, units }: Props = $props(); let { latitude, longitude, population, countryCode, selectedDayKey, units }: Props = $props();
const COUNT = 10; const COUNT = 10;
@@ -45,6 +49,7 @@
$effect(() => { $effect(() => {
const lat = latitude; const lat = latitude;
const lon = longitude; const lon = longitude;
const pop = population ?? 0;
const unitPrefs = { ...units }; const unitPrefs = { ...units };
let cancelled = false; let cancelled = false;
@@ -54,7 +59,7 @@
(async () => { (async () => {
try { try {
const found = await findNearbyCities(lat, lon, COUNT); const found = await findNearbyCities(lat, lon, COUNT, pop);
if (cancelled) return; if (cancelled) return;
cities = found; cities = found;
@@ -118,8 +123,11 @@
</svg> </svg>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium group-hover:underline">{city.name}</div> <div class="truncate text-sm font-medium group-hover:underline">{city.name}</div>
<div class="text-[11px] text-muted-foreground"> <div class="truncate text-[11px] text-muted-foreground">
{distance(city.distanceKm)} · {countryName(city.countryCode)} <!-- the country only earns its space when it isn't the one you are in -->
{distance(city.distanceKm)}{city.countryCode === countryCode
? ''
: ` · ${countryName(city.countryCode)}`}
</div> </div>
</div> </div>
<div class="text-right text-sm tabular-nums"> <div class="text-right text-sm tabular-nums">