i18n: translate the application views with t() as they render

Every data-i18n, data-i18n-title, data-i18n-aria-label and
data-i18n-placeholder marker in the Vue templates becomes a t() call with
the key the boot pass used to look up, contexts included. The three labels
the shell rewrote at boot (Visual Presets, Location and the standby
description) render through t() too, and that rewrite is gone. With the
locale in vue-i18n, view text now follows a locale change, and markup
under v-if or v-for no longer comes back in English when it re-renders.

Rendered text and attributes are unchanged in every locale. The boot pass
still translates static page markup such as the loading screen and a host's
own markup, and npm run i18n rejects data-i18n* in a Vue template.
This commit is contained in:
Sameh Khamis committed 2026-11-01 15:38:23 -08:00
1 parent 2c72a038d5
commit ba20bc13d8
33 files changed
+377 -344

No files matched your search

+12 -12
View File
@@ -36,12 +36,12 @@ const radioWords = {
</script>
<template>
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" aria-label="Internet radio and local SDR" data-i18n-aria-label :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" :aria-label="t('Internet radio and local SDR')" :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
<template #badge><span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span></template>
<div class="radio-directory-row">
<UiButton id="radio-enable-btn" @click="act('toggleRadio', 'panel')" class="panel-layer-toggle" :pressed="radio?.enable.active ?? false" pressed-class="active" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</UiButton>
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
<UiSelect class="gev-field" id="radio-filter" @change="setFilter($event.target)" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" :model-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
<label class="radio-filter-label" for="radio-filter">{{ t('STATION TAG') }}</label>
<UiSelect class="gev-field" id="radio-filter" @change="setFilter($event.target)" :aria-label="t('Filter stations by station tag')" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" :model-value="radio ? radio.filter.value : 'all'">
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
</UiSelect>
</div>
@@ -51,23 +51,23 @@ const radioWords = {
<span id="radio-station-tags">{{ radio?.station.tags ?? '' }}</span>
</div>
<RadioTuner />
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label>
<button id="radio-prev-btn" @click="act('cycleRadio', -1)" type="button" aria-label="Previous filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>PREV</button>
<div class="radio-transport" role="group" :aria-label="t('Radio playback')">
<button id="radio-prev-btn" @click="act('cycleRadio', -1)" type="button" :aria-label="t('Previous filtered station')" :disabled="radio ? radio.transport.stepDisabled : true">{{ t('PREV') }}</button>
<button id="radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected station')" :disabled="radio ? radio.transport.play.disabled : true">{{ radio ? radio.transport.play.text : t('PLAY') }}</button>
<button id="radio-next-btn" @click="act('cycleRadio', 1)" type="button" aria-label="Next filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>NEXT</button>
<button id="radio-stop-btn" @click="act('stopRadio')" type="button" aria-label="Stop radio playback" :disabled="radio ? radio.transport.stopDisabled : true" data-i18n-aria-label data-i18n>STOP</button>
<button id="radio-next-btn" @click="act('cycleRadio', 1)" type="button" :aria-label="t('Next filtered station')" :disabled="radio ? radio.transport.stepDisabled : true">{{ t('NEXT') }}</button>
<button id="radio-stop-btn" @click="act('stopRadio')" type="button" :aria-label="t('Stop radio playback')" :disabled="radio ? radio.transport.stopDisabled : true">{{ t('STOP') }}</button>
</div>
<label class="radio-volume-row" for="radio-volume">
<span data-i18n>VOLUME</span>
<UiSlider id="radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" aria-label="Radio volume" :disabled="radio?.volume.disabled" :model-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
<span>{{ t('VOLUME') }}</span>
<UiSlider id="radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" :aria-label="t('Radio volume')" :disabled="radio?.volume.disabled" :model-value="radio ? radio.volume.value : 80" />
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
</label>
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
<div class="radio-links">
<a id="radio-station-homepage" :href="radio ? radio.station.homepage || undefined : '#'" target="_blank" rel="noopener noreferrer" :hidden="radio ? !radio.station.homepage : true" data-i18n>STATION SITE</a>
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer" data-i18n>DIRECTORY: RADIO BROWSER</a>
<a id="radio-station-homepage" :href="radio ? radio.station.homepage || undefined : '#'" target="_blank" rel="noopener noreferrer" :hidden="radio ? !radio.station.homepage : true">{{ t('STATION SITE') }}</a>
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer">{{ t('DIRECTORY: RADIO BROWSER') }}</a>
</div>
<p class="radio-privacy" data-i18n>Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.</p>
<p class="radio-privacy">{{ t('Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.') }}</p>
<SdrCard />
</RailPanel>
</template>