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_daily": "täglich",
"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_daily": "daily",
"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_daily": "a diario",
"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_daily": "chaque jour",
"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_daily": "ogni giorno",
"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" />
</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">
{m.supporter_gate_body({ price })}
</p>
+18 -5
View File
@@ -84,28 +84,41 @@ async function loadTile(key: string): Promise<CityRow[]> {
*/
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.
*
* 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
* 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(
latitude: number,
longitude: number,
count = 10
count = 10,
population = 0
): Promise<NearbyCity[]> {
const rows = await loadTile(tileKey(latitude, longitude));
if (rows.length === 0) return [];
const ownFootprintKm = samePlaceKm(population);
let best: NearbyCity[] = [];
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
const minSeparationKm = Math.max(15, radius / 20);
const minSeparationKm = Math.max(25, radius / 20);
const scored = rows
.map(([id, name, countryCode, lat, lon, popK]) => {
@@ -123,7 +136,7 @@ export async function findNearbyCities(
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);
const picked: NearbyCity[] = [];
+2 -2
View File
@@ -1,6 +1,6 @@
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';
@@ -115,7 +115,7 @@ export async function resolveLocationFromRoute({
location = candidate;
} else {
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');
const geocodingResponse = await res.json();
@@ -541,6 +541,8 @@
<NearbyCities
latitude={location.latitude}
longitude={location.longitude}
population={location.population}
countryCode={location.country_code}
{selectedDayKey}
units={$storedUnits}
/>
@@ -24,6 +24,16 @@
groups.flatMap((group) => group.models).find((mo) => mo.value === 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");
// map the handful of forms onto messages instead of translating the data.
function updateLabel(update: string): string {
@@ -99,7 +109,7 @@
<Select.GroupHeading
class="text-[10.5px] font-bold tracking-wider text-primary/80 uppercase"
>
{group.label}
{groupLabel(group)}
</Select.GroupHeading>
{#each group.models as mo (mo.value)}
<Select.Item class="cursor-pointer" value={mo.value} label={mo.label}>
@@ -16,12 +16,16 @@
interface Props {
latitude: 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 */
selectedDayKey: string;
units: UnitPrefs;
}
let { latitude, longitude, selectedDayKey, units }: Props = $props();
let { latitude, longitude, population, countryCode, selectedDayKey, units }: Props = $props();
const COUNT = 10;
@@ -45,6 +49,7 @@
$effect(() => {
const lat = latitude;
const lon = longitude;
const pop = population ?? 0;
const unitPrefs = { ...units };
let cancelled = false;
@@ -54,7 +59,7 @@
(async () => {
try {
const found = await findNearbyCities(lat, lon, COUNT);
const found = await findNearbyCities(lat, lon, COUNT, pop);
if (cancelled) return;
cities = found;
@@ -118,8 +123,11 @@
</svg>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium group-hover:underline">{city.name}</div>
<div class="text-[11px] text-muted-foreground">
{distance(city.distanceKm)} · {countryName(city.countryCode)}
<div class="truncate text-[11px] text-muted-foreground">
<!-- 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 class="text-right text-sm tabular-nums">