Merge origin/i18n-spanish: whole-interface translation meets KartaView

This branch moved twice at once: upstream merged KartaView (#1121, a
third keyless street-level provider sharing a new lineCoverage.js), and
the remote i18n-spanish gained the whole-interface translation work
(1993 catalog entries, translateKnown, data-i18n-runtime, the unmarked-
string detector in npm run i18n) built on the Street View merge.

Integration keeps the remote's mechanisms and re-adds KartaView on top:

- presentMeta: remote's translateHint for coverage hints, plus the
  KartaView count line as its own literal tn pair (Russian plurals need
  the noun inside the template; keyed off coverageLabel.noun with the
  sequences pair as the default).
- uiState: upstream's highlightsSequence legend gate (KartaView's lines
  do not highlight) with t('Selected').
- layer-panels.html: remote's data-i18n/data-i18n-runtime hooks plus the
  sl-since-note wiring KartaView's upload-date SINCE note needs.
- kartaview viewer: the photo-navigation aria-label and img alt go
  through t(), as the remote's unmarked-string detector requires.
- catalogs: remote's 1987-entry base (its refined keys supersede this
  branch's earlier Street View wording) plus the KartaView entries:
  coverage hints, the upload-date note, the streets count line (with
  Russian one/few/many/other forms) and the viewer strings.

npm run i18n: complete, no unmarked strings. npm test: 6520/6522 (the
one failure is the pre-existing environmental OPENAI_API_KEY leak in
devFreshDotenv). Build passes.
This commit is contained in:
Meir Michanie committed 2026-10-14 17:45:57 +02:00
commit 32b11e27de
176 files changed
+9713 -2210

No files matched your search

