Skip to content

Commit ed301e1

Browse files
committed
feat(i18n): Integrate i18n across all components with enhancements
- Add i18n to ExternalFeaturesDiscoveryModal, WidgetBar, IFrame, ConfigurationGeneralView - Add en/zh translations for all new upstream components - Export getAvailableCockpitActions for dynamic translations - Resolve merge conflicts in VideoLibraryModal and WidgetHugger
1 parent cc58cbe commit ed301e1

121 files changed

Lines changed: 4818 additions & 2904 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/App.vue

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
<script setup lang="ts">
106106
import { useStorage } from '@vueuse/core'
107107
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'
108+
import { useI18n } from 'vue-i18n'
109+
import { useLocale } from 'vuetify'
108110
109111
import ArchitectureWarning from '@/components/ArchitectureWarning.vue'
110112
import CameraReplacementDialog from '@/components/CameraReplacementDialog.vue'
@@ -151,6 +153,30 @@ const missionStore = useMissionStore()
151153
// Initialize the snapshot store to register action callbacks
152154
useSnapshotStore()
153155
156+
// Sync Vuetify locale with vue-i18n
157+
const { locale: i18nLocale, t } = useI18n()
158+
const { current: vuetifyLocale } = useLocale()
159+
160+
// Map vue-i18n locales to Vuetify locales
161+
const localeMap: Record<string, string> = {
162+
en: 'en',
163+
zh: 'zhHans',
164+
}
165+
166+
// Watch for i18n locale changes and update Vuetify
167+
watch(
168+
i18nLocale,
169+
(newLocale) => {
170+
vuetifyLocale.value = localeMap[newLocale] || 'en'
171+
172+
// Update Electron menu language if running in Electron
173+
if (window.electronAPI?.updateMenuLanguage) {
174+
window.electronAPI.updateMenuLanguage(newLocale)
175+
}
176+
},
177+
{ immediate: true }
178+
)
179+
154180
const showAboutDialog = ref(false)
155181
const currentSubMenuComponent = ref<SubMenuComponent>(null)
156182
@@ -248,7 +274,7 @@ watch(
248274
(isOnline) => {
249275
if (!isOnline) {
250276
openSnackbar({
251-
message: 'Vehicle connection lost: reestablishing',
277+
message: t('errors.vehicleConnectionLost'),
252278
variant: 'error',
253279
duration: 3000,
254280
closeButton: false,
@@ -259,7 +285,7 @@ watch(
259285
return
260286
}
261287
262-
openSnackbar({ message: 'Vehicle connected', variant: 'success', duration: 3000, closeButton: false })
288+
openSnackbar({ message: t('errors.vehicleConnected'), variant: 'success', duration: 3000, closeButton: false })
263289
connectionStatusFeedback.value = { border: '3px solid green' }
264290
265291
resetConnectionStatusFeedback()

src/components/About.vue

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@
1616
<div class="w-[90%] flex justify-between my-6 py-3">
1717
<div class="w-[45%] flex flex-col text-start">
1818
<p class="mb-1">
19-
Cockpit is an intuitive and customizable cross-platform ground control station for remote vehicles of
20-
all types.
19+
{{ $t('about.description1') }}
2120
</p>
22-
<p class="my-3">It was created by Blue Robotics and is entirely open-source.</p>
21+
<p class="my-3">{{ $t('about.description2') }}</p>
2322
<p class="mt-1">
24-
It currently supports Ardupilot-based vehicles, but has plans to support any generic vehicle, be it
25-
communicating MAVLink or not.
23+
{{ $t('about.description3') }}
2624
</p>
2725
</div>
2826
<div class="w-[45%] flex flex-col justify-end text-end">
2927
<p class="mb-1">
30-
Version
28+
{{ $t('about.version') }}
3129
<a :href="app_version.link" target="_blank" class="text-primary hover:underline">
3230
{{ app_version.version }}
3331
</a>
3432
<br />
35-
<span class="text-sm text-gray-500">Released: {{ app_version.date }}</span>
33+
<span class="text-sm text-gray-500">{{ $t('about.released') }}: {{ app_version.date }}</span>
3634
</p>
37-
<p class="my-3">Created by Blue Robotics</p>
38-
<p class="mt-1">Licensed under AGPL-3.0-only or LicenseRef-Cockpit-Custom</p>
35+
<p class="my-3">{{ $t('about.createdBy') }}</p>
36+
<p class="mt-1">{{ $t('about.license') }}</p>
3937
</div>
4038
</div>
4139
<div class="mb-5 flex justify-center align-center">
@@ -67,7 +65,9 @@
6765
</div>
6866
</template>
6967
<template #actions
70-
><div class="flex w-full justify-end"><v-btn @click="closeDialog">Close</v-btn></div></template
68+
><div class="flex w-full justify-end">
69+
<v-btn @click="closeDialog">{{ $t('common.close') }}</v-btn>
70+
</div></template
7171
>
7272
</InteractionDialog>
7373
</teleport>

src/components/ArchitectureWarning.vue

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<InteractionDialog
33
v-model="showArchWarningDialog"
4-
title="Performance Warning"
4+
:title="$t('components.ArchitectureWarning.title')"
55
variant="text-only"
66
:actions="dialogActions"
77
max-width="820"
@@ -10,16 +10,14 @@
1010
<div class="flex items-center justify-center mb-2">
1111
<v-icon class="text-yellow text-[60px] mx-8">mdi-alert-rhombus</v-icon>
1212
<div class="flex flex-col font-medium gap-y-3 w-full">
13-
You are running the x64 version of Cockpit on an Apple Silicon Mac (M series), which causes severely degraded
14-
performance, including:
13+
{{ $t('components.ArchitectureWarning.runningWrongVersion') }}
1514
<ul class="mt- ml-4">
16-
<li>- 3-4x slower application startup times</li>
17-
<li>- 2x the memory usage</li>
18-
<li>- Reduced overall performance</li>
15+
<li>- {{ $t('components.ArchitectureWarning.slowerStartup') }}</li>
16+
<li>- {{ $t('components.ArchitectureWarning.doubleMemory') }}</li>
17+
<li>- {{ $t('components.ArchitectureWarning.reducedPerformance') }}</li>
1918
</ul>
2019
<p class="text-sm text-gray-600 mt-2">
21-
This warning cannot be disabled - we strongly recommend that you download and install the intended version
22-
for your system.
20+
{{ $t('components.ArchitectureWarning.cannotDisable') }}
2321
</p>
2422
</div>
2523
</div>
@@ -29,22 +27,24 @@
2927

3028
<script setup lang="ts">
3129
import { onBeforeMount, ref } from 'vue'
30+
import { useI18n } from 'vue-i18n'
3231
3332
import InteractionDialog, { type Action } from '@/components/InteractionDialog.vue'
3433
import { isElectron } from '@/libs/utils'
3534
import { PlatformUtils } from '@/types/platform'
3635
36+
const { t } = useI18n()
3737
const showArchWarningDialog = ref(false)
3838
39-
const dialogActions = [
39+
const dialogActions: Action[] = [
4040
{
41-
text: 'Dismiss',
41+
text: t('components.ArchitectureWarning.dismiss'),
4242
action: () => {
4343
showArchWarningDialog.value = false
4444
},
4545
},
4646
{
47-
text: 'Download ARM64 Version',
47+
text: t('components.ArchitectureWarning.downloadARM'),
4848
action: () => {
4949
window.open('https://github.com/bluerobotics/cockpit/releases/', '_blank')
5050
showArchWarningDialog.value = false

src/components/ArmSafetyDialog.vue

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<template>
2-
<InteractionDialog v-model="show" title="Be careful" variant="text-only" max-width="780px" :persistent="false">
2+
<InteractionDialog
3+
v-model="show"
4+
:title="$t('components.ArmSafetyDialog.title')"
5+
variant="text-only"
6+
max-width="780px"
7+
:persistent="false"
8+
>
39
<template #content>
410
<div class="flex gap-x-2 absolute top-0 right-0 py-2 pr-3">
511
<slot name="help-icon"></slot>
@@ -11,34 +17,42 @@
1117
<div class="flex items-center justify-center mb-6">
1218
<v-icon class="text-yellow text-[60px] mx-8">mdi-alert-rhombus</v-icon>
1319
<p class="w-[560px] text-balance">
14-
The vehicle is currently armed, and the main-menu contains configurations and tools that can cause unsafe
15-
situations.
20+
{{ $t('components.ArmSafetyDialog.vehicleArmedWarning') }}
1621
</p>
17-
<p class="w-[560px] text-balance">Come back later, or proceed carefully with one of the following options:</p>
22+
<p class="w-[560px] text-balance">{{ $t('components.ArmSafetyDialog.proceedCarefully') }}</p>
1823
</div>
1924
</template>
2025
<template #actions>
2126
<div class="flex items-center justify-between gap-8 w-full text-md">
22-
<button class="option-button" @click="neverAskAgain">Continue and never warn again</button>
27+
<button class="option-button" @click="neverAskAgain">
28+
{{ $t('components.ArmSafetyDialog.continueNeverWarn') }}
29+
</button>
2330
<button class="option-button" @click="doNotAskAgainInThisSession">
24-
Continue and don't warn again during this session
31+
{{ $t('components.ArmSafetyDialog.continueSessionWarn') }}
32+
</button>
33+
<button class="option-button" @click="continueAnyway">
34+
{{ $t('components.ArmSafetyDialog.continueAnyway') }}
35+
</button>
36+
<button class="option-button" @click="disarmVehicle">
37+
{{ $t('components.ArmSafetyDialog.disarmAndContinue') }}
2538
</button>
26-
<button class="option-button" @click="continueAnyway">Continue anyway</button>
27-
<button class="option-button" @click="disarmVehicle">Disarm vehicle and continue</button>
2839
</div>
2940
</template>
3041
</InteractionDialog>
3142
</template>
3243

3344
<script setup lang="ts">
3445
import { ref } from 'vue'
46+
import { useI18n } from 'vue-i18n'
3547
3648
import InteractionDialog from '@/components/InteractionDialog.vue'
3749
import { useSnackbar } from '@/composables/snackbar'
3850
import { useAlertStore } from '@/stores/alert'
3951
import { useAppInterfaceStore } from '@/stores/appInterface'
4052
import { useMainVehicleStore } from '@/stores/mainVehicle'
4153
54+
const { t } = useI18n()
55+
4256
const vehicleStore = useMainVehicleStore()
4357
const alertStore = useAlertStore()
4458
const interfaceStore = useAppInterfaceStore()
@@ -65,7 +79,7 @@ const neverAskAgain = (): void => {
6579
continueAnyway()
6680
6781
openSnackbar({
68-
message: 'Armed menu warning disabled. You can re-enable it in the Settings > Alerts menu.',
82+
message: t('components.ArmSafetyDialog.warningDisabled'),
6983
variant: 'info',
7084
duration: 10000,
7185
closeButton: true,

src/components/CameraReplacementDialog.vue

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<InteractionDialog
33
:show-dialog="showDialog"
4-
title="Camera stream change detected"
4+
:title="$t('components.CameraReplacementDialog.title')"
55
:actions="dialogActions"
66
variant="text-only"
77
max-width="520px"
@@ -10,8 +10,7 @@
1010
<template #content>
1111
<div class="flex flex-col gap-4 min-w-[340px]">
1212
<p class="text-sm text-gray-300">
13-
We noticed that a stream used by your widgets is no longer available, but a new stream has appeared. This
14-
usually happens when replacing a camera. Would you like to update your widgets to use the new stream?
13+
{{ $t('components.CameraReplacementDialog.description') }}
1514
</p>
1615

1716
<div v-for="orphan in orphanedWidgetStreams" :key="orphan.externalId" class="flex flex-col gap-3">
@@ -20,20 +19,20 @@
2019
<v-icon size="18" color="red-lighten-1">mdi-video-off</v-icon>
2120
<span class="font-medium text-white text-sm">{{ orphan.internalName }}</span>
2221
<v-chip size="x-small" color="red-darken-1" variant="flat" label class="text-white ml-auto">
23-
Unavailable
22+
{{ $t('components.CameraReplacementDialog.unavailable') }}
2423
</v-chip>
2524
</div>
2625
<div class="stream-card-details">
2726
<div class="detail-row">
28-
<span class="detail-label">Source</span>
27+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.source') }}</span>
2928
<span class="detail-value">{{ orphan.displayInfo.source }}</span>
3029
</div>
3130
<div class="detail-row">
32-
<span class="detail-label">Stream ID</span>
31+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.streamId') }}</span>
3332
<span class="detail-value text-xs">{{ orphan.externalId }}</span>
3433
</div>
3534
<div class="detail-row">
36-
<span class="detail-label">Type</span>
35+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.type') }}</span>
3736
<span class="detail-value">
3837
<v-chip
3938
size="x-small"
@@ -47,7 +46,7 @@
4746
</span>
4847
</div>
4948
<div v-if="orphan.displayInfo.resolution !== 'Unknown'" class="detail-row">
50-
<span class="detail-label">Resolution</span>
49+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.resolution') }}</span>
5150
<span class="detail-value">
5251
{{ orphan.displayInfo.resolution }}
5352
<template v-if="orphan.displayInfo.fps"> @ {{ orphan.displayInfo.fps }}</template>
@@ -69,7 +68,7 @@
6968
density="compact"
7069
variant="outlined"
7170
hide-details
72-
label="Replace with"
71+
:label="$t('components.CameraReplacementDialog.replaceWith')"
7372
/>
7473
</div>
7574
<template v-else>
@@ -78,20 +77,20 @@
7877
<v-icon size="18" color="green-lighten-1">mdi-video</v-icon>
7978
<span class="font-medium text-white text-sm">{{ corr.name }}</span>
8079
<v-chip size="x-small" color="green-darken-1" variant="flat" label class="text-white ml-auto">
81-
Available
80+
{{ $t('components.CameraReplacementDialog.available') }}
8281
</v-chip>
8382
</div>
8483
<div class="stream-card-details">
8584
<div class="detail-row">
86-
<span class="detail-label">Source</span>
85+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.source') }}</span>
8786
<span class="detail-value">{{ videoStore.getStreamDisplayInfo(corr.externalId).source }}</span>
8887
</div>
8988
<div class="detail-row">
90-
<span class="detail-label">Stream ID</span>
89+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.streamId') }}</span>
9190
<span class="detail-value text-xs">{{ corr.externalId }}</span>
9291
</div>
9392
<div class="detail-row">
94-
<span class="detail-label">Type</span>
93+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.type') }}</span>
9594
<span class="detail-value">
9695
<v-chip
9796
size="x-small"
@@ -109,7 +108,7 @@
109108
</span>
110109
</div>
111110
<div v-if="videoStore.getStreamDisplayInfo(corr.externalId).resolution !== '...'" class="detail-row">
112-
<span class="detail-label">Resolution</span>
111+
<span class="detail-label">{{ $t('components.CameraReplacementDialog.resolution') }}</span>
113112
<span class="detail-value">
114113
{{ videoStore.getStreamDisplayInfo(corr.externalId).resolution }}
115114
<template v-if="videoStore.getStreamDisplayInfo(corr.externalId).fps">
@@ -123,7 +122,7 @@
123122
</div>
124123

125124
<p class="text-xs text-gray-500 mt-1">
126-
{{ affectedWidgetCount }} widget{{ affectedWidgetCount === 1 ? '' : 's' }} will be updated.
125+
{{ $t('components.CameraReplacementDialog.widgetsWillBeUpdated', { count: affectedWidgetCount }) }}
127126
</p>
128127
</div>
129128
</template>
@@ -132,6 +131,7 @@
132131

133132
<script setup lang="ts">
134133
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
134+
import { useI18n } from 'vue-i18n'
135135
136136
import { useBlueOsStorage } from '@/composables/settingsSyncer'
137137
import { useVideoStore } from '@/stores/video'
@@ -142,6 +142,7 @@ import { MiniWidgetType, WidgetType } from '@/types/widgets'
142142
143143
import InteractionDialog, { type Action } from './InteractionDialog.vue'
144144
145+
const { t } = useI18n()
145146
const videoStore = useVideoStore()
146147
const widgetStore = useWidgetManagerStore()
147148
@@ -341,14 +342,14 @@ const handleDialogClose = (value: boolean): void => {
341342
342343
const dialogActions = computed((): Action[] => [
343344
{
344-
text: 'Dismiss',
345+
text: t('components.CameraReplacementDialog.dismiss'),
345346
action: () => {
346347
markDismissed()
347348
showDialog.value = false
348349
},
349350
},
350351
{
351-
text: 'Replace stream',
352+
text: t('components.CameraReplacementDialog.replaceStream'),
352353
color: 'white',
353354
action: replaceStreams,
354355
},

0 commit comments

Comments
 (0)