last translations
This commit is contained in:
+4
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user