feat: Add i18n infrastructure with Chinese and English translations#2314
feat: Add i18n infrastructure with Chinese and English translations#2314norulers wants to merge 2 commits intobluerobotics:masterfrom
Conversation
norulers
commented
Jan 7, 2026
- Add vue-i18n@9 for internationalization support
- Create translation files (en.json, zh.json) with UI strings
- Add LanguageSwitcher component for language selection
- Integrate i18n plugin into main app
- Add Vuetify Chinese locale support (zhHans)
- Add language switcher to main menu
3f25f72 to
66ad3a7
Compare
|
Hi @norulers, thanks for the contribution! We can't merge this until you've signed the contributor license agreement (see the comment above). I haven't had a chance to review yet, but thanks for taking the time to work on the changes Rafael requested. In future it would be preferable to force push over the original pull request instead of making a new one, but that's ok this time :-) |
|
@norulers could you check on the tests that are making the CI to fail? They are basically about linting and missing JSDocs. |
ES-Alexander
left a comment
There was a problem hiding this comment.
Some great work so far on getting things translation-ready - thanks!
Some requests focused on consistent application of "common" translations, keeping the commit history logical, and avoiding value: value translation mappings:
src/locales/en.json
Outdated
| "language": "Language", | ||
| "default": "default" | ||
| }, | ||
| "usernameDialog": { |
There was a problem hiding this comment.
I don't think all these translations should be added in the first infrastructure-focused commit, without the corresponding code variables to actually use them.
Maybe just include the "common" section in the first commit, so the files exist, and move the rest to the second commit (where they're first used)?
| const randomMissionName = coolMissionNames.random() | ||
| const randomMissionName = computed(() => { | ||
| const englishName = coolMissionNames.random() | ||
| return englishName ? t(`missionNames["${englishName}"]`) : '' |
There was a problem hiding this comment.
Given how these are used, I think it likely makes more sense to implement a per-language set of names, adjectives, etc in the funny-name library, rather than maintaining a 1:1 translation with the English ones. That also provides some extra scope for translators to have locale-specific jokes and wordplay.
This would match the "list of values" approach used for the splash screen startup messages.
| return description.slice(0, 128) + '...' | ||
|
|
||
| // Try to translate the description | ||
| const translationKey = `tools.mavlink.messageDescriptions.${description}` |
There was a problem hiding this comment.
I think this should use the messageName for the end of the key, as that's shorter and very unlikely to change, whereas the descriptions may change from time to time.
src/locales/en.json
Outdated
| "System ID of target system": "System ID of target system", | ||
| "Component ID of target component": "Component ID of target component", | ||
| "Command ID": "Command ID", | ||
| "Confirmation value (0: first transmission, 1-255: confirmations)": "Confirmation value (0: first transmission, 1-255: confirmations)", | ||
| "Parameter 1": "Parameter 1", | ||
| "Parameter 2": "Parameter 2", | ||
| "Parameter 3": "Parameter 3", | ||
| "Parameter 4": "Parameter 4", | ||
| "Parameter 5": "Parameter 5", | ||
| "Parameter 6": "Parameter 6", | ||
| "Parameter 7": "Parameter 7", | ||
| "Coordinate frame": "Coordinate frame", | ||
| "Current sequence number": "Current sequence number", | ||
| "Autocontinue bit": "Autocontinue bit", | ||
| "Latitude": "Latitude", | ||
| "Longitude": "Longitude", | ||
| "Altitude": "Altitude" |
There was a problem hiding this comment.
Using values directly as the keys likely indicates a translation is happening in the wrong place. In this case these should be associated with the src/libs/actions/mavlink-message-actions-message-definitions.ts file instead of the vue file that displays them.
| const translateFieldDescription = (description: string): string => { | ||
| const translationKey = `configuration.actions.mavlinkAction.fieldDescriptions.${description}` | ||
| const translated = t(translationKey) | ||
| // If translation key doesn't exist, return original description | ||
| return translated === translationKey ? description : translated | ||
| } |
There was a problem hiding this comment.
I think this translation should happen at the definition, in src/libs/actions/mavlink-message-actions-message-definitions.ts, rather than here. That would also allow using variable names for the translation keys, rather than just duplicating the descriptions.
| <v-card-actions> | ||
| <div class="flex justify-between items-center pa-2 w-full h-full" style="color: rgba(255, 255, 255, 0.5)"> | ||
| <v-btn @click="closeActionDialog">Cancel</v-btn> | ||
| <v-btn @click="closeActionDialog">{{ $t('configuration.actions.mavlinkAction.cancel') }}</v-btn> |
There was a problem hiding this comment.
| <v-btn @click="closeActionDialog">{{ $t('configuration.actions.mavlinkAction.cancel') }}</v-btn> | |
| <v-btn @click="closeActionDialog">{{ $t('common.cancel') }}</v-btn> |
| <v-btn @click="closeActionDialog">{{ $t('configuration.actions.mavlinkAction.cancel') }}</v-btn> | ||
| <div class="flex gap-x-10"> | ||
| <v-btn @click="resetNewAction">Reset</v-btn> | ||
| <v-btn @click="resetNewAction">{{ $t('configuration.actions.mavlinkAction.reset') }}</v-btn> |
There was a problem hiding this comment.
| <v-btn @click="resetNewAction">{{ $t('configuration.actions.mavlinkAction.reset') }}</v-btn> | |
| <v-btn @click="resetNewAction">{{ $t('common.reset') }}</v-btn> |
| <v-btn @click="resetNewAction">{{ $t('configuration.actions.mavlinkAction.reset') }}</v-btn> | ||
| <v-btn :disabled="!isFormValid" class="text-white" @click="saveActionConfig"> | ||
| {{ editMode ? 'Save' : 'Create' }} | ||
| {{ editMode ? $t('configuration.actions.mavlinkAction.save') : $t('configuration.actions.mavlinkAction.create') }} |
There was a problem hiding this comment.
| {{ editMode ? $t('configuration.actions.mavlinkAction.save') : $t('configuration.actions.mavlinkAction.create') }} | |
| {{ editMode ? $t('common.save') : $t('configuration.actions.mavlinkAction.create') }} |
src/locales/en.json
Outdated
| "cancel": "Cancel", | ||
| "save": "Save", |
There was a problem hiding this comment.
"common" fields should not be replicated in internal ones.
src/locales/en.json
Outdated
| "reset": "Reset", | ||
| "noMessagesFound": "No messages found", | ||
| "messageValues": "Message Values", | ||
| "messageDescriptions": { |
There was a problem hiding this comment.
Since these are coming from a submodule, I don't think they should be included in the English translations - it can instead rely on the fallback behaviour of directly using the imported value given an undefined translation.
8980922 to
af729b0
Compare
- Use common.cancel/save/reset across all components instead of duplicates - Refactor MAVLink field descriptions to use variable names as translation keys - Change 'description' to 'descriptionKey' in message definitions - Use field names (target_system, command, etc) as keys instead of values - Update MAVLink message descriptions to use messageName as translation key - Change from description values to message names (HEARTBEAT, BATTERY_STATUS, etc) - Remove English MAVLink descriptions from en.json (rely on library fallback) - Update zh.json to use message names as keys for all MAVLink translations - Add JSDoc comments to vite.config.ts plugin functions Addresses ES-Alexander architectural feedback in PR bluerobotics#2314
7c13450 to
2bc9053
Compare
|
This PR is continually growing in a way that makes it very challenging to review. If you are not "done" yet then please "Convert to draft" (in the bottom right corner, above the "add a comment" button) for now, then mark it as "Ready for review" again once you are finished changing it. More generally:
I am aware that updates to the main codebase also require rebasing and updating this PR, but we need to understand and agree on the approach before it can be reviewed properly, and if the commits are functionally separated that should be easier to do as well. Once a shared understanding is established we can do an initial review of the infrastructure, and once that seems fine we can set aside a "no merging" time period while the final translations get updated and reviewed, after which we can merge this PR and switch to maintenance mode (where changes trigger translation requests from relevant contributors). |
00a52e5 to
68e573e
Compare
0ee096d to
0a80d57
Compare
|
Noting that the last two commits you've added should be rebased in to follow the convention of the earlier ones (as outlined in point 3 of my previous comment). It would be helpful if you could respond to at least point 2 of my previous comment, so we can try to understand (and discuss) the logic being used for where a given string gets added in the translation file. |
Thanks for the feedback! Regarding the organization logic for the translation files (point 2): Current Organization Structure common - Universal UI elements used across multiple components (buttons, actions, status messages) Check if it's a common element (cancel, save, ok, etc.) → add to common Use common.save for the button (already exists) I'll also work on rebasing the last commits as suggested in point 3. |
702404a to
64e0af9
Compare
|
@norulers @ES-Alexander my suggestion to make it easier to find the translations (as well as automate that in the future) would be to use the component path itself as the location inside the translation file: Example translations for ...
"components": {
{
configurations: {
"ActionLinkConfig.vue": {
"noFasterThan": "No faster than",
"changesPerSecond": "Changes per second",
...
},
...
},
...
},
...
}What do you think? |
|
@rafaellehmkuhl, that's likely more verbose, but it does seem like it would make it easier to support naive automation approaches in future, and reduces ambiguity over where something should go (without us needing to extensively consider and discuss a rule-based standard). I'm unsure whether that has any kind of meaningful impact on performance (due to extra layers to traverse) 🤷♂️ . I imagine the memory usage would be a bit higher, but likely not significantly given the data is all text. Either way, I do think it makes sense to at least have a separated "common" (or "standard"?) section for the interface terms that are intended to be re-used in multiple places (save, cancel, continue, etc), to avoid the possibility of inconsistent translations for the same term. If all such interface components get pulled out into a consistent library or something then I suppose that would be even better, but in the absence of that being implemented in the code I'd want at least that one shared translation section to minimise duplicates. |
Agree. Common should stay. And performance-wise the longer paths won't make any difference. |
f3063a1 to
2c467f3
Compare
cbd4e54 to
465591e
Compare
1c86479 to
bca591a
Compare
- Add vue-i18n dependency and i18n plugin - Create English and Chinese translation files (en.json, zh.json) - Add Vuetify Chinese locale support - Configure i18n with fallback locale and locale persistence
bca591a to
9ef60af
Compare
- Import and use vue-i18n in all components and views - Add LanguageSwitcher component for runtime language switching - Replace all hardcoded strings with translation keys - Add getChatMessage to electron menu for language switching support - Fix ConfigurationLogsView.vue conflict with upstream monaco editor changes - Use common.* keys for shared translations (cancel, save, reset) - Add JSDoc to all new i18n helper functions
9ef60af to
df6ba4d
Compare