mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(i18n): keep English pixel-identical and stored data English
From the English-parity and breakage review against main:
- English wording back to main: "Configured Source", the "SHARED SUBJECT"
fallback, CCTV calibration "RANGE 120m".
- Voice header and CCTV calibration chip/select tweaks for long labels now
apply to non-English only (html:not([lang='en'])); English matches main.
The scene select font change is scoped the same way.
- New scenes and shots store their default title in English ("Scene 3",
"Shot 2") and translate it when shown, so a title captured in one language
does not leak into projects, storage or shared JSON.
- Choosing a language with storage blocked keeps it in ?lang instead of
silently doing nothing.
- One picked import file shows its name again.
- qa-l9-matrix reads the voice state from data-status, not the label.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a28e29e05e
commit
9d01f9bfcd
15 files changed
+118
-54
No files matched your search
@@ -2006,8 +2006,9 @@ async function runBrowserGroup(record) {
|
|||||||
for (let i = 0; i < 20; i += 1) {
|
for (let i = 0; i < 20; i += 1) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
const status = document.getElementById('gev-voice-status')?.textContent?.trim() || '';
|
// The label is translated; the machine state is on the control.
|
||||||
if (status && status !== 'CONNECTING') break;
|
const status = document.getElementById('gev-voice-control')?.dataset.status || '';
|
||||||
|
if (status && status !== 'connecting') break;
|
||||||
}
|
}
|
||||||
let diag = null;
|
let diag = null;
|
||||||
try { diag = vc.getDiagnostics?.(); } catch { /* not started */ }
|
try { diag = vc.getDiagnostics?.(); } catch { /* not started */ }
|
||||||
|
|||||||
@@ -188,6 +188,31 @@ test('applyDocumentTranslations translates opted-in text and attributes in place
|
|||||||
assert.equal(unknown.textContent, 'No such message');
|
assert.equal(unknown.textContent, 'No such message');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('persistLocaleAndReload keeps the choice in ?lang when storage is blocked', () => {
|
||||||
|
const calls = [];
|
||||||
|
const location = {
|
||||||
|
href: 'https://gev.local/?x=1#scene=a',
|
||||||
|
assign: (url) => calls.push(['assign', url]),
|
||||||
|
reload: () => calls.push(['reload']),
|
||||||
|
};
|
||||||
|
const history = {
|
||||||
|
replaceState: (_s, _t, url) => calls.push(['replace', url]),
|
||||||
|
};
|
||||||
|
const storage = {
|
||||||
|
setItem() {
|
||||||
|
throw new Error('blocked');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
persistLocaleAndReload('ru', { location, history, storage }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['replace', 'https://gev.local/?x=1&lang=ru#scene=a'],
|
||||||
|
['reload'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('persistLocaleAndReload stores the choice, keeps the hash, strips ?lang', () => {
|
test('persistLocaleAndReload stores the choice, keeps the hash, strips ?lang', () => {
|
||||||
const storage = memoryStorage();
|
const storage = memoryStorage();
|
||||||
const calls = [];
|
const calls = [];
|
||||||
|
|||||||
+5
-3
@@ -212,7 +212,7 @@ export function applyDocumentTranslations(doc) {
|
|||||||
/**
|
/**
|
||||||
* Persist a locale choice, then reload preserving window.location.hash exactly
|
* Persist a locale choice, then reload preserving window.location.hash exactly
|
||||||
* and stripping ?lang from the search string, so the override is one-shot.
|
* and stripping ?lang from the search string, so the override is one-shot.
|
||||||
* Storage is best-effort: where it is blocked the choice will not survive.
|
* Where storage is blocked, ?lang carries the choice instead.
|
||||||
* @returns {boolean} true when a reload was triggered.
|
* @returns {boolean} true when a reload was triggered.
|
||||||
*/
|
*/
|
||||||
export function persistLocaleAndReload(
|
export function persistLocaleAndReload(
|
||||||
@@ -224,12 +224,14 @@ export function persistLocaleAndReload(
|
|||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
|
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
|
||||||
writeStoredLocale(normalized, storage);
|
const stored = writeStoredLocale(normalized, storage);
|
||||||
if (!location || typeof location.assign !== 'function') return false;
|
if (!location || typeof location.assign !== 'function') return false;
|
||||||
let target = location.href;
|
let target = location.href;
|
||||||
try {
|
try {
|
||||||
|
// Without storage the choice only survives in the URL, so keep it there.
|
||||||
const url = new URL(location.href);
|
const url = new URL(location.href);
|
||||||
url.searchParams.delete('lang');
|
if (stored) url.searchParams.delete('lang');
|
||||||
|
else url.searchParams.set('lang', normalized);
|
||||||
target = url.href;
|
target = url.href;
|
||||||
} catch {
|
} catch {
|
||||||
// Keep the current href.
|
// Keep the current href.
|
||||||
|
|||||||
@@ -2025,5 +2025,8 @@
|
|||||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Usa contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Usa contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
"{state} {layers}": "{state}: {layers}",
|
||||||
"Selected Space Mission": "Misión espacial seleccionada",
|
"Selected Space Mission": "Misión espacial seleccionada",
|
||||||
"{duration}s + {hold}s": "{duration} s + {hold} s"
|
"{duration}s + {hold}s": "{duration} s + {hold} s",
|
||||||
|
"Configured Source": "Fuente configurada",
|
||||||
|
"subject": "sujeto",
|
||||||
|
"{value}m": "{value} m"
|
||||||
}
|
}
|
||||||
@@ -2057,5 +2057,8 @@
|
|||||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Используется зарегистрированный контент или связанные медиафайлы. Эти внешние файлы не входят в архив сцены. Их исходные уведомления по-прежнему действуют.",
|
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Используется зарегистрированный контент или связанные медиафайлы. Эти внешние файлы не входят в архив сцены. Их исходные уведомления по-прежнему действуют.",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
"{state} {layers}": "{state}: {layers}",
|
||||||
"Selected Space Mission": "Выбранная космическая миссия",
|
"Selected Space Mission": "Выбранная космическая миссия",
|
||||||
"{duration}s + {hold}s": "{duration} с + {hold} с"
|
"{duration}s + {hold}s": "{duration} с + {hold} с",
|
||||||
|
"Configured Source": "Настроенный источник",
|
||||||
|
"subject": "объект",
|
||||||
|
"{value}m": "{value} м"
|
||||||
}
|
}
|
||||||
@@ -507,9 +507,8 @@ export class SceneDirector {
|
|||||||
|
|
||||||
const scene = {
|
const scene = {
|
||||||
id: uid('scene'),
|
id: uid('scene'),
|
||||||
title:
|
// i18n-ignore: stored English, like normalizeProject; displaySceneTitle translates it.
|
||||||
sceneName.trim() ||
|
title: sceneName.trim() || `Scene ${this._project.scenes.length + 1}`,
|
||||||
t('Scene {n}', { n: this._project.scenes.length + 1 }),
|
|
||||||
shots: [],
|
shots: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -967,9 +966,8 @@ export class SceneDirector {
|
|||||||
const shot = normalizeShot(
|
const shot = normalizeShot(
|
||||||
{
|
{
|
||||||
id: uid('shot'),
|
id: uid('shot'),
|
||||||
title: t('Shot {n}', {
|
// i18n-ignore: stored English; displayShotTitle translates it when shown.
|
||||||
n: scene.shots.length + 1,
|
title: `Shot ${scene.shots.length + 1}`,
|
||||||
}),
|
|
||||||
durationSec: DEFAULT_SHOT_DURATION_SEC,
|
durationSec: DEFAULT_SHOT_DURATION_SEC,
|
||||||
holdSec: DEFAULT_HOLD_SEC,
|
holdSec: DEFAULT_HOLD_SEC,
|
||||||
camera,
|
camera,
|
||||||
|
|||||||
@@ -1045,8 +1045,10 @@ const BUILTIN_SHOT_TITLES = new Set([
|
|||||||
*/
|
*/
|
||||||
export function displaySceneTitle(scene) {
|
export function displaySceneTitle(scene) {
|
||||||
const builtin = SCENE_RECIPES.find((recipe) => recipe.id === scene?.id);
|
const builtin = SCENE_RECIPES.find((recipe) => recipe.id === scene?.id);
|
||||||
if (!builtin?.title) return scene?.title || '';
|
if (builtin?.title && scene.title === builtin.title) return t(builtin.title);
|
||||||
return scene.title === builtin.title ? t(builtin.title) : scene.title;
|
const title = scene?.title || '';
|
||||||
|
const numbered = /^Scene (\d+)$/.exec(title);
|
||||||
|
return numbered ? t('Scene {n}', { n: Number(numbered[1]) }) : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { formatNumber, t, msg } from '../i18n/index.js';
|
import { formatNumber, t, msg } from '../i18n/index.js';
|
||||||
import { formatQuantity } from './unitLabels.js';
|
|
||||||
|
|
||||||
/** Shortest-wrap signed degrees, for heading offsets typed as absolute values. */
|
/** Shortest-wrap signed degrees, for heading offsets typed as absolute values. */
|
||||||
const signedNormalizeDeg = (deg) => ((((deg + 180) % 360) + 360) % 360) - 180;
|
const signedNormalizeDeg = (deg) => ((((deg + 180) % 360) + 360) % 360) - 180;
|
||||||
@@ -73,8 +72,14 @@ function calValueText(field, value) {
|
|||||||
minimumFractionDigits: field.decimals,
|
minimumFractionDigits: field.decimals,
|
||||||
maximumFractionDigits: field.decimals,
|
maximumFractionDigits: field.decimals,
|
||||||
};
|
};
|
||||||
if (field.unit === 'm')
|
if (field.unit === 'm') {
|
||||||
return `${t(field.label)} ${formatQuantity(Number(value), 'm', options)}`;
|
// English keeps its compact "RANGE 120m"; other languages space the unit.
|
||||||
|
const metres = formatNumber(Number(value), {
|
||||||
|
useGrouping: false,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return `${t(field.label)} ${t('{value}m', { value: metres })}`;
|
||||||
|
}
|
||||||
const number = formatNumber(Number(value), {
|
const number = formatNumber(Number(value), {
|
||||||
useGrouping: false,
|
useGrouping: false,
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const SOURCE_MESSAGES = Object.freeze({
|
|||||||
|
|
||||||
const SOURCE_LABELS = Object.freeze({
|
const SOURCE_LABELS = Object.freeze({
|
||||||
'Configured source': msg('Configured source'),
|
'Configured source': msg('Configured source'),
|
||||||
'Configured Source': msg('Configured source'),
|
'Configured Source': msg('Configured Source'),
|
||||||
'Synthetic fallback': msg('Synthetic fallback'),
|
'Synthetic fallback': msg('Synthetic fallback'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -126,9 +126,13 @@ export function createSceneDialog(title, onClose) {
|
|||||||
const chosen = document.createElement('span');
|
const chosen = document.createElement('span');
|
||||||
const showChosen = () => {
|
const showChosen = () => {
|
||||||
const count = node.files?.length || 0;
|
const count = node.files?.length || 0;
|
||||||
chosen.textContent = count
|
// One file shows its name, like the browser's own picker did.
|
||||||
? tn(count, '{count} file selected', '{count} files selected')
|
chosen.textContent =
|
||||||
: t('No file chosen');
|
count === 1
|
||||||
|
? node.files[0].name
|
||||||
|
: count
|
||||||
|
? tn(count, '{count} file selected', '{count} files selected')
|
||||||
|
: t('No file chosen');
|
||||||
};
|
};
|
||||||
showChosen();
|
showChosen();
|
||||||
row.append(chooser, chosen);
|
row.append(chooser, chosen);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export class ShareRestoration {
|
|||||||
state: 'acquiring',
|
state: 'acquiring',
|
||||||
detail: t('SHARED {subject}', {
|
detail: t('SHARED {subject}', {
|
||||||
subject: String(
|
subject: String(
|
||||||
result.label ? t(result.label) : t('entity'),
|
result.label ? t(result.label) : t('subject'),
|
||||||
).toUpperCase(),
|
).toUpperCase(),
|
||||||
}),
|
}),
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
|||||||
@@ -139,15 +139,14 @@ export class ShellFeedback {
|
|||||||
// the flap keeps textContent equal to the settled label throughout, so
|
// the flap keeps textContent equal to the settled label throughout, so
|
||||||
// this stays a no-op on the repeat ticks exactly as it did before.
|
// this stays a no-op on the repeat ticks exactly as it did before.
|
||||||
if (presentation.label)
|
if (presentation.label)
|
||||||
if (presentation.label)
|
setSplitFlapText(
|
||||||
setSplitFlapText(
|
this._trafficSyncLabel,
|
||||||
this._trafficSyncLabel,
|
localizeStatusLabel(presentation.label),
|
||||||
localizeStatusLabel(presentation.label),
|
{
|
||||||
{
|
// Completion is a timing signal, not another animated loading stage.
|
||||||
// Completion is a timing signal, not another animated loading stage.
|
immediate: !presentation.busy,
|
||||||
immediate: !presentation.busy,
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
// Written on every change INCLUDING the empty settled value — the reducer
|
// Written on every change INCLUDING the empty settled value — the reducer
|
||||||
// clears the progress number once the sync lands, and a truthiness guard
|
// clears the progress number once the sync lands, and a truthiness guard
|
||||||
// here would strand the last "..." beside the settled label.
|
// here would strand the last "..." beside the settled label.
|
||||||
|
|||||||
+26
-11
@@ -106,26 +106,22 @@ html[lang='es'] .cctv-controls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Click-to-edit effective-pose readout (viewshed/gizmo design §3d): compact
|
/* Click-to-edit effective-pose readout (viewshed/gizmo design §3d): compact
|
||||||
value chips replace the seven offset sliders. Tight enough that the seven
|
value chips replace the seven offset sliders. */
|
||||||
chips take two rows in a long-label language too, which keeps SAVE / RESET
|
|
||||||
in view; chips grow to fill each row, and a value never wraps inside one. */
|
|
||||||
.cctv-cal-readout {
|
.cctv-cal-readout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cctv-cal-value {
|
.cctv-cal-value {
|
||||||
flex: 1 1 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.7px;
|
||||||
color: #91edff;
|
color: #91edff;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
padding: 4px;
|
padding: 4px 7px;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.15s ease,
|
border-color 0.15s ease,
|
||||||
@@ -175,9 +171,8 @@ html[lang='es'] .cctv-controls {
|
|||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-sans);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.04em;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -389,3 +384,23 @@ html[lang='es'] .cctv-controls {
|
|||||||
color: #a8f7ff;
|
color: #a8f7ff;
|
||||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Longer translated labels and units: tighter calibration chips keep the
|
||||||
|
seven values to two rows so SAVE / RESET stay in view, and the camera
|
||||||
|
select uses the compact mono face. English keeps its original look. */
|
||||||
|
html:not([lang='en']) .cctv-cal-readout {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not([lang='en']) .cctv-cal-value {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not([lang='en']) #cctv-camera-select {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
@@ -158,9 +158,8 @@
|
|||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-sans);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.04em;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -466,3 +465,9 @@
|
|||||||
.director-sharing-dialog::backdrop {
|
.director-sharing-dialog::backdrop {
|
||||||
background: rgba(0, 8, 14, 0.65);
|
background: rgba(0, 8, 14, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html:not([lang='en']) #scene-select {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
@@ -89,17 +89,18 @@
|
|||||||
cost past the card edge. The kicker already collapses to zero width here;
|
cost past the card edge. The kicker already collapses to zero width here;
|
||||||
taking it out of the row returns its gap to the state label. The row also
|
taking it out of the row returns its gap to the state label. The row also
|
||||||
reaches left over the gutter beside the mic ring: at heading height the
|
reaches left over the gutter beside the mic ring: at heading height the
|
||||||
ring has already curved away. */
|
ring has already curved away. English fits as it is, so this applies to
|
||||||
#command-dock .gev-voice-heading {
|
the other languages only. */
|
||||||
|
html:not([lang='en']) #command-dock .gev-voice-heading {
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
margin-left: -0.6rem;
|
margin-left: -0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#command-dock .gev-voice-heading > .gev-voice-kicker {
|
html:not([lang='en']) #command-dock .gev-voice-heading > .gev-voice-kicker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#command-dock .gev-voice-heading > #gev-voice-status {
|
html:not([lang='en']) #command-dock .gev-voice-heading > #gev-voice-status {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -108,24 +109,25 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#command-dock .gev-voice-cost {
|
html:not([lang='en']) #command-dock .gev-voice-cost {
|
||||||
gap: 0.16rem;
|
gap: 0.16rem;
|
||||||
padding-left: 0.1rem;
|
padding-left: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#command-dock .gev-voice-tier-btn {
|
html:not([lang='en']) #command-dock .gev-voice-tier-btn {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#command-dock .gev-voice-cost-value {
|
html:not([lang='en']) #command-dock .gev-voice-cost-value {
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Live hints ("Ask or command", "Requesting microphone") run longer in other
|
/* Live hints ("Ask or command", "Requesting microphone") run longer in other
|
||||||
languages; give them a second line from the spare space under the readout
|
languages; give them a second line from the spare space under the readout
|
||||||
rather than cutting them mid-word. */
|
rather than cutting them mid-word. */
|
||||||
#command-dock
|
html:not([lang='en'])
|
||||||
|
#command-dock
|
||||||
#gev-voice-control:is(
|
#gev-voice-control:is(
|
||||||
[data-status='connecting'],
|
[data-status='connecting'],
|
||||||
[data-status='listening'],
|
[data-status='listening'],
|
||||||
|
|||||||
Reference in new issue
Block a user