Skip to content

Commit af729b0

Browse files
committed
feat: Add Electron menu translations and expand i18n coverage
- Add Electron menu translation service with dynamic language switching - Move language selector from MainMenu to General settings - Translate all configuration views (Actions, Alerts, Development, General, Joystick, Logs, MAVLink, Mission, UI, Video) - Translate mission planning, data lake, and tools views - Translate all mini-widgets and major components - Add glass effect styling for language selector dropdown - Integrate Electron menu language sync with i18n system - Expand translation keys in en.json and zh.json for comprehensive UI coverage Changes: - 56 component/view files translated - 2 locale files updated with 500+ translation keys - Electron menu fully integrated with i18n - Language persisted to localStorage - Menu language updates automatically on app language change
1 parent 66ad3a7 commit af729b0

57 files changed

Lines changed: 2281 additions & 656 deletions

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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@
149149
<script setup lang="ts">
150150
import { useStorage, useWindowSize } from '@vueuse/core'
151151
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'
152+
import { useI18n } from 'vue-i18n'
153+
import { useLocale } from 'vuetify'
152154
153155
import ActionDiscoveryModal from '@/components/ActionDiscoveryModal.vue'
154156
import ArchitectureWarning from '@/components/ArchitectureWarning.vue'
@@ -194,6 +196,26 @@ const missionStore = useMissionStore()
194196
// Initialize the snapshot store to register action callbacks
195197
useSnapshotStore()
196198
199+
// Sync Vuetify locale with vue-i18n
200+
const { locale: i18nLocale } = useI18n()
201+
const { current: vuetifyLocale } = useLocale()
202+
203+
// Map vue-i18n locales to Vuetify locales
204+
const localeMap: Record<string, string> = {
205+
'en': 'en',
206+
'zh': 'zhHans',
207+
}
208+
209+
// Watch for i18n locale changes and update Vuetify
210+
watch(i18nLocale, (newLocale) => {
211+
vuetifyLocale.value = localeMap[newLocale] || 'en'
212+
213+
// Update Electron menu language if running in Electron
214+
if (window.electronAPI?.updateMenuLanguage) {
215+
window.electronAPI.updateMenuLanguage(newLocale)
216+
}
217+
}, { immediate: true })
218+
197219
const showAboutDialog = ref(false)
198220
const currentSubMenuComponent = ref<SubMenuComponent>(null)
199221

src/components/About.vue

Lines changed: 9 additions & 9 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">
@@ -75,13 +73,15 @@
7573

7674
<script setup lang="ts">
7775
import { onUnmounted, ref, watch } from 'vue'
76+
import { useI18n } from 'vue-i18n'
7877
7978
import CockpitLogo from '@/assets/cockpit-logo.png'
8079
import lite from '@/assets/lite.png'
8180
import InteractionDialog from '@/components/InteractionDialog.vue'
8281
import { app_version } from '@/libs/cosmos'
8382
import { isElectron } from '@/libs/utils'
8483
84+
const { t } = useI18n()
8585
const showDialog = ref(true)
8686
const emit = defineEmits(['update:showAboutDialog'])
8787

src/components/EditMenu.vue

Lines changed: 120 additions & 41 deletions
Large diffs are not rendered by default.
Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
<template>
2-
<div class="language-switcher">
3-
<v-select
4-
v-model="currentLocale"
5-
:items="languages"
6-
item-title="label"
7-
item-value="value"
8-
density="compact"
9-
variant="outlined"
10-
hide-details
11-
@update:model-value="changeLanguage"
12-
>
13-
<template #prepend-inner>
14-
<v-icon size="small">mdi-translate</v-icon>
15-
</template>
16-
</v-select>
17-
</div>
2+
<GlassButton
3+
:label="simplified ? '' : languageLabel"
4+
:label-class="[labelSize, '-mb-0.5 mt-6']"
5+
:icon="simplified ? 'mdi-translate' : undefined"
6+
:icon-size="simplified ? 25 : undefined"
7+
variant="uncontained"
8+
:tooltip="simplified ? languageLabel : undefined"
9+
:width="buttonSize"
10+
@click="toggleLanguage"
11+
>
12+
<v-icon v-if="!simplified" size="20" class="mr-2">mdi-translate</v-icon>
13+
</GlassButton>
1814
</template>
1915

