fix(i18n): translate the Street Level and Draw panels

Both panels landed after the original string extraction, so they rendered
English in every locale. Mark their markup and runtime text (status, SINCE
stops, provider tooltips, coverage meta, viewer captions, draw hints and
colours), the layer-list names kept in PANEL_LABELS, and add 90 es and ru
entries. Street Level is «Уличные снимки» / «A pie de calle».

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-12 18:32:28 +04:00
1 parent eee905e787
commit 6ff7afcc8b
13 files changed
+378 -98

No files matched your search

+66 -30
View File
@@ -1,4 +1,5 @@
import { keySetupRequirement } from '../keySetupCore.mjs';
import { keySetupRequirementEnvVars } from '../keySetupCore.mjs';
import { formatNumber, msg, t, tn } from '../i18n/index.js';
// Street Level UI state to panel strings and flags. Pure, so it is testable.
@@ -6,15 +7,15 @@ const DAY_MS = 86_400_000;
/** SINCE slider stops, in relative days so a share link keeps its meaning. */
export const SINCE_STOPS = Object.freeze([
Object.freeze({ days: 0, label: 'ANY DATE' }),
Object.freeze({ days: 3652, label: 'LAST 10 YEARS' }),
Object.freeze({ days: 1826, label: 'LAST 5 YEARS' }),
Object.freeze({ days: 1095, label: 'LAST 3 YEARS' }),
Object.freeze({ days: 730, label: 'LAST 2 YEARS' }),
Object.freeze({ days: 365, label: 'LAST YEAR' }),
Object.freeze({ days: 182, label: 'LAST 6 MONTHS' }),
Object.freeze({ days: 91, label: 'LAST 3 MONTHS' }),
Object.freeze({ days: 30, label: 'LAST MONTH' }),
Object.freeze({ days: 0, label: msg('ANY DATE') }),
Object.freeze({ days: 3652, label: msg('LAST 10 YEARS') }),
Object.freeze({ days: 1826, label: msg('LAST 5 YEARS') }),
Object.freeze({ days: 1095, label: msg('LAST 3 YEARS') }),
Object.freeze({ days: 730, label: msg('LAST 2 YEARS') }),
Object.freeze({ days: 365, label: msg('LAST YEAR') }),
Object.freeze({ days: 182, label: msg('LAST 6 MONTHS') }),
Object.freeze({ days: 91, label: msg('LAST 3 MONTHS') }),
Object.freeze({ days: 30, label: msg('LAST MONTH') }),
]);
/** The exact stop for a day count, else the nearest one. */
@@ -34,13 +35,19 @@ export function sinceStopIndex(days) {
function presentSince(days, now) {
const value = Number(days) || 0;
const index = sinceStopIndex(value);
if (value <= 0) return { index, days: 0, label: 'ANY DATE' };
if (value <= 0) return { index, days: 0, label: t('ANY DATE') };
const stop = SINCE_STOPS[index];
const window = stop.days === value ? stop.label : `LAST ${value} DAYS`;
const window =
stop.days === value
? t(stop.label)
: tn(value, 'LAST {count} DAY', 'LAST {count} DAYS');
return {
index,
days: value,
label: `${window} · SINCE ${formatDate(now - value * DAY_MS)}`,
label: t('{window} · SINCE {date}', {
window,
date: formatDate(now - value * DAY_MS),
}),
};
}
@@ -53,18 +60,28 @@ function formatDate(ms) {
}
}
/** The Provider Settings hint for a missing key, or '' for a hidden key. */
function keyRequirement(id) {
const envVars = keySetupRequirementEnvVars(id);
return envVars
? t('Needs {envVars} — add it in Provider Settings', { envVars })
: '';
}
function presentStatus(state) {
const pressed = state.enabled === true;
const title = pressed ? 'Turn Street Level off' : 'Turn Street Level on';
const title = pressed
? t('Turn Street Level off')
: t('Turn Street Level on');
if (state.keyRejected)
return { text: 'KEY REJECTED', tone: 'warn', pressed, title };
return { text: t('KEY REJECTED'), tone: 'warn', pressed, title };
if (state.keyRequired)
return { text: 'KEY REQUIRED', tone: 'warn', pressed, title };
return { text: t('KEY REQUIRED'), tone: 'warn', pressed, title };
if (state.coverage.loading)
return { text: 'LOADING', tone: 'busy', pressed, title };
return { text: t('LOADING'), tone: 'busy', pressed, title };
return pressed
? { text: 'ON', tone: 'on', pressed, title }
: { text: 'OFF', tone: '', pressed, title };
? { text: t('ON'), tone: 'on', pressed, title }
: { text: t('OFF'), tone: '', pressed, title };
}
/** A chip is lit only while the layer and its provider are both on. */
@@ -73,11 +90,13 @@ function presentProviders(state) {
return (state.providers || []).map((provider) => {
const keyRequired = provider.keyRequired === true;
const on = enabled && provider.on === true;
let title = `${provider.name} imagery ${on ? 'on' : 'off'}`;
let title = on
? t('{name} imagery on', { name: provider.name })
: t('{name} imagery off', { name: provider.name });
if (provider.keyRejected && provider.error)
title = `${provider.name}: ${provider.error}`;
else if (keyRequired && provider.requiresKeyId)
title = `${provider.name}: ${keySetupRequirement(provider.requiresKeyId)}`;
title = `${provider.name}: ${keyRequirement(provider.requiresKeyId)}`;
else if (provider.error) title = `${provider.name}: ${provider.error}`;
return {
id: provider.id,
@@ -109,7 +128,9 @@ function presentViewer(state) {
open: street.open === true,
loading: street.loading === true && !street.imageId,
renderMode: street.renderMode === 'fill' ? 'fill' : 'letterbox',
captionLeft: street.creator ? `Image by ${street.creator}` : '',
captionLeft: street.creator
? t('Image by {creator}', { creator: street.creator })
: '',
captionRight: right.join(' · '),
link: street.externalUrl || null,
linkLabel: street.providerLabel ? `${street.providerLabel} ↗` : '',
@@ -118,22 +139,37 @@ function presentViewer(state) {
disabled: street.open !== true || street.followAvailable !== true,
title:
street.followAvailable === true
? 'Camera follows view: move the globe camera wherever the street-level view looks'
: 'Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE',
? t(
'Camera follows view: move the globe camera wherever the street-level view looks',
)
: t(
'Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE',
),
},
};
}
function presentMeta(state) {
if (state.providers.length === 0) return state.coverage.hint || '';
if (!state.enabled) return 'Switch a provider on to draw its coverage.';
if (state.providers.length === 0)
return state.coverage.hint ? t(state.coverage.hint) : '';
if (!state.enabled) return t('Switch a provider on to draw its coverage.');
if (state.sequence.selectedId)
return state.sequence.loading
? 'Loading this sequence…'
: `${state.sequence.images.toLocaleString()} images in this sequence · Esc clears`;
? t('Loading this sequence…')
: tn(
state.sequence.images,
'{n} image in this sequence · Esc clears',
'{n} images in this sequence · Esc clears',
{ n: formatNumber(state.sequence.images) },
);
if (state.coverage.count > 0)
return `${state.coverage.count.toLocaleString()} sequences in view · click a line for its photos`;
return state.coverage.hint || '';
return tn(
state.coverage.count,
'{n} sequence in view · click a line for its photos',
'{n} sequences in view · click a line for its photos',
{ n: formatNumber(state.coverage.count) },
);
return state.coverage.hint ? t(state.coverage.hint) : '';
}
/** @param {{now?: number}} [options] Clock for the SINCE readout. */