mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(i18n): translate the whole interface
Mark every UI string the unmarked-text check reported (534) plus text it cannot see (helpers, joined labels, runtime-built panels), and translate the result to Spanish and Russian: 1,679 messages each. - Kept on purpose, with the reason where the text lives: provider and product names, place and camera names, codes and units, dB values, the HUD terminal readouts, demo shot titles that saved projects look up by title, and text for AI tools and the voice model. - Modules shared with the server or AI tools stay free of the i18n import (local msg); the wind reading and voice session stay English and are translated where the page shows them. - npm test now runs the unmarked-text check. - Fixes on the way: a duplicated radio privacy paragraph, the contact panel toggle saying Expand while open, the CSS filter mistaking & for CSS. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ee72133574
commit
3129e531e6
99 files changed
+4670
-1294
No files matched your search
+157
-97
@@ -8,6 +8,7 @@
|
||||
* strip and selection bar are this adapter's own keyed DOM. Source toggles
|
||||
* live on the data-panel row.
|
||||
*/
|
||||
import { t, tn, msg } from '../i18n/index.js';
|
||||
import { createRailCardBlocks } from './railCardBlocks.js';
|
||||
import { createRailCards } from './railCards.js';
|
||||
import { createImagerySplit } from './imagerySplit.js';
|
||||
@@ -20,28 +21,28 @@ import {
|
||||
|
||||
const PANEL_ID = 'recent-imagery-panel';
|
||||
const OVERVIEW_MIN_BOX_KM = 25;
|
||||
const ESRI_NOTE = 'Imagery on Esri · Google 3D returns when cleared';
|
||||
const ESRI_NOTE = msg('Imagery on Esri · Google 3D returns when cleared');
|
||||
const MODE_LABELS = [
|
||||
['image', 'IMAGE'],
|
||||
['basemap', 'VS BASEMAP'],
|
||||
['ab', 'A / B'],
|
||||
['image', msg('IMAGE')],
|
||||
['basemap', msg('VS BASEMAP')],
|
||||
['ab', msg('A / B')],
|
||||
];
|
||||
const START_HERE_TITLES = {
|
||||
clear: 'Newest low-cloud day for this box · scene cloud, not box cloud',
|
||||
cloudy: 'Newest day covering this box · cloudier than 20%',
|
||||
partial: 'Newest day with imagery for this box · partial coverage',
|
||||
overview: 'Newest daily overview for this box',
|
||||
clear: msg('Newest low-cloud day for this box · scene cloud, not box cloud'),
|
||||
cloudy: msg('Newest day covering this box · cloudier than 20%'),
|
||||
partial: msg('Newest day with imagery for this box · partial coverage'),
|
||||
overview: msg('Newest daily overview for this box'),
|
||||
};
|
||||
const START_HERE_REASONS = {
|
||||
clear: 'newest clear day',
|
||||
cloudy: 'newest day (cloudy)',
|
||||
partial: 'newest day (partial coverage)',
|
||||
overview: 'newest overview',
|
||||
clear: msg('newest clear day'),
|
||||
cloudy: msg('newest day (cloudy)'),
|
||||
partial: msg('newest day (partial coverage)'),
|
||||
overview: msg('newest overview'),
|
||||
};
|
||||
const PLACEHOLDERS = {
|
||||
empty: 'No imagery',
|
||||
error: 'Unavailable',
|
||||
present: 'Loading',
|
||||
empty: msg('No imagery'),
|
||||
error: msg('Unavailable'),
|
||||
present: msg('Loading'),
|
||||
};
|
||||
const KEY_STEPS = {
|
||||
ArrowLeft: (index) => index - 1,
|
||||
@@ -55,7 +56,7 @@ const appearedDocuments = new WeakSet();
|
||||
|
||||
const sensorLine = (product) =>
|
||||
PRODUCTS[product]
|
||||
? `${PRODUCTS[product].name} · ${PRODUCTS[product].resolutionM} m`
|
||||
? `${t(PRODUCTS[product].name)} · ${PRODUCTS[product].resolutionM} m`
|
||||
: product || '';
|
||||
const utcTime = (iso) => (hhmm(iso) ? `${hhmm(iso)}Z` : '');
|
||||
const longerSideKm = (size) =>
|
||||
@@ -68,35 +69,36 @@ const kmText = (km) =>
|
||||
: km.toFixed(1);
|
||||
|
||||
function cloudText(candidate) {
|
||||
if (candidate.thumbnail?.status === 'empty') return 'no imagery';
|
||||
if (!candidate.cloud) return 'cloud unknown';
|
||||
if (candidate.thumbnail?.status === 'empty') return t('no imagery');
|
||||
if (!candidate.cloud) return t('cloud unknown');
|
||||
const min = Math.round(candidate.cloud.min);
|
||||
const max = Math.round(candidate.cloud.max);
|
||||
return min === max ? `${min}% cloud` : `${min}–${max}% cloud`;
|
||||
return min === max
|
||||
? t('{percent}% cloud', { percent: min })
|
||||
: t('{min}–{max}% cloud', { min, max });
|
||||
}
|
||||
|
||||
function countText(snapshot) {
|
||||
if (!snapshot.box) return 'NO BOX';
|
||||
if (snapshot.searching) return 'SEARCHING';
|
||||
const count = snapshot.candidates.length;
|
||||
return `${count} DAY${count === 1 ? '' : 'S'}`;
|
||||
if (!snapshot.box) return t('NO BOX');
|
||||
if (snapshot.searching) return t('SEARCHING');
|
||||
return tn(snapshot.candidates.length, '{count} DAY', '{count} DAYS');
|
||||
}
|
||||
|
||||
/** The one-line hint under the strip: the next thing to do, per state. */
|
||||
function hintText(snapshot) {
|
||||
if (snapshot.zoomToFit) return 'Zoom in or draw a smaller box';
|
||||
if (!snapshot.box) return 'Select a box or use the view';
|
||||
if (snapshot.searching) return 'Searching the last 30 days';
|
||||
if (!snapshot.candidates.length) return 'No days to show for this box';
|
||||
if (snapshot.zoomToFit) return t('Zoom in or draw a smaller box');
|
||||
if (!snapshot.box) return t('Select a box or use the view');
|
||||
if (snapshot.searching) return t('Searching the last 30 days');
|
||||
if (!snapshot.candidates.length) return t('No days to show for this box');
|
||||
const { mode, pins, shown } = snapshot;
|
||||
if (shown.swipe !== 'none') return 'Drag the divider · SWAP trades sides';
|
||||
if (shown.swipe !== 'none') return t('Drag the divider · SWAP trades sides');
|
||||
if (mode === 'ab') {
|
||||
if (!pins.a.key && !pins.b.key)
|
||||
return '← → preview · A or B pins the focused day';
|
||||
return '← → preview the other side · A or B pins it';
|
||||
return t('← → preview · A or B pins the focused day');
|
||||
return t('← → preview the other side · A or B pins it');
|
||||
}
|
||||
if (pins.a.key) return 'S on another day replaces it · × unpins';
|
||||
return '← → preview · S shows the focused day';
|
||||
if (pins.a.key) return t('S on another day replaces it · × unpins');
|
||||
return t('← → preview · S shows the focused day');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +175,7 @@ export function createRecentImageryPanel({
|
||||
|
||||
// ---- build: every block exists from the start, in its final order ----
|
||||
const root = el('section', 'recent-imagery-readout');
|
||||
root.setAttribute('aria-label', 'Recent imagery');
|
||||
root.setAttribute('aria-label', t('Recent imagery'));
|
||||
|
||||
const actionsHost = el('div', 'ri-block ri-actions-row', root);
|
||||
actionsHost.id = 'ri-actions';
|
||||
@@ -191,10 +193,15 @@ export function createRecentImageryPanel({
|
||||
noticeText.id = 'ri-notice-text';
|
||||
noticeText.setAttribute('role', 'status');
|
||||
noticeText.setAttribute('aria-live', 'polite');
|
||||
const zoomIn = el('button', 'data-toggle-chip ri-zoom-in', notice, 'ZOOM IN');
|
||||
const zoomIn = el(
|
||||
'button',
|
||||
'data-toggle-chip ri-zoom-in',
|
||||
notice,
|
||||
t('ZOOM IN'),
|
||||
);
|
||||
zoomIn.id = 'ri-zoom-in';
|
||||
zoomIn.type = 'button';
|
||||
zoomIn.title = 'Fly in until the view fits the 1,000 km limit';
|
||||
zoomIn.title = t('Fly in until the view fits the 1,000 km limit');
|
||||
|
||||
const strip = el('div', 'ri-block ri-strip', root);
|
||||
strip.id = 'ri-strip';
|
||||
@@ -203,7 +210,7 @@ export function createRecentImageryPanel({
|
||||
strip.setAttribute('aria-orientation', 'horizontal');
|
||||
strip.setAttribute(
|
||||
'aria-label',
|
||||
'Imagery days, newest first · arrows move, S or A and B pin',
|
||||
t('Imagery days, newest first · arrows move, S or A and B pin'),
|
||||
);
|
||||
strip.setAttribute('aria-keyshortcuts', 'A B S');
|
||||
const stripEmpty = el('div', 'ri-strip-empty', strip);
|
||||
@@ -213,16 +220,16 @@ export function createRecentImageryPanel({
|
||||
|
||||
const selection = el('section', 'ri-block ri-selection', root);
|
||||
selection.id = 'ri-selection';
|
||||
selection.setAttribute('aria-label', 'On the map');
|
||||
selection.setAttribute('aria-label', t('On the map'));
|
||||
const modeGroup = el('div', 'ri-mode', selection);
|
||||
modeGroup.id = 'ri-mode';
|
||||
modeGroup.setAttribute('role', 'radiogroup');
|
||||
modeGroup.setAttribute('aria-label', 'Mode');
|
||||
modeGroup.setAttribute('aria-label', t('Mode'));
|
||||
const modeButtons = new Map(
|
||||
MODE_LABELS.map(([mode, label]) => {
|
||||
const node = el('button', 'data-toggle-chip ri-mode-btn', modeGroup);
|
||||
node.type = 'button';
|
||||
node.textContent = label;
|
||||
node.textContent = t(label);
|
||||
node.dataset.mode = mode;
|
||||
node.setAttribute('role', 'radio');
|
||||
return [mode, node];
|
||||
@@ -244,7 +251,7 @@ export function createRecentImageryPanel({
|
||||
const controls = el('div', 'ri-block ri-controls', root);
|
||||
controls.id = 'ri-controls';
|
||||
const opacityLabel = el('label', 'ri-opacity-label', controls);
|
||||
el('span', '', opacityLabel, 'OPACITY');
|
||||
el('span', '', opacityLabel, t('OPACITY'));
|
||||
const opacityValue = el('span', 'ri-opacity-value', opacityLabel, '100%');
|
||||
const opacity = el('input', 'ri-opacity', controls);
|
||||
opacity.id = 'ri-opacity';
|
||||
@@ -255,13 +262,13 @@ export function createRecentImageryPanel({
|
||||
step: '1',
|
||||
value: '100',
|
||||
});
|
||||
opacity.setAttribute('aria-label', 'Imagery opacity');
|
||||
opacity.setAttribute('aria-label', t('Imagery opacity'));
|
||||
opacityLabel.htmlFor = 'ri-opacity';
|
||||
// Its slot is always there; it is only usable while a swipe is live.
|
||||
const swap = el('button', 'data-toggle-chip ri-swap', controls, 'SWAP');
|
||||
const swap = el('button', 'data-toggle-chip ri-swap', controls, t('SWAP'));
|
||||
swap.id = 'ri-swap';
|
||||
swap.type = 'button';
|
||||
swap.title = 'Trade the two sides of the divider';
|
||||
swap.title = t('Trade the two sides of the divider');
|
||||
const exportHost = el('div', 'ri-exports', controls);
|
||||
const exports = createRailCardBlocks({
|
||||
container: exportHost,
|
||||
@@ -318,8 +325,8 @@ export function createRecentImageryPanel({
|
||||
actions: [
|
||||
{
|
||||
id: 'select-box',
|
||||
label: 'SELECT BOX',
|
||||
title: 'Drag a box on the map (Esc cancels)',
|
||||
label: t('SELECT BOX'),
|
||||
title: t('Drag a box on the map (Esc cancels)'),
|
||||
onClick: () => {
|
||||
exportError = null;
|
||||
if (tool?.isActive()) tool.cancel('toggle');
|
||||
@@ -328,8 +335,8 @@ export function createRecentImageryPanel({
|
||||
},
|
||||
{
|
||||
id: 'use-view',
|
||||
label: 'USE VIEW',
|
||||
title: 'Use the current view as the box',
|
||||
label: t('USE VIEW'),
|
||||
title: t('Use the current view as the box'),
|
||||
onClick: () => {
|
||||
exportError = null;
|
||||
layer.useCurrentView(viewer);
|
||||
@@ -337,8 +344,8 @@ export function createRecentImageryPanel({
|
||||
},
|
||||
{
|
||||
id: 'clear',
|
||||
label: 'CLEAR',
|
||||
title: 'Forget the box and its images',
|
||||
label: t('CLEAR'),
|
||||
title: t('Forget the box and its images'),
|
||||
disabled: !snapshot.box && !snapshot.boxError,
|
||||
onClick: () => {
|
||||
exportError = null;
|
||||
@@ -366,7 +373,7 @@ export function createRecentImageryPanel({
|
||||
function renderNotice() {
|
||||
const warning = snapshot.boxError || snapshot.error || exportError;
|
||||
const text =
|
||||
warning || snapshot.notice || (snapshot.borrowedEsri ? ESRI_NOTE : '');
|
||||
warning || snapshot.notice || (snapshot.borrowedEsri ? t(ESRI_NOTE) : '');
|
||||
set(noticeText, 'textContent', text);
|
||||
attribute(noticeText, 'title', text);
|
||||
toggle(notice, 'warn', Boolean(warning));
|
||||
@@ -388,7 +395,7 @@ export function createRecentImageryPanel({
|
||||
entry.thumb = el('div', 'ri-thumb', card);
|
||||
entry.placeholder = el('span', 'ri-thumb-text', entry.thumb);
|
||||
entry.flag = el('span', 'ri-card-flag', entry.thumb);
|
||||
entry.start = el('span', 'ri-card-start', entry.thumb, 'START HERE');
|
||||
entry.start = el('span', 'ri-card-start', entry.thumb, t('START HERE'));
|
||||
entry.date = el('div', 'ri-card-date', card);
|
||||
entry.sensor = el('div', 'ri-card-sensor', card);
|
||||
entry.cloud = el('div', 'ri-card-cloud', card);
|
||||
@@ -432,13 +439,17 @@ export function createRecentImageryPanel({
|
||||
}
|
||||
if (entry.image) set(entry.image, 'hidden', !shown);
|
||||
set(entry.placeholder, 'hidden', shown);
|
||||
set(entry.placeholder, 'textContent', PLACEHOLDERS[status] || 'Checking');
|
||||
set(
|
||||
entry.placeholder,
|
||||
'textContent',
|
||||
PLACEHOLDERS[status] ? t(PLACEHOLDERS[status]) : t('Checking'),
|
||||
);
|
||||
if (entry.thumb.dataset.status !== status)
|
||||
entry.thumb.dataset.status = status;
|
||||
const flag = candidate.preview
|
||||
? 'PREVIEW'
|
||||
? t('PREVIEW')
|
||||
: candidate.pending
|
||||
? 'LOADING'
|
||||
? t('LOADING')
|
||||
: '';
|
||||
set(entry.flag, 'textContent', flag);
|
||||
set(entry.flag, 'hidden', !flag);
|
||||
@@ -448,8 +459,10 @@ export function createRecentImageryPanel({
|
||||
set(
|
||||
entry.start,
|
||||
'title',
|
||||
START_HERE_TITLES[snapshot.recommended.reason] ||
|
||||
'Newest day with imagery for this box',
|
||||
t(
|
||||
START_HERE_TITLES[snapshot.recommended.reason] ||
|
||||
msg('Newest day with imagery for this box'),
|
||||
),
|
||||
);
|
||||
set(entry.date, 'textContent', day);
|
||||
set(entry.sensor, 'textContent', sensorLine(candidate.product));
|
||||
@@ -459,7 +472,7 @@ export function createRecentImageryPanel({
|
||||
const chip = entry.chips[slotId];
|
||||
const live = slotId === 'a' || ab;
|
||||
const on = candidate.pinned === slotId && live;
|
||||
const label = slotId === 'a' ? (ab ? 'A' : 'SHOW') : 'B';
|
||||
const label = slotId === 'a' ? (ab ? 'A' : t('SHOW')) : 'B';
|
||||
set(chip, 'textContent', label);
|
||||
set(chip, 'disabled', empty || !live);
|
||||
toggle(chip, 'active', on);
|
||||
@@ -469,22 +482,29 @@ export function createRecentImageryPanel({
|
||||
attribute(
|
||||
chip,
|
||||
'aria-label',
|
||||
ab ? `Pin ${day} as ${slotId.toUpperCase()}` : `Show ${day}`,
|
||||
ab
|
||||
? t('Pin {day} as {slot}', { day, slot: slotId.toUpperCase() })
|
||||
: t('Show {day}', { day }),
|
||||
);
|
||||
}
|
||||
const state = candidate.pinned
|
||||
? ab
|
||||
? ` · pinned ${candidate.pinned.toUpperCase()}`
|
||||
? t('pinned {slot}', { slot: candidate.pinned.toUpperCase() })
|
||||
: candidate.pinned === 'a'
|
||||
? ' · shown'
|
||||
? t('shown')
|
||||
: ''
|
||||
: candidate.preview
|
||||
? ' · preview'
|
||||
? t('preview')
|
||||
: '';
|
||||
attribute(
|
||||
card,
|
||||
'aria-label',
|
||||
`${day} · ${sensorLine(candidate.product)} · ${cloudText(candidate)}${recommended ? ' · start here' : ''}${state}`,
|
||||
[
|
||||
day,
|
||||
sensorLine(candidate.product),
|
||||
cloudText(candidate),
|
||||
...[recommended ? t('start here') : '', state].filter(Boolean),
|
||||
].join(' · '),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,14 +532,14 @@ export function createRecentImageryPanel({
|
||||
candidates.length
|
||||
? ''
|
||||
: !snapshot.box
|
||||
? 'No box'
|
||||
? t('No box')
|
||||
: snapshot.searching
|
||||
? 'Searching'
|
||||
? t('Searching')
|
||||
: snapshot.hiddenCount
|
||||
? 'Every day is empty here'
|
||||
? t('Every day is empty here')
|
||||
: !snapshot.sources.hls && !snapshot.sources.viirs
|
||||
? 'Sources off'
|
||||
: 'No imagery',
|
||||
? t('Sources off')
|
||||
: t('No imagery'),
|
||||
);
|
||||
set(stripEmpty, 'hidden', candidates.length > 0);
|
||||
if (candidates[snapshot.focusIndex])
|
||||
@@ -596,18 +616,22 @@ export function createRecentImageryPanel({
|
||||
set(
|
||||
tag,
|
||||
'textContent',
|
||||
ab ? slotId.toUpperCase() : slotId === 'a' ? 'IMAGE' : 'VS',
|
||||
ab ? slotId.toUpperCase() : slotId === 'a' ? t('IMAGE') : t('VS'),
|
||||
);
|
||||
let text = 'Not set';
|
||||
if (basemapRow) text = 'Basemap';
|
||||
let text = t('Not set');
|
||||
if (basemapRow) text = t('Basemap');
|
||||
else if (image?.label)
|
||||
text = `${image.sourceOff ? 'Source off · ' : ''}${image.label}${
|
||||
text = [
|
||||
image.sourceOff ? t('Source off') : '',
|
||||
image.label,
|
||||
image.preview
|
||||
? ' · preview'
|
||||
? t('preview')
|
||||
: image.candidate && !image.drapable && !image.sourceOff
|
||||
? ' · loading'
|
||||
: ''
|
||||
}`;
|
||||
? t('loading')
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
set(value, 'textContent', text);
|
||||
attribute(value, 'title', text);
|
||||
const lit = basemapRow
|
||||
@@ -625,7 +649,9 @@ export function createRecentImageryPanel({
|
||||
attribute(
|
||||
unpin,
|
||||
'aria-label',
|
||||
ab ? `Unpin ${slotId.toUpperCase()}` : 'Unpin the image',
|
||||
ab
|
||||
? t('Unpin {slot}', { slot: slotId.toUpperCase() })
|
||||
: t('Unpin the image'),
|
||||
);
|
||||
set(unpin, 'title', unpin.getAttribute('aria-label'));
|
||||
}
|
||||
@@ -658,8 +684,12 @@ export function createRecentImageryPanel({
|
||||
type: 'actions',
|
||||
actions: ['a', 'b'].map((slotId) => ({
|
||||
id: `export-${slotId}`,
|
||||
label: ab ? `EXPORT ${slotId.toUpperCase()}` : 'EXPORT',
|
||||
title: `Download ${ab ? slotId.toUpperCase() : 'the image'} as a PNG`,
|
||||
label: ab
|
||||
? t('EXPORT {slot}', { slot: slotId.toUpperCase() })
|
||||
: t('EXPORT'),
|
||||
title: ab
|
||||
? t('Download {slot} as a PNG', { slot: slotId.toUpperCase() })
|
||||
: t('Download the image as a PNG'),
|
||||
disabled: !exportTarget(slotId) || exporting.has(slotId),
|
||||
onClick: () => void exportImage(slotId),
|
||||
})),
|
||||
@@ -682,7 +712,10 @@ export function createRecentImageryPanel({
|
||||
if (snapshot.box)
|
||||
lines.push({
|
||||
id: 'box',
|
||||
text: `Box ${kmText(snapshot.boxSizeKm?.width)} × ${kmText(snapshot.boxSizeKm?.height)} km`,
|
||||
text: t('Box {width} × {height} km', {
|
||||
width: kmText(snapshot.boxSizeKm?.width),
|
||||
height: kmText(snapshot.boxSizeKm?.height),
|
||||
}),
|
||||
});
|
||||
if (focus) {
|
||||
lines.push({ id: 'readout', text: snapshot.readout || '' });
|
||||
@@ -690,14 +723,24 @@ export function createRecentImageryPanel({
|
||||
if (focus.timeRange?.start) {
|
||||
const start = utcTime(focus.timeRange.start);
|
||||
const end = utcTime(focus.timeRange.end || focus.timeRange.start);
|
||||
times.push(`Acquired ${start === end ? start : `${start}–${end}`}`);
|
||||
times.push(
|
||||
t('Acquired {time}', {
|
||||
time: start === end ? start : `${start}–${end}`,
|
||||
}),
|
||||
);
|
||||
} else if (focus.thumbnail?.acquisitionTime)
|
||||
times.push(`Acquired ${utcTime(focus.thumbnail.acquisitionTime)}`);
|
||||
times.push(`Coverage ${focus.coverage || 'unknown'}`);
|
||||
times.push(
|
||||
t('Acquired {time}', {
|
||||
time: utcTime(focus.thumbnail.acquisitionTime),
|
||||
}),
|
||||
);
|
||||
times.push(
|
||||
t('Coverage {coverage}', { coverage: focus.coverage || t('unknown') }),
|
||||
);
|
||||
lines.push({ id: 'acquired', text: times.join(' · ') });
|
||||
(focus.granules || []).forEach((granule, index) => {
|
||||
const cloud = Number.isFinite(granule.cloud)
|
||||
? ` · ${Math.round(granule.cloud)}% cloud`
|
||||
? ` · ${t('{percent}% cloud', { percent: Math.round(granule.cloud) })}`
|
||||
: '';
|
||||
lines.push({
|
||||
id: `granule-${index}`,
|
||||
@@ -707,7 +750,11 @@ export function createRecentImageryPanel({
|
||||
});
|
||||
}
|
||||
const reason = START_HERE_REASONS[snapshot.recommended?.reason];
|
||||
if (reason) lines.push({ id: 'start', text: `START HERE · ${reason}` });
|
||||
if (reason)
|
||||
lines.push({
|
||||
id: 'start',
|
||||
text: t('START HERE · {reason}', { reason: t(reason) }),
|
||||
});
|
||||
for (const [index, note] of snapshot.notes.entries())
|
||||
lines.push({ id: `note-${index}`, text: note, muted: true });
|
||||
if (
|
||||
@@ -718,12 +765,14 @@ export function createRecentImageryPanel({
|
||||
)
|
||||
lines.push({
|
||||
id: 'overview-scale',
|
||||
text: 'Daily overview shows little detail in a box this small',
|
||||
text: t('Daily overview shows little detail in a box this small'),
|
||||
muted: true,
|
||||
});
|
||||
lines.push({
|
||||
id: 'credit',
|
||||
text: 'Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS',
|
||||
text: t(
|
||||
'Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS',
|
||||
),
|
||||
muted: true,
|
||||
});
|
||||
return lines;
|
||||
@@ -735,9 +784,11 @@ export function createRecentImageryPanel({
|
||||
details.update([
|
||||
{
|
||||
id: 'details',
|
||||
title: 'Details',
|
||||
title: t('Details'),
|
||||
open: detailsOpen,
|
||||
compact: snapshot.focus ? snapshot.readout : 'Times, coverage, sources',
|
||||
compact: snapshot.focus
|
||||
? snapshot.readout
|
||||
: t('Times, coverage, sources'),
|
||||
blocks: [
|
||||
{ id: 'lines', type: 'lines', lines: detailLines() },
|
||||
{
|
||||
@@ -747,9 +798,9 @@ export function createRecentImageryPanel({
|
||||
{
|
||||
id: 'toggle-empty',
|
||||
label: showing
|
||||
? 'HIDE EMPTY DAYS'
|
||||
: `SHOW EMPTY DAYS · ${hidden}`,
|
||||
title: 'Days the probe found empty in this box',
|
||||
? t('HIDE EMPTY DAYS')
|
||||
: t('SHOW EMPTY DAYS · {count}', { count: hidden }),
|
||||
title: t('Days the probe found empty in this box'),
|
||||
disabled: !showing && !hidden,
|
||||
onClick: () => layer.setShowUnavailable(!showing),
|
||||
},
|
||||
@@ -773,8 +824,10 @@ export function createRecentImageryPanel({
|
||||
const describe = (c) => (c ? `${sensorLine(c.product)} · ${c.day}` : '');
|
||||
// The two sides as [label, title, spoken name]; SWAP trades them.
|
||||
const sides = [
|
||||
basemap ? ['IMAGE', describe(a), 'image'] : ['A', describe(a), 'A'],
|
||||
basemap ? ['BASEMAP', 'The basemap', 'basemap'] : ['B', describe(b), 'B'],
|
||||
basemap ? [t('IMAGE'), describe(a), t('image')] : ['A', describe(a), 'A'],
|
||||
basemap
|
||||
? [t('BASEMAP'), t('The basemap'), t('basemap')]
|
||||
: ['B', describe(b), 'B'],
|
||||
];
|
||||
const [before, after] = swapped ? [sides[1], sides[0]] : sides;
|
||||
const signature = live
|
||||
@@ -799,10 +852,15 @@ export function createRecentImageryPanel({
|
||||
beforeTitle: before[1],
|
||||
afterTitle: after[1],
|
||||
ariaLabel: basemap
|
||||
? 'Recent imagery against the basemap divider'
|
||||
: 'Recent imagery A and B divider',
|
||||
? t('Recent imagery against the basemap divider')
|
||||
: t('Recent imagery A and B divider'),
|
||||
formatValueText: (leftPercent, rightPercent) =>
|
||||
`${before[2][0].toUpperCase()}${before[2].slice(1)} ${leftPercent} percent, ${after[2]} ${rightPercent} percent`,
|
||||
t('{before} {left} percent, {after} {right} percent', {
|
||||
before: `${before[2][0].toUpperCase()}${before[2].slice(1)}`,
|
||||
left: leftPercent,
|
||||
after: after[2],
|
||||
right: rightPercent,
|
||||
}),
|
||||
getViewportWidth: () =>
|
||||
Number(viewer?.scene?.canvas?.clientWidth) ||
|
||||
Number(document.documentElement?.clientWidth) ||
|
||||
@@ -895,7 +953,9 @@ export function createRecentImageryPanel({
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!destroyed)
|
||||
exportError = `Export failed · ${error?.message || error}`;
|
||||
exportError = t('Export failed · {error}', {
|
||||
error: error?.message || error,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
exporting.delete(slotId);
|
||||
|
||||
Reference in new issue
Block a user