2016
<script setup lang="ts">
21-
import { ref, onMounted } from 'vue'
17+
import { computed } from 'vue'
2218
import { useI18n } from 'vue-i18n'
19+
import { useLocale } from 'vuetify'
20+
import GlassButton from './GlassButton.vue'
21+
22+
interface Props {
23+
simplified?: boolean
24+
buttonSize?: string
25+
labelSize?: string
26+
}
27+
28+
const props = defineProps<Props>()
2329
2430
const { locale } = useI18n()
25-
const currentLocale = ref(locale.value)
31+
const { current: vuetifyLocale } = useLocale()
2632
27-
const languages = [
28-
{ label: 'English', value: 'en' },
29-
{ label: '中文', value: 'zh' },
30-
]
33+
const languageLabel = computed(() => {
34+
return locale.value === 'zh' ? '中文' : 'English'
35+
})
3136
32-
const changeLanguage = (newLocale: string) => {
37+
const toggleLanguage = () => {
38+
const newLocale = locale.value === 'zh' ? 'en' : 'zh'
3339
locale.value = newLocale
34-
currentLocale.value = newLocale
40+
41+
// Update Vuetify locale
42+
vuetifyLocale.value = newLocale === 'zh' ? 'zhHans' : 'en'
43+
44+
// Save to localStorage
3545
localStorage.setItem('cockpit-language', newLocale)
3646
3747
// Update Electron menu language if running in Electron
3848
if (window.electronAPI?.updateMenuLanguage) {
3949
window.electronAPI.updateMenuLanguage(newLocale)
4050
}
4151
}
42-
43-
onMounted(() => {
44-
currentLocale.value = locale.value
45-
})
4652
</script>
47-
48-
<style scoped>
49-
.language-switcher {
50-
min-width: 120px;
51-
}
52-
</style>

src/components/MainMenu.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@
128128
alt="Fullscreen Icon"
129129
/>
130130
</GlassButton>
131-
<LanguageSwitcher :simplified="simplifiedMainMenu" :button-size="buttonSize" :label-size="menuLabelSize" />
132131
<GlassButton
133132
:label="simplifiedMainMenu ? '' : $t('menu.about')"
134133
:label-class="[menuLabelSize, '-mb-0.5 mt-6']"
@@ -218,7 +217,6 @@ import MissionPlanningIcon from '@/assets/icons/mission-planning.svg'
218217
import SettingsIcon from '@/assets/icons/settings.svg'
219218
import ToolsIcon from '@/assets/icons/tools.svg'
220219
import GlassButton from '@/components/GlassButton.vue'
221-
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
222220
import {
223221
availableCockpitActions,
224222
registerActionCallback,

src/components/SplashScreen.vue

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -131,55 +131,29 @@ const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
131131
const isDecember = (): boolean => getMonth(new Date()) === 11
132132
133133
import { useFullscreen } from '@vueuse/core'
134-
import { onBeforeUnmount, onMounted, ref } from 'vue'
134+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
135+
import { useI18n } from 'vue-i18n'
135136
136137
import { isElectron } from '@/libs/utils'
137138
139+
const { tm } = useI18n()
140+
138141
const randomLightHeartedMessage = ref<string>('')
139142
let timerId: ReturnType<typeof setTimeout>
140143
141-
const startupLightHeartedMessages: string[] = [
142-
'Distributing dolphins for sonar translations...',
143-
'Observing octopuses to optimize dark mode...',
144-
'Persuading Poseidon to trade us his trident...',
145-
'Sailing the seas, in sync with the breeze...',
146-
'Jiggling jellyfish to frost up the UI...',
147-
'Corralling coral for calibration...',
148-
'Languishing in life-jackets...',
149-
'Salvaging shipwrecks...',
150-
'Searching for Nemo...',
151-
'Singing with whales...',
152-
'Tuning harps for carp...',
153-
'Stargazing with starfish...',
154-
'Recharging electric eels...',
155-
'Swaying at the seaweed disco...',
156-
'Fencing in the swordfish showdown...',
157-
'Assembling AUVs into a single-file line...',
158-
'Polishing portholes for crystal-clear viewports...',
159-
'Convincing crabs to stop double-clicking everything...',
160-
'Syncing compass with the stars (hold still, Orion)...',
161-
'Kowtowing to kelp for a greener UI theme...',
162-
'Warming up thrusters — and the coffee machine...',
163-
'Updating barnacle firmware — this might tickle...',
164-
'Deploying rubber ducks for safety certification...',
165-
'Filling ballast tanks with fresh ideas...',
166-
'Mapping ocean puns… depth-level humor detected...',
167-
'Rendering waves pixel by pixel — surf’s almost up...',
168-
'Teaching seagulls the latest hover gestures...',
169-
'Checking tide tables to schedule snack breaks...',
170-
'Swapping batteries in the sea turtles (just kidding)...',
171-
'Dusting off code gremlins — please keep arms inside the Cockpit...',
172-
'Aligning gyros — because spin is only fun on dance floors...',
173-
]
174-
175-
const remainingMessages = ref<string[]>([...startupLightHeartedMessages])
144+
const getStartupMessages = (): string[] => {
145+
const messages = tm('splash.messages')
146+
return Array.isArray(messages) ? messages : []
147+
}
148+
149+
const remainingMessages = ref<string[]>([...getStartupMessages()])
176150
177151
const scheduleNextMessage = (): void => {
178152
const randomIndex = Math.floor(Math.random() * remainingMessages.value.length)
179153
const delay = Math.random() * 5000 + 3000
180154
181155
if (remainingMessages.value.length === 0) {
182-
remainingMessages.value = [...startupLightHeartedMessages]
156+
remainingMessages.value = [...getStartupMessages()]
183157
}
184158
randomLightHeartedMessage.value = remainingMessages.value.splice(randomIndex, 1)[0]
185159
timerId = setTimeout(scheduleNextMessage, delay)

src/components/TransformingFunctionDialog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ const isValidForm = computed(() => {
211211
212212
const saveTransformingFunction = (): void => {
213213
if (!isValidForm.value) {
214-
openSnackbar({ message: 'Please fill in all fields', variant: 'error' })
214+
openSnackbar({ message: t('errors.pleaseFillAllFields'), variant: 'error' })
215215
return
216216
}
217217

src/components/VehicleDiscoveryDialog.vue

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
<template>
22
<InteractionDialog
33
v-model="isOpen"
4-
:title="searching ? 'Searching for vehicles...' : 'Vehicle Discovery'"
4+
:title="searching ? $t('vehicleDiscovery.searching') : $t('vehicleDiscovery.title')"
55
:actions="dialogActions"
66
:persistent="searching"
77
:variant="'text-only'"
88
>
99
<template #content>
1010
<div v-if="props.showAutoSearchOption && preventAutoSearch">
11-
<div class="text-sm mb-4">You can still search for vehicles in the general configuration menu.</div>
11+
<div class="text-sm mb-4">{{ $t('vehicleDiscovery.canStillSearch') }}</div>
1212
</div>
1313
<div v-else class="flex flex-col items-center justify-center gap-4 min-w-[300px] min-h-[100px]">
1414
<div v-if="searching" class="flex flex-col items-center gap-2 mb-2">
1515
<v-progress-circular class="mb-2" indeterminate />
16-
<span>Searching for vehicles in your network...</span>
16+
<span>{{ $t('vehicleDiscovery.searching_status') }}</span>
1717
</div>
1818

1919
<div v-else-if="vehicles.length > 0" class="flex flex-col gap-2 mb-3">
20-
<div class="h-4 font-weight-bold text-center mb-5">Vehicles found!</div>
20+
<div class="h-4 font-weight-bold text-center mb-5">{{ $t('vehicleDiscovery.vehiclesFound') }}</div>
2121
<div v-for="vehicle in vehicles" :key="vehicle.address" class="flex items-center gap-2">
2222
<v-btn variant="tonal" class="max-w-[500px] justify-start truncate" @click="selectVehicle(vehicle.address)">
2323
<span class="max-w-[300px] truncate">{{ vehicle.name }}</span>
@@ -26,18 +26,18 @@
2626
</div>
2727
</div>
2828

29-
<div v-else-if="searched" class="text-sm">No vehicles found in your network.</div>
29+
<div v-else-if="searched" class="text-sm">{{ $t('vehicleDiscovery.noVehicles') }}</div>
3030

3131
<div v-if="!searching && !searched" class="flex flex-col gap-2 items-center justify-center text-center">
32-
<p v-if="props.showAutoSearchOption" class="font-bold">It looks like you're not connected to a vehicle!</p>
32+
<p v-if="props.showAutoSearchOption" class="font-bold">{{ $t('vehicleDiscovery.notConnected') }}</p>
3333
<p class="max-w-[25rem] mb-2">
34-
This tool allows you to locate and connect to BlueOS vehicles within your network.
34+
{{ $t('vehicleDiscovery.description') }}
3535
</p>
3636
</div>
3737

3838
<div v-if="!searching" class="flex justify-center items-center">
3939
<v-btn variant="outlined" :disabled="searching" class="mb-5" @click="searchVehicles">
40-
{{ searched ? 'Search again' : 'Search for vehicles' }}
40+
{{ searched ? $t('vehicleDiscovery.searchAgain') : $t('vehicleDiscovery.searchButton') }}
4141
</v-btn>
4242
</div>
4343
</div>
@@ -48,6 +48,7 @@
4848
<script setup lang="ts">
4949
import { useStorage } from '@vueuse/core'
5050
import { ref, watch } from 'vue'
51+
import { useI18n } from 'vue-i18n'
5152
5253
import { useSnackbar } from '@/composables/snackbar'
5354
import vehicleDiscover, { NetworkVehicle } from '@/libs/electron/vehicle-discovery'
@@ -71,6 +72,7 @@ const emit = defineEmits<{
7172
(e: 'update:modelValue', value: boolean): void
7273
}>()
7374
75+
const { t } = useI18n()
7476
const { openSnackbar } = useSnackbar()
7577
const mainVehicleStore = useMainVehicleStore()
7678
const discoveryService = vehicleDiscover
@@ -83,7 +85,7 @@ const preventAutoSearch = useStorage('cockpit-prevent-auto-vehicle-discovery-dia
8385
8486
const originalActions = [
8587
{
86-
text: 'Close',
88+
text: t('vehicleDiscovery.close'),
8789
action: () => {
8890
isOpen.value = false
8991
},
@@ -92,7 +94,7 @@ const originalActions = [
9294
9395
if (props.showAutoSearchOption) {
9496
originalActions.unshift({
95-
text: "Don't show again",
97+
text: t('vehicleDiscovery.dontShowAgain'),
9698
action: () => preventFutureAutoSearchs(),
9799
})
98100
}
@@ -123,7 +125,7 @@ const selectVehicle = async (address: string): Promise<void> => {
123125
mainVehicleStore.globalAddress = address
124126
isOpen.value = false
125127
await reloadCockpitAndWarnUser()
126-
openSnackbar({ message: 'Vehicle address updated', variant: 'success', duration: 5000 })
128+
openSnackbar({ message: t('success.vehicleAddressUpdated'), variant: 'success', duration: 5000 })
127129
}
128130
129131
const preventFutureAutoSearchs = (): void => {

0 commit comments

Comments
 (0)