mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Switch language in place, without a reload
Choosing a language in the picker now applies it at once. The choice is stored and ?lang is kept in step through replaceState, and the picker keeps focus. Code that writes translated text itself re-presents it on a switch: onLocaleChange() lets an owner listen, removed at teardown, and status writers hand over a function of the locale (voice cards, scene director, SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV, launch, awareness and credit texts follow the same hook, and applyDocumentTranslations can be applied again without undoing text code wrote since. The unsaved-work registry and persistLocaleAndReload existed only to guard the reload, so they go, with their catalog entries.
This commit is contained in:
1 parent
ba20bc13d8
commit
b20c80e590
87 files changed
+1194
-777
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { t, msg, onLocaleChange } from '../i18n/index.js';
|
||||
|
||||
const YOUTUBE_HOSTS = new Set([
|
||||
'youtube.com',
|
||||
@@ -674,6 +674,30 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
let record = null;
|
||||
let warmRecord = null;
|
||||
let generation = 0;
|
||||
// Text the cards show, kept as the calls that word it so a locale change
|
||||
// words it again. A node is forgotten once it has left the page.
|
||||
const worded = new Map();
|
||||
function forgetDetached() {
|
||||
for (const node of worded.keys())
|
||||
if (!node.isConnected) worded.delete(node);
|
||||
}
|
||||
function word(node, property, text) {
|
||||
let properties = worded.get(node);
|
||||
if (!properties) worded.set(node, (properties = new Map()));
|
||||
properties.set(property, text);
|
||||
writeWord(node, property, text());
|
||||
}
|
||||
function writeWord(node, property, value) {
|
||||
if (property === 'textContent' || property === 'title')
|
||||
node[property] = value;
|
||||
else node.setAttribute(property, value);
|
||||
}
|
||||
const offLocale = onLocaleChange(() => {
|
||||
forgetDetached();
|
||||
for (const [node, properties] of worded)
|
||||
for (const [property, text] of properties)
|
||||
writeWord(node, property, text());
|
||||
});
|
||||
const leavingRecords = new Set();
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
@@ -818,6 +842,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
current.youtubeSession?.destroy();
|
||||
current.card?.remove?.();
|
||||
leavingRecords.delete(current);
|
||||
forgetDetached();
|
||||
}
|
||||
|
||||
const position = () => {
|
||||
@@ -871,6 +896,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
destroyFacebookSession(warmRecord.facebookSession);
|
||||
else warmRecord.iframe?.remove?.();
|
||||
warmRecord = null;
|
||||
forgetDetached();
|
||||
}
|
||||
|
||||
function takeWarmFacebook(key, host) {
|
||||
@@ -917,9 +943,11 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
startAtSec,
|
||||
endAtSec,
|
||||
});
|
||||
iframe.title = t('Preloading {provider} media', {
|
||||
provider: providerLabel(source.provider),
|
||||
});
|
||||
word(iframe, 'title', () =>
|
||||
t('Preloading {provider} media', {
|
||||
provider: providerLabel(source.provider),
|
||||
}),
|
||||
);
|
||||
iframe.tabIndex = -1;
|
||||
iframe.setAttribute('aria-hidden', 'true');
|
||||
iframe.loading = 'eager';
|
||||
@@ -930,7 +958,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
}
|
||||
|
||||
async function mountX(current, token) {
|
||||
current.status.textContent = t('LOADING X POST…');
|
||||
word(current.status, 'textContent', () => t('LOADING X POST…'));
|
||||
try {
|
||||
const api = await xLoader(documentRef, globalRef);
|
||||
if (token !== generation || record !== current) return;
|
||||
@@ -950,14 +978,18 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
throw new Error(
|
||||
msg('The X post is unavailable or cannot be embedded.'),
|
||||
);
|
||||
current.status.textContent = t('X POST · PROVIDER CONTROLS');
|
||||
word(current.status, 'textContent', () =>
|
||||
t('X POST · PROVIDER CONTROLS'),
|
||||
);
|
||||
position();
|
||||
} catch (error) {
|
||||
if (token !== generation || record !== current) return;
|
||||
current.card.classList.add('is-unavailable');
|
||||
current.status.textContent = t('{reason} OPEN ORIGINAL.', {
|
||||
reason: t(String(error?.message || msg('X embed unavailable'))),
|
||||
});
|
||||
word(current.status, 'textContent', () =>
|
||||
t('{reason} OPEN ORIGINAL.', {
|
||||
reason: t(String(error?.message || msg('X embed unavailable'))),
|
||||
}),
|
||||
);
|
||||
position();
|
||||
}
|
||||
}
|
||||
@@ -970,22 +1002,28 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
session.shouldPlay = autoplay === true;
|
||||
session.onReady = () => {
|
||||
if (token !== generation || record !== current) return;
|
||||
current.status.textContent = t('FACEBOOK · PROVIDER CONTROLS');
|
||||
word(current.status, 'textContent', () =>
|
||||
t('FACEBOOK · PROVIDER CONTROLS'),
|
||||
);
|
||||
current.card.classList.remove('is-unavailable');
|
||||
position();
|
||||
};
|
||||
session.onError = (error) => {
|
||||
if (token !== generation || record !== current) return;
|
||||
current.card.classList.add('is-unavailable');
|
||||
current.status.textContent = t('{reason} OPEN ORIGINAL.', {
|
||||
reason: t(String(error?.message || msg('Facebook embed unavailable'))),
|
||||
});
|
||||
word(current.status, 'textContent', () =>
|
||||
t('{reason} OPEN ORIGINAL.', {
|
||||
reason: t(
|
||||
String(error?.message || msg('Facebook embed unavailable')),
|
||||
),
|
||||
}),
|
||||
);
|
||||
position();
|
||||
};
|
||||
session.onPlaybackBlocked = () => {
|
||||
if (token !== generation || record !== current) return;
|
||||
current.status.textContent = t(
|
||||
'AUTOPLAY BLOCKED · USE PROVIDER CONTROLS',
|
||||
word(current.status, 'textContent', () =>
|
||||
t('AUTOPLAY BLOCKED · USE PROVIDER CONTROLS'),
|
||||
);
|
||||
};
|
||||
if (session.ready) session.onReady();
|
||||
@@ -1036,8 +1074,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
const portraitClass = isPortraitMedia(media) ? ' is-portrait' : '';
|
||||
card.className = `bhote-embedded-callout is-${source.provider}${portraitClass}`;
|
||||
card.dataset.provider = source.provider;
|
||||
card.setAttribute(
|
||||
'aria-label',
|
||||
word(card, 'aria-label', () =>
|
||||
t('Embedded {provider} media for {source}', {
|
||||
provider: providerLabel(source.provider),
|
||||
source: observation?.title
|
||||
@@ -1057,19 +1094,23 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
player.className = 'bhote-embedded-callout-player ui-scroll';
|
||||
const footer = documentRef.createElement('footer');
|
||||
const title = documentRef.createElement('strong');
|
||||
title.textContent = bhoteEventText(
|
||||
observation?.shortTitle || observation?.title || t('Witness source'),
|
||||
).toUpperCase();
|
||||
word(title, 'textContent', () =>
|
||||
bhoteEventText(
|
||||
observation?.shortTitle || observation?.title || t('Witness source'),
|
||||
).toUpperCase(),
|
||||
);
|
||||
const status = documentRef.createElement('small');
|
||||
status.setAttribute('role', 'status');
|
||||
status.textContent = t('{provider} · EMBEDDED SOURCE', {
|
||||
provider: providerLabel(source.provider),
|
||||
});
|
||||
word(status, 'textContent', () =>
|
||||
t('{provider} · EMBEDDED SOURCE', {
|
||||
provider: providerLabel(source.provider),
|
||||
}),
|
||||
);
|
||||
const link = documentRef.createElement('a');
|
||||
link.href = source.url;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.textContent = t('OPEN ORIGINAL ↗');
|
||||
word(link, 'textContent', () => t('OPEN ORIGINAL ↗'));
|
||||
footer.append(title, status, link);
|
||||
const body = documentRef.createElement('div');
|
||||
body.className = 'bhote-embedded-callout-body';
|
||||
@@ -1109,12 +1150,14 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
url.searchParams.set('origin', globalRef.location.origin);
|
||||
iframe.src = url.href;
|
||||
}
|
||||
iframe.title = t('{provider} media: {source}', {
|
||||
provider: providerLabel(source.provider),
|
||||
source: observation?.title
|
||||
? bhoteEventText(observation.title)
|
||||
: t('witness source'),
|
||||
});
|
||||
word(iframe, 'title', () =>
|
||||
t('{provider} media: {source}', {
|
||||
provider: providerLabel(source.provider),
|
||||
source: observation?.title
|
||||
? bhoteEventText(observation.title)
|
||||
: t('witness source'),
|
||||
}),
|
||||
);
|
||||
iframe.allow =
|
||||
'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
||||
iframe.allowFullscreen = true;
|
||||
@@ -1131,9 +1174,11 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
record.youtubeSession.getState().phase,
|
||||
)
|
||||
) {
|
||||
status.textContent = t('{provider} · PROVIDER CONTROLS', {
|
||||
provider: providerLabel(source.provider),
|
||||
});
|
||||
word(status, 'textContent', () =>
|
||||
t('{provider} · PROVIDER CONTROLS', {
|
||||
provider: providerLabel(source.provider),
|
||||
}),
|
||||
);
|
||||
}
|
||||
position();
|
||||
},
|
||||
@@ -1151,11 +1196,13 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
isCurrent: () => token === generation && record === current,
|
||||
onStatus: (phase) => {
|
||||
if (phase !== 'completed')
|
||||
status.textContent = t('YOUTUBE {state} · OPEN ORIGINAL', {
|
||||
state: YOUTUBE_PHASE_LABELS[phase]
|
||||
? t(YOUTUBE_PHASE_LABELS[phase])
|
||||
: phase.toUpperCase(),
|
||||
});
|
||||
word(status, 'textContent', () =>
|
||||
t('YOUTUBE {state} · OPEN ORIGINAL', {
|
||||
state: YOUTUBE_PHASE_LABELS[phase]
|
||||
? t(YOUTUBE_PHASE_LABELS[phase])
|
||||
: phase.toUpperCase(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1165,6 +1212,8 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
offLocale();
|
||||
worded.clear();
|
||||
hide({ immediate: true });
|
||||
for (const current of [...leavingRecords]) removeRecord(current);
|
||||
clearWarm();
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from './bhoteKoshiEmbeddedMedia.js';
|
||||
import { acquireImageryComparison } from '../maps/imageryComparison.js';
|
||||
import { createImagerySplit } from '../ui/imagerySplit.js';
|
||||
import { formatDate, t, tn, msg } from '../i18n/index.js';
|
||||
import { formatDate, t, tn, msg, onLocaleChange } from '../i18n/index.js';
|
||||
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
|
||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||
|
||||
@@ -1006,8 +1006,10 @@ export function createBhoteKoshiEventLayer({
|
||||
let _panelRefs = null;
|
||||
let _panelOwner = null;
|
||||
let _panelPublished = '';
|
||||
// The dates and field reports, which stay as they were when the panel opened.
|
||||
// The dates and field reports, which stay as they were when the panel
|
||||
// opened, until a locale change words them again.
|
||||
let _panelFixed = null;
|
||||
let _offPanelLocale = null;
|
||||
let _splitControl = null;
|
||||
let _beforeLayer = null;
|
||||
let _afterLayer = null;
|
||||
@@ -2604,9 +2606,9 @@ export function createBhoteKoshiEventLayer({
|
||||
id: 'bhote-koshi-split-line',
|
||||
handleClass: 'bhote-koshi-split-handle',
|
||||
cssProperty: '--bhote-koshi-split',
|
||||
beforeTitle: imageryEndpointText(event.imagery.before),
|
||||
afterTitle: imageryEndpointText(event.imagery.after),
|
||||
ariaLabel: t('Historical reference and post-event image divider'),
|
||||
beforeTitle: () => imageryEndpointText(event.imagery.before),
|
||||
afterTitle: () => imageryEndpointText(event.imagery.after),
|
||||
ariaLabel: () => t('Historical reference and post-event image divider'),
|
||||
formatValueText: (beforePercent, afterPercent) =>
|
||||
t(
|
||||
'A historical reference {before} percent, B post-event {after} percent',
|
||||
@@ -2666,6 +2668,19 @@ export function createBhoteKoshiEventLayer({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Word the panel again in the current locale: fresh records, written by
|
||||
* the same syncs that keep them current.
|
||||
*/
|
||||
function relocalizePanel() {
|
||||
if (!_panelRefs || !_event) return;
|
||||
_panelRefs = createPanelRecords(_event);
|
||||
_panelFixed = null;
|
||||
writeCinematicButton();
|
||||
syncPanel();
|
||||
syncPresentationMode();
|
||||
}
|
||||
|
||||
function syncCinematicButton() {
|
||||
if (writeCinematicButton()) publishPanel();
|
||||
}
|
||||
@@ -3517,6 +3532,8 @@ export function createBhoteKoshiEventLayer({
|
||||
),
|
||||
);
|
||||
publishPanel();
|
||||
_offPanelLocale?.();
|
||||
_offPanelLocale = onLocaleChange(relocalizePanel);
|
||||
syncPresentationMode();
|
||||
overlayHost.setVisible(
|
||||
BHOTE_KOSHI_OVERLAY_SOURCE_ID,
|
||||
@@ -3613,6 +3630,8 @@ export function createBhoteKoshiEventLayer({
|
||||
panelStore.connect(null);
|
||||
panelStore.publish(null);
|
||||
}
|
||||
_offPanelLocale?.();
|
||||
_offPanelLocale = null;
|
||||
_panelOwner = null;
|
||||
_panelRefs = null;
|
||||
_panelPublished = '';
|
||||
|
||||
Reference in new issue
Block a user