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