mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.
- Simplify src/i18n: drop namespaces, the configurable locale pair
(GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
and markup, and fails on missing, unused, placeholder-mismatched or
wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
merge resurrected in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
189 lines
6.3 KiB
JavaScript
189 lines
6.3 KiB
JavaScript
import { locationMiniStatus } from '../locationStatus.js';
|
|
import { t } from '../i18n/index.js';
|
|
|
|
const POI_KEYS = ['Q', 'W', 'E', 'R', 'T'];
|
|
|
|
/**
|
|
* locationStatus.js sentinel outputs used as localized-rewrite triggers for
|
|
* the collapsed LOCATION mini-status. Built by calling the module with no
|
|
* real data, so the comparisons track that module's copy without restating
|
|
* English strings here. Resolved destinations (place names) stay verbatim.
|
|
*/
|
|
const LOCATION_MINI_EMPTY = Object.freeze(locationMiniStatus());
|
|
const LOCATION_MINI_SEARCHED_PLACEHOLDER = Object.freeze(
|
|
locationMiniStatus({ searchedLabel: '·' }),
|
|
);
|
|
|
|
/** Location DOM, keyboard handling and pending row animation over supplied actions. */
|
|
export class LocationControls {
|
|
constructor({
|
|
elements,
|
|
cities,
|
|
getExpandedCity,
|
|
onCity,
|
|
onPoi,
|
|
onSearch,
|
|
onReset,
|
|
doc = document,
|
|
requestFrame = (callback) => requestAnimationFrame(callback),
|
|
cancelFrame = (id) => cancelAnimationFrame(id),
|
|
}) {
|
|
Object.assign(this, {
|
|
elements,
|
|
cities,
|
|
getExpandedCity,
|
|
onCity,
|
|
onPoi,
|
|
onSearch,
|
|
onReset,
|
|
doc,
|
|
requestFrame,
|
|
cancelFrame,
|
|
});
|
|
this.removers = [];
|
|
this.poiRemovers = [];
|
|
this.frame = null;
|
|
this.destroyed = false;
|
|
this.rowGeneration = 0;
|
|
elements.pills.replaceChildren();
|
|
for (const [id, city] of Object.entries(cities)) {
|
|
const pill = doc.createElement('button');
|
|
pill.type = 'button';
|
|
pill.className = 'location-pill';
|
|
pill.dataset.locationId = id;
|
|
pill.textContent = city.name;
|
|
this.bind(pill, 'click', () => onCity(id));
|
|
elements.pills.appendChild(pill);
|
|
}
|
|
this.bind(doc, 'keydown', (event) => {
|
|
const cityId = getExpandedCity();
|
|
if (
|
|
!cityId ||
|
|
event.target?.matches?.('select, input, textarea') ||
|
|
event.target === elements.search
|
|
)
|
|
return;
|
|
const index = POI_KEYS.indexOf(event.key.toUpperCase());
|
|
if (index !== -1 && index < (cities[cityId]?.pois.length || 0))
|
|
onPoi(cityId, index);
|
|
});
|
|
this.bind(elements.searchToggle, 'click', () => {
|
|
elements.search.classList.toggle('expanded');
|
|
if (elements.search.classList.contains('expanded'))
|
|
elements.search.focus();
|
|
});
|
|
this.bind(elements.search, 'keydown', (event) => {
|
|
if (event.key === 'Enter') void onSearch(elements.search.value);
|
|
});
|
|
for (const button of elements.resetButtons)
|
|
this.bind(button, 'click', onReset);
|
|
}
|
|
bind(element, event, handler, removers = this.removers) {
|
|
if (!element) return;
|
|
const listener = (...args) => {
|
|
if (!this.destroyed) return handler(...args);
|
|
};
|
|
element.addEventListener(event, listener);
|
|
removers.push(() => element.removeEventListener(event, listener));
|
|
}
|
|
cancelExpansion() {
|
|
this.rowGeneration++;
|
|
if (this.frame !== null) this.cancelFrame(this.frame);
|
|
this.frame = null;
|
|
}
|
|
showPois(cityId) {
|
|
if (this.destroyed) return;
|
|
const city = this.cities[cityId];
|
|
if (!city) return;
|
|
this.cancelExpansion();
|
|
const generation = this.rowGeneration;
|
|
for (const remove of this.poiRemovers.splice(0)) remove();
|
|
this.elements.poiRow.replaceChildren();
|
|
city.pois.forEach((poi, index) => {
|
|
const pill = this.doc.createElement('button');
|
|
pill.type = 'button';
|
|
pill.className = 'poi-pill';
|
|
pill.dataset.poiIndex = index;
|
|
const key = this.doc.createElement('span');
|
|
key.className = 'poi-pill-key';
|
|
key.textContent = POI_KEYS[index] || index + 1;
|
|
const label = this.doc.createElement('span');
|
|
label.className = 'poi-pill-name';
|
|
label.textContent = poi.name;
|
|
pill.append(key, label);
|
|
this.bind(
|
|
pill,
|
|
'click',
|
|
() => this.onPoi(cityId, index),
|
|
this.poiRemovers,
|
|
);
|
|
this.elements.poiRow.appendChild(pill);
|
|
});
|
|
this.frame = this.requestFrame(() => {
|
|
if (this.destroyed || generation !== this.rowGeneration) return;
|
|
this.frame = null;
|
|
this.elements.poiRow.classList.add('expanded');
|
|
this.elements.divider.classList.add('visible');
|
|
});
|
|
}
|
|
hidePois() {
|
|
if (this.destroyed) return;
|
|
this.cancelExpansion();
|
|
this.elements.poiRow.classList.remove('expanded');
|
|
this.elements.divider.classList.remove('visible');
|
|
}
|
|
highlightPoi(index) {
|
|
if (this.destroyed) return;
|
|
for (const pill of this.elements.poiRow.querySelectorAll('.poi-pill'))
|
|
pill.classList.toggle('active', Number(pill.dataset.poiIndex) === index);
|
|
}
|
|
highlightCity(id) {
|
|
if (this.destroyed) return;
|
|
for (const pill of this.elements.pills.querySelectorAll('.location-pill'))
|
|
pill.classList.toggle('active', pill.dataset.locationId === id);
|
|
}
|
|
renderStatus(state) {
|
|
if (this.destroyed || !this.elements.statusCity || !this.elements.statusPoi)
|
|
return;
|
|
const lines = locationMiniStatus(state);
|
|
// The initial/placeholder states get catalog keys at the writer so a
|
|
// non-English boot never leaks the English sentinels back over the
|
|
// localized static readout (resolved place names stay verbatim data).
|
|
this.elements.statusCity.textContent =
|
|
lines.city === LOCATION_MINI_EMPTY.city
|
|
? t('📍 Location: --')
|
|
: lines.city;
|
|
if (lines.poi === LOCATION_MINI_EMPTY.poi) {
|
|
this.elements.statusPoi.textContent = t('Landmark: --');
|
|
} else if (lines.poi === LOCATION_MINI_SEARCHED_PLACEHOLDER.poi) {
|
|
this.elements.statusPoi.textContent = t('Searched location');
|
|
} else {
|
|
this.elements.statusPoi.textContent = lines.poi;
|
|
}
|
|
}
|
|
createOrbitIndicator() {
|
|
if (this.destroyed) return null;
|
|
if (!this.orbitIndicator) {
|
|
this.orbitIndicator = this.doc.createElement('div');
|
|
this.orbitIndicator.id = 'orbit-indicator';
|
|
const icon = this.doc.createElement('span');
|
|
icon.className = 'orbit-icon';
|
|
icon.textContent = '↻';
|
|
this.orbitIndicator.append(icon, ` ${t('ORBIT')}`);
|
|
this.doc.body.appendChild(this.orbitIndicator);
|
|
}
|
|
return this.orbitIndicator;
|
|
}
|
|
destroy() {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
this.cancelExpansion();
|
|
for (const remove of [
|
|
...this.poiRemovers.splice(0),
|
|
...this.removers.splice(0),
|
|
])
|
|
remove();
|
|
this.orbitIndicator?.remove();
|
|
}
|
|
}
|