mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +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
@@ -3,14 +3,14 @@ import { normalizeLongitude, sampleWind } from './model.js';
|
||||
/** Fixed physical scales shared by imagery and the field legend. */
|
||||
export const WIND_FIELDS = Object.freeze({
|
||||
speed: Object.freeze({
|
||||
label: 'Wind speed',
|
||||
label: 'Wind speed', // i18n-ignore: internal field name; the shown label comes from index.js
|
||||
units: 'm/s',
|
||||
min: 0,
|
||||
max: 30,
|
||||
stops: ['#133a67', '#2876a3', '#66c9d1', '#c8dfac', '#f1cf79', '#ee9761'],
|
||||
}),
|
||||
temperature: Object.freeze({
|
||||
label: '2 m air temperature',
|
||||
label: '2 m air temperature', // i18n-ignore: internal field name; the shown label comes from index.js
|
||||
units: '°C',
|
||||
min: -40,
|
||||
max: 50,
|
||||
@@ -30,7 +30,7 @@ export const WIND_FIELDS = Object.freeze({
|
||||
],
|
||||
}),
|
||||
pressure: Object.freeze({
|
||||
label: 'Mean sea-level pressure',
|
||||
label: 'Mean sea-level pressure', // i18n-ignore: internal field name; the shown label comes from index.js
|
||||
units: 'hPa',
|
||||
min: 960,
|
||||
max: 1050,
|
||||
|
||||
+48
-37
@@ -11,6 +11,7 @@ import {
|
||||
createWindInspectionMarker,
|
||||
WIND_UNITS,
|
||||
} from './inspection.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
/** Format a forecast timestamp explicitly in UTC. */
|
||||
export function formatWindValidTime(value) {
|
||||
@@ -242,7 +243,7 @@ export function createWindLayer({
|
||||
getRowControls() {
|
||||
const historyStatus =
|
||||
clock?.getState().mode === 'history'
|
||||
? 'Forecast · does not follow history'
|
||||
? t('Forecast · does not follow history')
|
||||
: null;
|
||||
const valid = formatWindValidTime(manifest?.cycle?.validIso);
|
||||
const run = formatWindValidTime(manifest?.cycle?.runIso);
|
||||
@@ -261,10 +262,10 @@ export function createWindLayer({
|
||||
const imageryError =
|
||||
enabled && !loading ? diagnostic?.imageryError : null;
|
||||
const label = {
|
||||
none: 'Wind motion',
|
||||
speed: 'Wind speed',
|
||||
pressure: 'Sea-level pressure',
|
||||
temperature: 'Air temperature · 2 m',
|
||||
none: t('Wind motion'),
|
||||
speed: t('Wind speed'),
|
||||
pressure: t('Sea-level pressure'),
|
||||
temperature: t('Air temperature · 2 m'),
|
||||
}[overlay];
|
||||
const kind = ['temperature', 'pressure'].includes(overlay)
|
||||
? overlay
|
||||
@@ -290,60 +291,64 @@ export function createWindLayer({
|
||||
readout: true,
|
||||
summary: {
|
||||
label,
|
||||
coverage: 'Global · 1° grid',
|
||||
coverage: t('Global · 1° grid'),
|
||||
validTime: manifest?.cycle?.validIso,
|
||||
issuedTime: manifest?.cycle?.runIso,
|
||||
detail: `${model === 'ifs' ? 'ECMWF IFS' : 'GFS'} forecast · ${valid || 'Unavailable'}`,
|
||||
detail: t('{model} forecast · {time}', {
|
||||
model: model === 'ifs' ? 'ECMWF IFS' : 'GFS',
|
||||
time: valid || t('Unavailable'),
|
||||
}),
|
||||
status: loading
|
||||
? 'Loading forecast'
|
||||
? t('Loading forecast')
|
||||
: preparing
|
||||
? 'Preparing flow'
|
||||
? t('Preparing flow')
|
||||
: error ||
|
||||
manifest?.reason ||
|
||||
imageryError ||
|
||||
(scalarMissing ? 'Selected field unavailable' : null) ||
|
||||
(manifest?.stale ? 'Cached forecast · stale' : null),
|
||||
(scalarMissing ? t('Selected field unavailable') : null) ||
|
||||
(manifest?.stale ? t('Cached forecast · stale') : null),
|
||||
units: legendUnit,
|
||||
},
|
||||
chips: [
|
||||
...['gfs', 'ifs'].map((value) => ({
|
||||
id: `model-${value}`,
|
||||
label: value === 'ifs' ? 'ECMWF' : 'GFS',
|
||||
label: value === 'ifs' ? 'ECMWF' : 'GFS', // i18n-ignore: forecast model names
|
||||
active: model === value,
|
||||
params: { model: value },
|
||||
title:
|
||||
value === 'ifs'
|
||||
? 'ECMWF IFS surface forecast'
|
||||
: 'NOAA GFS surface forecast',
|
||||
? t('ECMWF IFS surface forecast')
|
||||
: t('NOAA GFS surface forecast'),
|
||||
})),
|
||||
{
|
||||
id: 'motion',
|
||||
label: diagnostic?.reducedMotion
|
||||
? 'Reduced motion'
|
||||
? t('Reduced motion')
|
||||
: paused
|
||||
? 'Resume'
|
||||
: 'Pause',
|
||||
? t('Resume')
|
||||
: t('Pause'),
|
||||
active: motionPaused,
|
||||
disabled: Boolean(diagnostic?.reducedMotion),
|
||||
params: { paused: !paused },
|
||||
title:
|
||||
title: t(
|
||||
'Pause visual flow; forecast time does not advance with animation',
|
||||
),
|
||||
},
|
||||
...['none', 'speed', 'pressure', 'temperature'].map((value) => ({
|
||||
id: `overlay-${value}`,
|
||||
label: {
|
||||
none: 'None',
|
||||
speed: 'Speed',
|
||||
pressure: 'Pressure',
|
||||
temperature: 'Temperature',
|
||||
none: t('None'),
|
||||
speed: t('Speed'),
|
||||
pressure: t('Pressure'),
|
||||
temperature: t('Temperature'),
|
||||
}[value],
|
||||
active: overlay === value,
|
||||
params: { overlay: value },
|
||||
title: {
|
||||
none: 'Wind trails with no field shading',
|
||||
speed: 'How hard the surface wind is blowing',
|
||||
pressure: 'Sea-level air pressure: broad highs and lows',
|
||||
temperature: 'Air temperature two meters above the surface',
|
||||
none: t('Wind trails with no field shading'),
|
||||
speed: t('How hard the surface wind is blowing'),
|
||||
pressure: t('Sea-level air pressure: broad highs and lows'),
|
||||
temperature: t('Air temperature two meters above the surface'),
|
||||
}[value],
|
||||
})),
|
||||
...(speedLegendVisible ||
|
||||
@@ -356,15 +361,16 @@ export function createWindLayer({
|
||||
label: value,
|
||||
active: units === value,
|
||||
params: { units: value },
|
||||
title: 'Wind speed units',
|
||||
title: t('Wind speed units'),
|
||||
})),
|
||||
{
|
||||
id: 'read-wind',
|
||||
label: 'Read wind at map center',
|
||||
label: t('Read wind at map center'),
|
||||
params: { inspect: true },
|
||||
disabled: loading || !manifest?.u || manifest?.unavailable,
|
||||
title:
|
||||
title: t(
|
||||
'Read the forecast at the center of the map without changing selection',
|
||||
),
|
||||
},
|
||||
],
|
||||
legend:
|
||||
@@ -373,9 +379,14 @@ export function createWindLayer({
|
||||
(overlay === 'none' && !speedLegendVisible)
|
||||
? []
|
||||
: legend,
|
||||
info: `${model === 'ifs' ? 'ECMWF IFS' : 'GFS'} forecast · ${label} (${legendUnit})${historyStatus ? `\n${historyStatus}` : ''}\nValid: ${valid || 'Unavailable'}${loading ? ' · loading' : preparing ? ' · preparing' : ''}\nIssued: ${run || 'Unavailable'}${manifest?.stale ? ' · STALE' : ''}${error ? '\n' + error : ''}${scalarMissing ? '\nSelected field unavailable · wind remains visible' : ''}${imageryError && !scalarMissing ? '\n' + imageryError + ' · wind remains visible' : ''}`,
|
||||
infoTitle:
|
||||
info: `${t('{model} forecast · {field} ({unit})', {
|
||||
model: model === 'ifs' ? 'ECMWF IFS' : 'GFS',
|
||||
field: label,
|
||||
unit: legendUnit,
|
||||
})}${historyStatus ? `\n${historyStatus}` : ''}\n${t('Valid: {time}', { time: valid || t('Unavailable') })}${loading ? ` · ${t('loading')}` : preparing ? ` · ${t('preparing')}` : ''}\n${t('Issued: {time}', { time: run || t('Unavailable') })}${manifest?.stale ? ' · STALE' : ''}${error ? '\n' + error : ''}${scalarMissing ? `\n${t('Selected field unavailable · wind remains visible')}` : ''}${imageryError && !scalarMissing ? `\n${t('{error} · wind remains visible', { error: imageryError })}` : ''}`,
|
||||
infoTitle: t(
|
||||
'Surface wind at 10 m. Approximately 1° global grid. Curves follow the 10 m wind field, lifted 12 km for visibility; display height is not weather altitude. View lighting is for readability. Animation shows flow through one fixed forecast; it does not advance time. Color fields drape the globe basemap or the active photorealistic 3D Tiles.',
|
||||
),
|
||||
};
|
||||
controls.summary.reading = reading;
|
||||
controls.summary.result = reading ? windReadingResult(reading) : null;
|
||||
@@ -385,12 +396,12 @@ export function createWindLayer({
|
||||
chips: controls.chips.filter((chip) => chip.id.startsWith(prefix)),
|
||||
});
|
||||
controls.summary.settings = [
|
||||
setting('model', 'MODEL', 'model-'),
|
||||
setting('field', 'FIELD', 'overlay-'),
|
||||
setting('model', t('MODEL'), 'model-'),
|
||||
setting('field', t('FIELD'), 'overlay-'),
|
||||
...(controls.chips.some(({ id }) => id.startsWith('units-'))
|
||||
? [{ id: 'units', label: 'UNITS', chips: windUnitChips(units) }]
|
||||
? [{ id: 'units', label: t('UNITS'), chips: windUnitChips(units) }]
|
||||
: []),
|
||||
setting('motion', 'MOTION', 'motion'),
|
||||
setting('motion', t('MOTION'), 'motion'),
|
||||
];
|
||||
controls.summary.actions = controls.chips.filter(
|
||||
({ id }) => id === 'read-wind',
|
||||
@@ -425,7 +436,7 @@ export function createWindLayer({
|
||||
: null;
|
||||
return {
|
||||
...windStats(manifest),
|
||||
countLabel: 'Forecast',
|
||||
countLabel: t('Forecast'),
|
||||
loading: loading || Boolean(preparing),
|
||||
model: model.toUpperCase(),
|
||||
overlay,
|
||||
@@ -433,7 +444,7 @@ export function createWindLayer({
|
||||
stale: Boolean(manifest?.stale),
|
||||
source: model === 'ifs' ? 'ECMWF IFS' : 'NOAA GFS',
|
||||
validTime:
|
||||
formatWindValidTime(manifest?.cycle?.validIso) || 'Unavailable',
|
||||
formatWindValidTime(manifest?.cycle?.validIso) || t('Unavailable'),
|
||||
error:
|
||||
error ||
|
||||
windStats(manifest).error ||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { sampleWind, windSpeed } from './model.js';
|
||||
import { sampleScalar } from './fields.js';
|
||||
|
||||
// The reading stays English: AI tools read it too. Text is marked for i18n
|
||||
// extraction and translated where the WEATHER card shows it (presentation.js).
|
||||
const msg = (text) => text;
|
||||
|
||||
const DIRECTIONS = [
|
||||
'N',
|
||||
'NNE',
|
||||
'NE',
|
||||
'ENE',
|
||||
'E',
|
||||
'ESE',
|
||||
'SE',
|
||||
'SSE',
|
||||
'S',
|
||||
'SSW',
|
||||
'SW',
|
||||
'WSW',
|
||||
'W',
|
||||
'WNW',
|
||||
'NW',
|
||||
'NNW',
|
||||
msg('N'),
|
||||
msg('NNE'),
|
||||
msg('NE'),
|
||||
msg('ENE'),
|
||||
msg('E'),
|
||||
msg('ESE'),
|
||||
msg('SE'),
|
||||
msg('SSE'),
|
||||
msg('S'),
|
||||
msg('SSW'),
|
||||
msg('SW'),
|
||||
msg('WSW'),
|
||||
msg('W'),
|
||||
msg('WNW'),
|
||||
msg('NW'),
|
||||
msg('NNW'),
|
||||
];
|
||||
export const WIND_UNITS = Object.freeze({
|
||||
'km/h': 3.6,
|
||||
@@ -56,8 +60,8 @@ export function inspectWindAtCenter(
|
||||
));
|
||||
if (!point || !snapshot?.u || !snapshot?.v)
|
||||
return {
|
||||
coordinates: 'No surface reading',
|
||||
wind: 'Aim the map center at Earth',
|
||||
coordinates: msg('No surface reading'),
|
||||
wind: msg('Aim the map center at Earth'),
|
||||
model,
|
||||
validTime,
|
||||
status,
|
||||
@@ -87,11 +91,11 @@ export function inspectWindAtCenter(
|
||||
scalarKind: overlay,
|
||||
scalarLabel:
|
||||
overlay === 'speed'
|
||||
? 'Wind speed'
|
||||
? msg('Wind speed')
|
||||
: overlay === 'temperature'
|
||||
? 'Air temperature · 2 m'
|
||||
? msg('Air temperature · 2 m')
|
||||
: overlay === 'pressure'
|
||||
? 'Sea-level pressure'
|
||||
? msg('Sea-level pressure')
|
||||
: null,
|
||||
scalarValue:
|
||||
overlay === 'speed'
|
||||
@@ -102,8 +106,9 @@ export function inspectWindAtCenter(
|
||||
model,
|
||||
validTime,
|
||||
status,
|
||||
explanation:
|
||||
explanation: msg(
|
||||
'Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatWindSpeed, WIND_UNITS } from './inspection.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
/** Reformat a captured sample; changing units never samples the map again. */
|
||||
export function formatWindReading(reading, units) {
|
||||
@@ -22,33 +23,52 @@ export function windUnitChips(units) {
|
||||
label: value,
|
||||
active: units === value,
|
||||
params: { units: value },
|
||||
title: 'Wind speed units',
|
||||
title: t('Wind speed units'),
|
||||
}));
|
||||
}
|
||||
|
||||
/** The wind line in the active language; the reading itself stays English. */
|
||||
function windText(reading) {
|
||||
if (!Number.isFinite(reading.speed))
|
||||
return reading.wind === 'Aim the map center at Earth'
|
||||
? t('Aim the map center at Earth')
|
||||
: reading.wind;
|
||||
const speed = formatWindSpeed(reading.speed, reading.units);
|
||||
return reading.from === 'Calm'
|
||||
? `${speed}${t(' · calm')}`
|
||||
: `${speed}${t(' from {direction}', { direction: t(reading.from) })}`;
|
||||
}
|
||||
|
||||
/** Portable result block consumed by the WEATHER card. */
|
||||
export function windReadingResult(reading) {
|
||||
const coordinates =
|
||||
reading.coordinates === 'No surface reading'
|
||||
? t('No surface reading')
|
||||
: reading.coordinates.replace(' · ', ' ');
|
||||
return {
|
||||
id: 'reading',
|
||||
label: `WIND AT ${reading.coordinates.replace(' · ', ' ')}`,
|
||||
label: t('WIND AT {coordinates}', { coordinates }),
|
||||
lines: [
|
||||
{
|
||||
id: 'wind',
|
||||
text: reading.wind,
|
||||
text: windText(reading),
|
||||
},
|
||||
{
|
||||
id: 'meta',
|
||||
text: `${reading.model} · valid ${reading.validTime?.replace(/^\d{4}-/, '')}`,
|
||||
text: t('{model} · valid {time}', {
|
||||
model: reading.model,
|
||||
time: reading.validTime?.replace(/^\d{4}-/, ''),
|
||||
}),
|
||||
},
|
||||
...(reading.scalarValue
|
||||
? [
|
||||
{
|
||||
id: 'scalar',
|
||||
text: `${reading.scalarLabel} · ${reading.scalarValue}`,
|
||||
text: `${t(reading.scalarLabel)} · ${reading.scalarValue}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ id: 'explanation', text: reading.explanation },
|
||||
{ id: 'explanation', text: t(reading.explanation) },
|
||||
],
|
||||
clear: { params: { inspect: false } },
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user