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:
| "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.
| "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') }} |
| "cancel": "Cancel", | ||
| "save": "Save", |
There was a problem hiding this comment.
"common" fields should not be replicated in internal ones.
| "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
9ef60af to
df6ba4d
Compare
7558007 to
6bc47ea
Compare
43e76e2 to
51a9236
Compare
51a9236 to
7159d42
Compare
|
Hi @norulers! We are finishing the changes for the 1.18 stable release. Once this is done I will make sure we put some time to get this PR merged. Thanks a lot for the contribution and sorry for the delay on that! |
7159d42 to
9e72fd6
Compare
|
Hi @rafaellehmkuhl! Thanks for the heads up. Just rebased the branch onto the latest master to resolve the conflicts - ready whenever you have time to review. |
72147dd to
e61a6d9
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
- 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
e61a6d9 to
5916cdd
Compare