Internationalization and themes
Language and theme follow the same principle: each has one source of truth, and changes are reflected immediately in the DOM and component library without requiring a page refresh.
Internationalization structure
src/config/locale.ts Language registry
src/locales/ Application copy
src/plugins/vueI18n/ Creates the i18n instance
src/store/modules/locale.ts Current language and persistence
src/hooks/useLocale.ts Runtime switching
ConfigGlobal Synchronizes the Element Plus localelocaleRegistry registers application messages, display names, and Element Plus language packs together:
export const localeRegistry = {
'zh-CN': {
label: '简体中文',
messages: zhCNMessages,
elementLocale: zhCNElement
},
en: {
label: 'English',
messages: enMessages,
elementLocale: enElement
}
} as constBoth the dropdown options and vue-i18n messages are derived from this registry, so the language list does not need to be maintained in three separate files.
Switch languages
const { changeLocale } = useLocale()
changeLocale('en')One call synchronizes:
- The current vue-i18n locale.
- Locale Store, persisted to
vea-locale-v1. <html lang="...">.- The Element Plus locale in ConfigGlobal.
- The browser title generated by
useTitle.
Language switching therefore does not require a refresh.
Add a language
Using Japanese as an example:
- Create
src/locales/ja.tswith the same key structure as the existing languages. - Import
element-plus/es/locale/lang/jafrom Element Plus. - Add
jatolocaleRegistry.
LocaleCode, the dropdown options, and the message map automatically receive the new type and data.
Language packs are currently imported synchronously. Asynchronous loading becomes worthwhile only when the number or size of language packs grows substantially; two languages do not justify adding switching states and load-failure branches.
Use translated copy
const { t } = useI18n()
const title = computed(() => t('router.dashboard'))Use functions for form validation messages so a new validation run reads the active language after a locale change:
username: required(() => t('common.required'))The current route meta.title stores an i18n key. Menus, breadcrumbs, and TagsView should all render it through the same translation logic.
Theme structure
Light and dark state is stored in App Store:
const appStore = useAppStore()
appStore.isDark = !appStore.isDarkuseTheme watches this state and switches the light or dark class on <html>. The state is persisted in vea-app-v2 and restored after a page refresh.
Only manual light and dark modes are currently supported. There is no follow-system option, so documentation and product settings should not present a third mode that does not exist.
Theme variables
Shared variables live in packages/styles/src/var.css:
html:root {
--el-color-primary: #5b5bd6;
--app-content-bg-color: #f6f7fb;
--left-menu-bg-color: #fff;
}
html:root.dark {
--el-color-primary: #818cf8;
--app-content-bg-color: #0b1120;
--left-menu-bg-color: #111827;
}Element Plus variables and semantic layout variables are overridden under the same theme. This avoids isolated color systems for the top navigation, sidebar, and content area.
When introducing a color, define its purpose first—for example, --layout-border-color. Avoid defining a context-free name such as --gray-7 and making every component guess how it should be used.
Theme component boundaries
ThemeSwitch can remain in @vea/components because it only accepts controlled state and emits an event. The Admin app still owns Store access and DOM switching. A shared component must not read useAppStore() directly, or another app will be unable to reuse it.
If another application only needs a normal button, it can invoke the theme action directly. It does not have to use the same presentation component merely for consistency.