diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index c3970214a..12ac3e34c 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1316,7 +1316,11 @@ "src/layers/streetLevel/selection.js", "src/layers/streetLevel/providers/mapillary/viewer.js", "src/layers/streetLevel/providers/mapillary/source.js", - "scripts/qa-street-level.mjs" + "scripts/qa-street-level.mjs", + "src/i18n/index.js", + "src/i18n/locale.js", + "src/i18n/locales/es.json", + "src/i18n/locales/ru.json" ], "external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"] }, diff --git a/src/annotations/drawMode.js b/src/annotations/drawMode.js index f28a7ae6f..34f08e8f1 100644 --- a/src/annotations/drawMode.js +++ b/src/annotations/drawMode.js @@ -1,3 +1,5 @@ +import { t, tn } from '../i18n/index.js'; + /** * Manual whiteboard drawing: the pure half. * @@ -284,20 +286,26 @@ export function closeRing(pairs) { /** One line of guidance for the person drawing, by state. */ export function drawHint(session) { - if (!session) return 'Pick a shape, then click the map.'; + if (!session) return t('Pick a shape, then click the map.'); const n = session.vertices.length; if (session.shape === 'pin') return n - ? 'Enter to place the pin, Esc to cancel.' - : 'Click where the pin goes.'; + ? t('Enter to place the pin, Esc to cancel.') + : t('Click where the pin goes.'); const need = MIN_VERTICES[session.shape] - n; - if (need > 0) return `Click ${need} more point${need === 1 ? '' : 's'}.`; + if (need > 0) + return tn(need, 'Click {count} more point.', 'Click {count} more points.'); if (finishReason(session) === 'degenerate') { return session.shape === 'area' - ? 'Those points are in a line — move one off it to enclose an area.' - : 'That line has no length — click somewhere further away.'; + ? t('Those points are in a line — move one off it to enclose an area.') + : t('That line has no length — click somewhere further away.'); } const full = - n >= MAX_VERTICES ? ` · ${MAX_VERTICES}-point limit reached` : ''; - return `${formatMeasure(session)} · double-click or Enter to finish, Backspace undoes, Esc cancels.${full}`; + n >= MAX_VERTICES + ? t(' · {max}-point limit reached', { max: MAX_VERTICES }) + : ''; + return t( + '{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}', + { measure: formatMeasure(session), full }, + ); } diff --git a/src/annotations/drawTool.js b/src/annotations/drawTool.js index 7f164b54f..5ffe1e6df 100644 --- a/src/annotations/drawTool.js +++ b/src/annotations/drawTool.js @@ -48,6 +48,7 @@ import { removeLastVertex, } from './drawMode.js'; +import { t } from '../i18n/index.js'; /** The id this tool claims the pointer under. */ export const DRAW_POINTER_OWNER = 'draw'; const PREVIEW_DATA_SOURCE_NAME = 'gev-draw-preview'; @@ -200,7 +201,7 @@ export function initDrawTool({ viewer, annotations }) { `That shape already has ${MAX_VERTICES} points — finish it or press Backspace.`, ); else if (reason === 'invalid') - setHint('That point is off the globe — click on the world.'); + setHint(t('That point is off the globe — click on the world.')); }; const onMove = (event) => { if (!session || session.shape === 'pin' || destroyed) return; @@ -236,7 +237,7 @@ export function initDrawTool({ viewer, annotations }) { flyTo: false, }); if (destroyed || attempt !== generation) return result; - if (result?.drawn === 0) setHint('That shape could not be placed.'); + if (result?.drawn === 0) setHint(t('That shape could not be placed.')); return result; } catch (error) { if (destroyed || attempt !== generation) return null; @@ -258,7 +259,7 @@ export function initDrawTool({ viewer, annotations }) { if (session) session = createDrawSession(shape); annotations.clear(); syncPreview(); - if (!destroyed && !active) setHint('Board cleared.'); + if (!destroyed && !active) setHint(t('Board cleared.')); }; // ---- keys: only while drawing, never while typing in another field ---- diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 3ee5163c1..68adfd532 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -847,7 +847,7 @@ "Next session: {pendingId} — this session stays on {modelId}": "Sesión siguiente: {pendingId} — esta sesión sigue en {modelId}", "Voice model: {pendingId} — click to switch to {target}; applies next session": "Modelo de voz: {pendingId} — haz clic para cambiar a {target}; se aplica en la próxima sesión", "Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "Costo de sesión estimado en {modelId} — {responses} respuesta(s). Avisa a {warn} y cierra la sesión a {cap}.", - "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Imágenes y cobertura a nivel de calle en la capa Street Level. Gratis: registra una app en el panel de desarrolladores de Mapillary y pega su Client Token", + "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Imágenes y cobertura a nivel de calle en la capa A pie de calle. Gratis: registra una app en el panel de desarrolladores de Mapillary y pega su Client Token", "Voice control — API key or ChatGPT OAuth": "Control de voz — clave de API u OAuth de ChatGPT", "WITHIN {km} KM": "A MENOS DE {km} KM", "Syncing flow · Roads: {source}": "Sincronizando flujo · Vías: {source}", @@ -873,5 +873,111 @@ "Scanlines": "Líneas de barrido", "Sensitivity": "Sensibilidad", "WHOT/BHOT": "WHOT/BHOT", - "Ironbow": "Ironbow" + "Ironbow": "Ironbow", + "Pick a shape, then click the map.": "Elige una forma y haz clic en el mapa.", + "Enter to place the pin, Esc to cancel.": "Enter para colocar el pin, Esc para cancelar.", + "Click where the pin goes.": "Haz clic donde va el pin.", + "Click {count} more points.": { + "one": "Haz clic en {count} punto más.", + "many": "Haz clic en {count} de puntos más.", + "other": "Haz clic en {count} puntos más." + }, + "Those points are in a line — move one off it to enclose an area.": "Esos puntos están en línea: mueve uno fuera de ella para cerrar un área.", + "That line has no length — click somewhere further away.": "Esa línea no tiene longitud: haz clic más lejos.", + " · {max}-point limit reached": " · límite de {max} puntos alcanzado", + "{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · doble clic o Enter para terminar, Backspace deshace, Esc cancela.{full}", + "That point is off the globe — click on the world.": "Ese punto está fuera del globo: haz clic sobre el mundo.", + "That shape could not be placed.": "No se pudo colocar esa forma.", + "Board cleared.": "Tablero borrado.", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "Dibuja sobre el mundo: haz clic en los vértices, doble clic o Enter para terminar, Esc para cancelar", + "Draw": "Dibujo", + "Shape": "Forma", + "Shape to draw": "Forma a dibujar", + "Area": "Área", + "Line": "Línea", + "Pin": "Pin", + "Label (optional)": "Etiqueta (opcional)", + "Label for the drawn shape": "Etiqueta de la forma dibujada", + "Colour of the drawn shape": "Color de la forma dibujada", + "Primary": "Principal", + "Amber": "Ámbar", + "Cyan": "Cian", + "Green": "Verde", + "Red": "Rojo", + "Clear": "Borrar", + "Remove every mark from the board": "Quitar todas las marcas del tablero", + "Point the camera at the globe for street-level coverage": "Apunta la cámara al globo para ver la cobertura a pie de calle", + "No street-level imagery providers configured": "No hay proveedores de imágenes a pie de calle configurados", + "Selected": "Seleccionada", + "Live Vessels": "Buques en vivo", + "Bike Share": "Bicis compartidas", + "Cameras": "Cámaras", + "Street Level": "A pie de calle", + "Mapped ALPR Cameras": "Cámaras ALPR cartografiadas", + "Data Centers": "Centros de datos", + "Active Fires": "Incendios activos", + "Street Level could not start": "No se pudo iniciar A pie de calle", + "Street Level toggle failed": "No se pudo cambiar A pie de calle", + "Street-level image": "Imagen a pie de calle", + "SHRINK": "REDUCIR", + "EXPAND": "AMPLIAR", + "Shrink": "Reducir", + "Expand": "Ampliar", + "ANY DATE": "CUALQUIER FECHA", + "LAST 10 YEARS": "ÚLTIMOS 10 AÑOS", + "LAST 5 YEARS": "ÚLTIMOS 5 AÑOS", + "LAST 3 YEARS": "ÚLTIMOS 3 AÑOS", + "LAST 2 YEARS": "ÚLTIMOS 2 AÑOS", + "LAST YEAR": "ÚLTIMO AÑO", + "LAST 6 MONTHS": "ÚLTIMOS 6 MESES", + "LAST 3 MONTHS": "ÚLTIMOS 3 MESES", + "LAST MONTH": "ÚLTIMO MES", + "LAST {count} DAYS": { + "one": "ÚLTIMO {count} DÍA", + "many": "ÚLTIMOS {count} DE DÍAS", + "other": "ÚLTIMOS {count} DÍAS" + }, + "{window} · SINCE {date}": "{window} · DESDE {date}", + "Turn Street Level off": "Desactivar A pie de calle", + "Turn Street Level on": "Activar A pie de calle", + "KEY REJECTED": "CLAVE RECHAZADA", + "{name} imagery on": "Imágenes de {name} activadas", + "{name} imagery off": "Imágenes de {name} desactivadas", + "Image by {creator}": "Imagen de {creator}", + "Camera follows view: move the globe camera wherever the street-level view looks": "La cámara sigue la vista: mueve la cámara del globo hacia donde mira la vista a pie de calle", + "Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE": "Seguir con la cámara requiere el mapa Google 3D: elige Google 3D en FUENTE DEL MAPA", + "Switch a provider on to draw its coverage.": "Activa un proveedor para dibujar su cobertura.", + "Loading this sequence…": "Cargando esta secuencia…", + "{n} images in this sequence · Esc clears": { + "one": "{n} imagen en esta secuencia · Esc la quita", + "many": "{n} de imágenes en esta secuencia · Esc la quita", + "other": "{n} imágenes en esta secuencia · Esc la quita" + }, + "{n} sequences in view · click a line for its photos": { + "one": "{n} secuencia a la vista · haz clic en una línea para ver sus fotos", + "many": "{n} de secuencias a la vista · haz clic en una línea para ver sus fotos", + "other": "{n} secuencias a la vista · haz clic en una línea para ver sus fotos" + }, + "STREET LEVEL": "A PIE DE CALLE", + "Expand the street-level view (Esc shrinks it)": "Ampliar la vista a pie de calle (Esc la reduce)", + "Image fit": "Ajuste de la imagen", + "FIT": "AJUSTAR", + "Fit: show the whole photo, with dark bars where its shape differs from the frame": "Ajustar: muestra la foto entera, con barras oscuras donde su forma no coincide con el marco", + "FILL": "RELLENAR", + "Fill: fill the frame edge to edge, cropping the photo": "Rellenar: llena el marco de borde a borde, recortando la foto", + "Camera follows view": "La cámara sigue la vista", + "FOLLOW": "SEGUIR", + "Close the image and deselect it on the map": "Cerrar la imagen y quitar la selección en el mapa", + "Close the street-level image": "Cerrar la imagen a pie de calle", + "Street-level viewer": "Visor a pie de calle", + "Open this image on the provider's site": "Abrir esta imagen en el sitio del proveedor", + "Imagery providers": "Proveedores de imágenes", + "PROVIDERS": "PROVEEDORES", + "Imagery filters": "Filtros de imágenes", + "IMAGERY": "IMÁGENES", + "Panorama filter": "Filtro de panoramas", + "ALL": "TODAS", + "FLAT": "PLANAS", + "SINCE": "DESDE", + "Coverage colours": "Colores de cobertura" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 46d99e42a..40fa121c2 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -73,7 +73,7 @@ "Remove this key from your saved configuration?": "Удалить этот ключ из сохранённой конфигурации?", "Removed from {store}. Restarting — this page reloads itself.": "Удалено: {store}. Перезапуск — страница перезагрузится сама.", "More flight-polling credits (anonymous works without)": "Больше кредитов на опрос рейсов (анонимно работает и без ключа)", - "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Уличные снимки и покрытие в слое «Street Level». Бесплатно: зарегистрируйте приложение в панели разработчика Mapillary и вставьте его Client Token", + "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Уличные снимки и покрытие в слое «Уличные снимки». Бесплатно: зарегистрируйте приложение в панели разработчика Mapillary и вставьте его Client Token", "Higher space-missions request allowance": "Повышенный лимит запросов для космических миссий", "The photorealistic 3D planet + place search": "Фотореалистичная 3D-планета + поиск мест", "Places context + Street View fallback; optional separate key": "Контекст Places + резервный Street View; отдельный ключ — по желанию", @@ -880,5 +880,115 @@ "Scanlines": "Строки развёртки", "Sensitivity": "Чувствительность", "WHOT/BHOT": "WHOT/BHOT", - "Ironbow": "Ironbow" + "Ironbow": "Ironbow", + "Pick a shape, then click the map.": "Выберите фигуру и щёлкните по карте.", + "Enter to place the pin, Esc to cancel.": "Enter — поставить метку, Esc — отменить.", + "Click where the pin goes.": "Щёлкните, где поставить метку.", + "Click {count} more points.": { + "one": "Щёлкните ещё {count} точку.", + "few": "Щёлкните ещё {count} точки.", + "many": "Щёлкните ещё {count} точек.", + "other": "Щёлкните ещё {count} точки." + }, + "Those points are in a line — move one off it to enclose an area.": "Точки лежат на одной линии — сдвиньте одну, чтобы получилась площадь.", + "That line has no length — click somewhere further away.": "У линии нет длины — щёлкните дальше.", + " · {max}-point limit reached": " · достигнут предел в {max} точек", + "{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · двойной щелчок или Enter — завершить, Backspace — отменить точку, Esc — отменить.{full}", + "That point is off the globe — click on the world.": "Эта точка вне глобуса — щёлкните по Земле.", + "That shape could not be placed.": "Не удалось разместить фигуру.", + "Board cleared.": "Доска очищена.", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "Рисовать на карте мира — щёлкайте вершины, двойной щелчок или Enter — завершить, Esc — отменить", + "Draw": "Рисование", + "Shape": "Фигура", + "Shape to draw": "Фигура для рисования", + "Area": "Область", + "Line": "Линия", + "Pin": "Метка", + "Label (optional)": "Подпись (необязательно)", + "Label for the drawn shape": "Подпись нарисованной фигуры", + "Colour of the drawn shape": "Цвет нарисованной фигуры", + "Primary": "Основной", + "Amber": "Янтарный", + "Cyan": "Голубой", + "Green": "Зелёный", + "Red": "Красный", + "Clear": "Очистить", + "Remove every mark from the board": "Убрать все пометки с доски", + "Point the camera at the globe for street-level coverage": "Наведите камеру на глобус, чтобы увидеть покрытие уличных снимков", + "No street-level imagery providers configured": "Источники уличных снимков не настроены", + "Selected": "Выбранная", + "Live Vessels": "Суда в реальном времени", + "Bike Share": "Прокат велосипедов", + "Cameras": "Камеры", + "Street Level": "Уличные снимки", + "Mapped ALPR Cameras": "Камеры ALPR на карте", + "Data Centers": "Дата-центры", + "Active Fires": "Активные пожары", + "Street Level could not start": "Не удалось запустить «Уличные снимки»", + "Street Level toggle failed": "Не удалось переключить «Уличные снимки»", + "Street-level image": "Уличный снимок", + "SHRINK": "СВЕРНУТЬ", + "EXPAND": "РАЗВЕРНУТЬ", + "Shrink": "Свернуть", + "Expand": "Развернуть", + "ANY DATE": "ЛЮБАЯ ДАТА", + "LAST 10 YEARS": "ЗА 10 ЛЕТ", + "LAST 5 YEARS": "ЗА 5 ЛЕТ", + "LAST 3 YEARS": "ЗА 3 ГОДА", + "LAST 2 YEARS": "ЗА 2 ГОДА", + "LAST YEAR": "ЗА ГОД", + "LAST 6 MONTHS": "ЗА 6 МЕСЯЦЕВ", + "LAST 3 MONTHS": "ЗА 3 МЕСЯЦА", + "LAST MONTH": "ЗА МЕСЯЦ", + "LAST {count} DAYS": { + "one": "ЗА {count} ДЕНЬ", + "few": "ЗА {count} ДНЯ", + "many": "ЗА {count} ДНЕЙ", + "other": "ЗА {count} ДНЯ" + }, + "{window} · SINCE {date}": "{window} · С {date}", + "Turn Street Level off": "Выключить «Уличные снимки»", + "Turn Street Level on": "Включить «Уличные снимки»", + "KEY REJECTED": "КЛЮЧ ОТКЛОНЁН", + "{name} imagery on": "Снимки {name} включены", + "{name} imagery off": "Снимки {name} выключены", + "Image by {creator}": "Автор снимка: {creator}", + "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": "Слежению камеры нужна карта Google 3D: выберите Google 3D в разделе «ИСТОЧНИК КАРТЫ»", + "Switch a provider on to draw its coverage.": "Включите источник, чтобы показать его покрытие.", + "Loading this sequence…": "Загрузка последовательности…", + "{n} images in this sequence · Esc clears": { + "one": "{n} снимок в последовательности · Esc — сбросить", + "few": "{n} снимка в последовательности · Esc — сбросить", + "many": "{n} снимков в последовательности · Esc — сбросить", + "other": "{n} снимка в последовательности · Esc — сбросить" + }, + "{n} sequences in view · click a line for its photos": { + "one": "{n} последовательность в кадре · щёлкните линию, чтобы открыть снимки", + "few": "{n} последовательности в кадре · щёлкните линию, чтобы открыть снимки", + "many": "{n} последовательностей в кадре · щёлкните линию, чтобы открыть снимки", + "other": "{n} последовательности в кадре · щёлкните линию, чтобы открыть снимки" + }, + "STREET LEVEL": "УЛИЧНЫЕ СНИМКИ", + "Expand the street-level view (Esc shrinks it)": "Развернуть уличный снимок (Esc сворачивает)", + "Image fit": "Масштаб снимка", + "FIT": "ВПИСАТЬ", + "Fit: show the whole photo, with dark bars where its shape differs from the frame": "Вписать: показать снимок целиком, с тёмными полосами там, где его форма не совпадает с рамкой", + "FILL": "ЗАПОЛНИТЬ", + "Fill: fill the frame edge to edge, cropping the photo": "Заполнить: растянуть на всю рамку, обрезав снимок", + "Camera follows view": "Камера следует за видом", + "FOLLOW": "СЛЕДИТЬ", + "Close the image and deselect it on the map": "Закрыть снимок и снять выделение на карте", + "Close the street-level image": "Закрыть уличный снимок", + "Street-level viewer": "Просмотр уличных снимков", + "Open this image on the provider's site": "Открыть снимок на сайте источника", + "Imagery providers": "Источники снимков", + "PROVIDERS": "ИСТОЧНИКИ", + "Imagery filters": "Фильтры снимков", + "IMAGERY": "СНИМКИ", + "Panorama filter": "Фильтр панорам", + "ALL": "ВСЕ", + "FLAT": "ПЛОСКИЕ", + "SINCE": "С ДАТЫ", + "Coverage colours": "Цвета покрытия" } diff --git a/src/layers/streetLevel/providers/mapillary/coverage.js b/src/layers/streetLevel/providers/mapillary/coverage.js index 5b9b5246c..7057f5229 100644 --- a/src/layers/streetLevel/providers/mapillary/coverage.js +++ b/src/layers/streetLevel/providers/mapillary/coverage.js @@ -33,6 +33,7 @@ import { SEQUENCE_VIEW_RANGE_PER_HEIGHT, } from './policy.js'; +import { msg } from '../../../../i18n/index.js'; /** Draped lines stay at most this long while a tile's cast lines build. */ export const SWAP_MAX_WAIT_MS = 4000; /** Tiles touched by new mesh samples are redrawn at most this often. */ @@ -694,8 +695,9 @@ export function createCoverage({ state, source }) { if (kind === 'overview' && (!bbox || zoom <= 1)) bbox = [-180, -85, 180, 85]; if (zoom == null || !bbox) { - state.coverage.hint = - 'Point the camera at the globe for street-level coverage'; + state.coverage.hint = msg( + 'Point the camera at the globe for street-level coverage', + ); clear(); notify(); return; diff --git a/src/layers/streetLevel/uiState.js b/src/layers/streetLevel/uiState.js index a2420314f..09c81d03f 100644 --- a/src/layers/streetLevel/uiState.js +++ b/src/layers/streetLevel/uiState.js @@ -1,5 +1,6 @@ import { COLORS } from './policy.js'; +import { msg, t } from '../../i18n/index.js'; /** * @typedef {object} ProviderSnapshot * @property {string} id @@ -29,7 +30,7 @@ export function summarizeCoverage(providers) { loading: active.some((p) => p.loading), hint: providers.length === 0 - ? 'No street-level imagery providers configured' + ? msg('No street-level imagery providers configured') : active.find((p) => p.hint)?.hint || '', error: active.find((p) => p.error)?.error || null, keyRequired: @@ -63,7 +64,11 @@ export function composeUIState({ color: provider.color, })); if (active.length) - legend.push({ key: 'selected', label: 'Selected', color: COLORS.selected }); + legend.push({ + key: 'selected', + label: t('Selected'), + color: COLORS.selected, + }); return { enabled, keyRequired, diff --git a/src/streetLevelMarkup.test.mjs b/src/streetLevelMarkup.test.mjs index de1878a79..4bf34f676 100644 --- a/src/streetLevelMarkup.test.mjs +++ b/src/streetLevelMarkup.test.mjs @@ -25,7 +25,10 @@ test('Street Level is an ordinary collapsible GEV panel that starts collapsed', html, /
- Shape -
- - - + Shape +
+ + +
- - + - +
diff --git a/src/ui/templates/layer-panels.html b/src/ui/templates/layer-panels.html index a48bb28a0..51d039b39 100644 --- a/src/ui/templates/layer-panels.html +++ b/src/ui/templates/layer-panels.html @@ -76,7 +76,7 @@
- STREET LEVEL + STREET LEVEL @@ -86,53 +86,53 @@