dashboard增加多语言支持

This commit is contained in:
jingrow 2025-12-28 23:14:23 +08:00
parent b0cdcdb25b
commit bcbae262b8
6 changed files with 129 additions and 9 deletions

View File

@ -72,9 +72,8 @@
" "
class="border bg-red-200 px-5 py-3 text-base text-red-900" class="border bg-red-200 px-5 py-3 text-base text-red-900"
> >
You are not logged in. {{ $t('You are not logged in.') }}
<router-link to="/login" class="underline">Login</router-link> to <router-link to="/login" class="underline">{{ $t('Login') }}</router-link> {{ $t('to access dashboard.') }}
access dashboard.
</div> </div>
<router-view /> <router-view />
</div> </div>
@ -89,13 +88,14 @@
</template> </template>
<script setup> <script setup>
import { defineAsyncComponent, computed, watch, ref, provide, onMounted, onUnmounted, h, getCurrentInstance } from 'vue'; import { defineAsyncComponent, computed, watch, ref, provide, onMounted, onUnmounted } from 'vue';
import { NLayout, NLayoutSider, NConfigProvider, NButton, NIcon, NDropdown } from 'naive-ui'; import { NLayout, NLayoutSider, NConfigProvider, NButton, NIcon, NDropdown } from 'naive-ui';
import { Toaster } from 'vue-sonner'; import { Toaster } from 'vue-sonner';
import { dialogs } from './utils/components'; import { dialogs } from './utils/components';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { getTeam } from './data/team'; import { getTeam } from './data/team';
import { session } from './data/session.js'; import { session } from './data/session.js';
import { useI18n } from './composables/useI18n';
import JLogo from '@/components/icons/JLogo.vue'; import JLogo from '@/components/icons/JLogo.vue';
import MenuIcon from '~icons/lucide/menu'; import MenuIcon from '~icons/lucide/menu';
import ChevronDown from '~icons/lucide/chevron-down'; import ChevronDown from '~icons/lucide/chevron-down';
@ -111,7 +111,7 @@ const SwitchTeamDialog = defineAsyncComponent(
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const team = getTeam(); const team = getTeam();
const instance = getCurrentInstance(); const { t } = useI18n();
const showTeamSwitcher = ref(false); const showTeamSwitcher = ref(false);
const isMobileDropdownOpen = ref(false); const isMobileDropdownOpen = ref(false);
@ -152,7 +152,7 @@ const sidebarCollapsed = ref(false);
// //
const teamUserText = computed(() => { const teamUserText = computed(() => {
if (team?.get?.loading) { if (team?.get?.loading) {
return '加载中...'; return t('Loading...');
} }
return team?.pg?.user || ''; return team?.pg?.user || '';
}); });
@ -207,12 +207,12 @@ const mobileDropdownOptions = computed(() => {
(teamData.valid_teams?.length > 1 || teamData.is_desk_user) (teamData.valid_teams?.length > 1 || teamData.is_desk_user)
) { ) {
options.push({ options.push({
label: '切换团队', label: t('Switch Team'),
key: 'switch-team', key: 'switch-team',
}); });
} }
options.push({ options.push({
label: '退出登录', label: t('Logout'),
key: 'logout', key: 'logout',
}); });
return options; return options;

View File

@ -0,0 +1,14 @@
import { t, getLocale, currentLocale } from '@/utils/i18n';
/**
* i18n composable
* 在组件中使用const { t, getLocale, currentLocale } = useI18n();
*/
export function useI18n() {
return {
t,
getLocale,
currentLocale
};
}

View File

@ -6,6 +6,7 @@ import { debounce } from 'jingrow-ui';
import { getTeam } from './data/team'; import { getTeam } from './data/team';
import * as formatters from './utils/format'; import * as formatters from './utils/format';
import { getPlatform, isMobile } from './utils/device'; import { getPlatform, isMobile } from './utils/device';
import { t } from './utils/i18n';
export default function globals(app) { export default function globals(app) {
app.config.globalProperties.$session = session; app.config.globalProperties.$session = session;
@ -18,6 +19,7 @@ export default function globals(app) {
app.config.globalProperties.$log = console.log; app.config.globalProperties.$log = console.log;
app.config.globalProperties.$debounce = debounce; app.config.globalProperties.$debounce = debounce;
app.config.globalProperties.$isMobile = isMobile(); app.config.globalProperties.$isMobile = isMobile();
app.config.globalProperties.$t = t;
// legacy globals for old dashboard // legacy globals for old dashboard
// TODO: remove later // TODO: remove later

View File

@ -13,6 +13,7 @@ import { fetchPlans } from './data/plans.js';
import * as Sentry from '@sentry/vue'; import * as Sentry from '@sentry/vue';
import { session } from './data/session.js'; import { session } from './data/session.js';
import { unreadNotificationsCount } from './data/notifications.js'; import { unreadNotificationsCount } from './data/notifications.js';
import { initI18n } from './utils/i18n';
import './vendor/posthog.js'; import './vendor/posthog.js';
const request = (options) => { const request = (options) => {
@ -36,7 +37,10 @@ setConfig('defaultDocDeleteUrl', 'jcloud.api.client.delete');
let app; let app;
let socket; let socket;
getInitialData().then(() => { getInitialData().then(async () => {
// 初始化 i18n在创建 app 之前)
await initI18n();
app = createApp(App); app = createApp(App);
app.use(router); app.use(router);
app.use(resourcesPlugin); app.use(resourcesPlugin);

View File

@ -0,0 +1,93 @@
import { ref } from 'vue';
import { jingrowRequest } from 'jingrow-ui';
// 从构建时配置获取语言,默认为英文
const buildLocale = import.meta.env.DASHBOARD_LOCALE || 'en';
// 当前语言(从构建配置获取,不可切换)
const currentLocale = ref(buildLocale);
const translations = ref({});
const isLoading = ref(false);
const initPromise = ref(null);
/**
* 从后端加载翻译
* @param {string} locale - 语言代码默认 'en'
*/
export async function loadTranslations(locale = 'en') {
// 如果正在加载或已加载相同语言,直接返回
if (isLoading.value) {
return initPromise.value;
}
if (currentLocale.value === locale && Object.keys(translations.value).length > 0) {
return Promise.resolve();
}
isLoading.value = true;
const promise = jingrowRequest({
url: '/api/action/jingrow.translate.get_app_translations',
method: 'GET',
params: { _lang: locale }
})
.then((response) => {
translations.value = response || {};
currentLocale.value = locale;
})
.catch((error) => {
console.error('Failed to load translations:', error);
translations.value = {};
})
.finally(() => {
isLoading.value = false;
});
initPromise.value = promise;
return promise;
}
/**
* 翻译函数
* @param {string} key - 翻译键通常是英文原文
* @param {object} params - 参数对象用于替换占位符
* @returns {string} 翻译后的文本
*/
export function t(key, params = {}) {
if (!key || typeof key !== 'string') return key;
// 从翻译字典中获取翻译,如果没有则返回原文
let translated = translations.value[key] || key;
// 支持参数替换,如 t('Hello {name}', { name: 'John' })
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
Object.keys(params).forEach((paramKey) => {
const regex = new RegExp(`\\{${paramKey}\\}`, 'g');
translated = translated.replace(regex, String(params[paramKey]));
});
}
return translated;
}
/**
* 获取当前语言构建时配置的语言
* @returns {string} 当前语言代码
*/
export function getLocale() {
return currentLocale.value;
}
/**
* 初始化 i18n
* 使用构建时配置的语言
*/
export async function initI18n() {
const locale = buildLocale.split('-')[0] || 'en'; // 处理 'en-US' -> 'en'
await loadTranslations(locale);
document.documentElement.lang = locale;
}
// 导出响应式状态(供 composable 使用)
export { currentLocale, isLoading };

View File

@ -9,6 +9,9 @@ import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver'; import IconsResolver from 'unplugin-icons/resolver';
import { sentryVitePlugin } from '@sentry/vite-plugin'; import { sentryVitePlugin } from '@sentry/vite-plugin';
// 语言配置:通过环境变量 DASHBOARD_LOCALE 设置,默认为 'en'
const locale = process.env.DASHBOARD_LOCALE || 'en';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
@ -61,6 +64,10 @@ export default defineConfig({
} }
} }
}, },
define: {
// 将语言配置注入到代码中,构建时使用
'import.meta.env.DASHBOARD_LOCALE': JSON.stringify(locale)
},
// @ts-ignore // @ts-ignore
test: { test: {
globals: true, globals: true,