+7 -2
View File
@@ -18,6 +18,7 @@ import { cameraHeightAboveGround, viewCentre, whenIdle } from './view.js';
import { createGroundClick, groundClickHeightOk } from './groundClick.js';
import { createGroundCaster, nextSurfaceMode } from './groundCast.js';
import { createMeshSampler } from './meshSampler.js';
import { t, translateKnown } from '../../i18n/index.js';
/** Above this camera height the ground under the camera is not worth fetching. */
const SURFACE_WARM_BELOW_M = 6000;
@@ -335,7 +336,9 @@ export function createStreetLevelLayer({
}
function unavailableReason(entry) {
return `${entry.def.name} needs the Google 3D map: choose it under MAP SOURCE`;
return t('{name} needs the Google 3D map: choose it under MAP SOURCE', {
name: translateKnown(entry.def.name),
});
}
/** One imagery source at a time: lighting a provider darkens the others. */
@@ -410,7 +413,9 @@ export function createStreetLevelLayer({
/** The hint a ground-click provider shows, or the provider's own. */
function hintFor(entry, stats) {
if (entry.def.groundClick && stats.hint && !state.groundClickReady)
return `zoom in to a street to open ${entry.def.name}`;
return t('zoom in to a street to open {name}', {
name: translateKnown(entry.def.name),
});
return stats.hint || '';
}
@@ -1,5 +1,6 @@
import { FOLLOW_MAP_STACK_ID } from '../../policy.js';
import { createMapsLoader } from './mapsLoader.js';
import { getLocale } from '../../../../i18n/index.js';
import { createGoogleViewer } from './viewer.js';
import {
COLORS,
@@ -48,7 +49,7 @@ function settleWithin(promise, { ms, signal, loader }) {
*/
export function createGoogleProvider({
getApiKey,
loader = createMapsLoader({ getApiKey }),
loader = createMapsLoader({ getApiKey, getLanguage: getLocale }),
}) {
if (typeof getApiKey !== 'function')
throw new TypeError('Google Street View needs getApiKey');
@@ -9,10 +9,13 @@ const READY_CALLBACK = '__gevGoogleMapsReady';
const PAGE_LOAD = Symbol.for('gev.googleMapsLoad');
/**
* @param {{getApiKey: () => string|null|undefined, document?: Document, global?: object}} options
* @param {{getApiKey: () => string|null|undefined, getLanguage?: () => string|null|undefined, document?: Document, global?: object}} options
* `getLanguage` names the UI language; Google's own controls (Terms,
* Report a problem) follow it. English adds nothing, as before.
*/
export function createMapsLoader({
getApiKey,
getLanguage,
document = globalThis.document,
global = globalThis,
}) {
@@ -53,12 +56,15 @@ export function createMapsLoader({
};
const script = document.createElement('script');
script.async = true;
script.src = `${SCRIPT_ORIGIN}/maps/api/js?${new URLSearchParams({
const params = new URLSearchParams({
key,
v: 'weekly',
loading: 'async',
callback: READY_CALLBACK,
})}`;
});
const language = getLanguage?.();
if (language && language !== 'en') params.set('language', language);
script.src = `${SCRIPT_ORIGIN}/maps/api/js?${params}`;
script.onerror = () => {
if (global[PAGE_LOAD] === loading) delete global[PAGE_LOAD];
script.remove();
@@ -142,3 +142,19 @@ test('a retried load still reports a refusal once, and the page handler once', a
assert.deepEqual(heard, ['a']);
assert.equal(before, 1);
});
test('Google controls follow the UI language; English adds no parameter', async () => {
const language = (locale) => {
const page = fakePage();
const loader = createMapsLoader({
getApiKey: () => 'k-1',
getLanguage: () => locale,
...page,
});
void loader.importLibrary('streetView').catch(() => {});
return new URL(page.scripts[0].src).searchParams.get('language');
};
assert.equal(language('es'), 'es');
assert.equal(language('ru'), 'ru');
assert.equal(language('en'), null);
});
@@ -1,10 +1,11 @@
import { COLORS as SHARED_COLORS, PROVIDER_COLORS } from '../../policy.js';
import { msg } from '../../../../i18n/index.js';
export { NEAREST_RADIUS_M } from '../../policy.js';
export const GOOGLE_PROVIDER_ID = 'google';
export const GOOGLE_NAME = 'Google Street View';
export const GOOGLE_LABEL = 'GOOGLE STREET VIEW';
export const GOOGLE_NAME = msg('Google Street View');
export const GOOGLE_LABEL = msg('GOOGLE STREET VIEW');
/** The browser Maps key; Street View also needs the Maps JavaScript API on it. */
export const GOOGLE_KEY_ID = 'google-maps';
@@ -19,8 +20,9 @@ export const GOOGLE_CREDIT_HTML =
'Street View imagery © <a href="https://www.google.com/streetview/" target="_blank" rel="noopener">Google</a>';
/** Shown when Google refuses the key (gm_authFailure). */
export const KEY_REJECTED_MESSAGE =
'Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer';
export const KEY_REJECTED_MESSAGE = msg(
'Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer',
);
/** Past these, a nearest lookup, an open or a details lookup has failed. */
export const NEAREST_TIMEOUT_MS = 10_000;
@@ -29,10 +31,11 @@ export const DETAILS_TIMEOUT_MS = 10_000;
/** Pause after a failed details lookup before a view event can retry. */
export const DETAILS_RETRY_COOLDOWN_MS = 5_000;
export const NO_ANSWER_MESSAGE =
'Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY';
export const NO_ANSWER_MESSAGE = msg(
'Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY',
);
export const GROUND_CLICK_HINT = 'click a street to open Street View';
export const GROUND_CLICK_HINT = msg('click a street to open Street View');
/** Deep link to a panorama on Google Maps (Maps URLs). */
export function streetViewUrl(panoId, { heading = 0, pitch = 0 } = {}) {
@@ -3,6 +3,7 @@ import {
PANO_FOV_DEFAULT_DEG,
wrapDegrees,
} from './panorama.js';
import { t } from '../../../../i18n/index.js';
import {
FLAT_FULL_SIZE,
FLAT_VIEW_SIZE,
@@ -436,11 +437,11 @@ export function createKartaViewViewer({
root.setAttribute('role', 'group');
root.setAttribute(
'aria-label',
'KartaView photo. ← and → step along the sequence. On a 360° photo, drag or use Shift+← →, ↑ ↓ to look around, and + − to zoom',
t('KartaView photo. ← and → step along the sequence. On a 360° photo, drag or use Shift+← →, ↑ ↓ to look around, and + − to zoom'),
);
picture = doc.createElement('img');
picture.className = 'sl-kv-photo';
picture.alt = 'KartaView street-level photo';
picture.alt = t('KartaView street-level photo');
picture.draggable = false;
picture.hidden = true;
canvas = makeCanvas(doc);
+5 -1
View File
@@ -119,7 +119,11 @@ export function composeUIState({
provider.groundClick !== true && provider.highlightsSequence !== false,
)
)
legend.push({ key: 'selected', label: t('Selected'), color: COLORS.selected });
legend.push({
key: 'selected',
label: t('Selected'),
color: COLORS.selected,
});
return {
enabled,
groundClickReady: groundClickReady === true,