dashboard增加多语言支持
This commit is contained in:
parent
b0cdcdb25b
commit
bcbae262b8
@ -72,9 +72,8 @@
|
||||
"
|
||||
class="border bg-red-200 px-5 py-3 text-base text-red-900"
|
||||
>
|
||||
You are not logged in.
|
||||
<router-link to="/login" class="underline">Login</router-link> to
|
||||
access dashboard.
|
||||
{{ $t('You are not logged in.') }}
|
||||
<router-link to="/login" class="underline">{{ $t('Login') }}</router-link> {{ $t('to access dashboard.') }}
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
@ -89,13 +88,14 @@
|
||||
</template>
|
||||
|
||||
<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 { Toaster } from 'vue-sonner';
|
||||
import { dialogs } from './utils/components';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getTeam } from './data/team';
|
||||
import { session } from './data/session.js';
|
||||
import { useI18n } from './composables/useI18n';
|
||||
import JLogo from '@/components/icons/JLogo.vue';
|
||||
import MenuIcon from '~icons/lucide/menu';
|
||||
import ChevronDown from '~icons/lucide/chevron-down';
|
||||
@ -111,7 +111,7 @@ const SwitchTeamDialog = defineAsyncComponent(
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const team = getTeam();
|
||||
const instance = getCurrentInstance();
|
||||
const { t } = useI18n();
|
||||
const showTeamSwitcher = ref(false);
|
||||
const isMobileDropdownOpen = ref(false);
|
||||
|
||||
@ -152,7 +152,7 @@ const sidebarCollapsed = ref(false);
|
||||
// 团队用户文本(用于移动端显示)
|
||||
const teamUserText = computed(() => {
|
||||
if (team?.get?.loading) {
|
||||
return '加载中...';
|
||||
return t('Loading...');
|
||||
}
|
||||
return team?.pg?.user || '';
|
||||
});
|
||||
@ -207,12 +207,12 @@ const mobileDropdownOptions = computed(() => {
|
||||
(teamData.valid_teams?.length > 1 || teamData.is_desk_user)
|
||||
) {
|
||||
options.push({
|
||||
label: '切换团队',
|
||||
label: t('Switch Team'),
|
||||
key: 'switch-team',
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: '退出登录',
|
||||
label: t('Logout'),
|
||||
key: 'logout',
|
||||
});
|
||||
return options;
|
||||
|
||||
14
dashboard/src/composables/useI18n.js
Normal file
14
dashboard/src/composables/useI18n.js
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { debounce } from 'jingrow-ui';
|
||||
import { getTeam } from './data/team';
|
||||
import * as formatters from './utils/format';
|
||||
import { getPlatform, isMobile } from './utils/device';
|
||||
import { t } from './utils/i18n';
|
||||
|
||||
export default function globals(app) {
|
||||
app.config.globalProperties.$session = session;
|
||||
@ -18,6 +19,7 @@ export default function globals(app) {
|
||||
app.config.globalProperties.$log = console.log;
|
||||
app.config.globalProperties.$debounce = debounce;
|
||||
app.config.globalProperties.$isMobile = isMobile();
|
||||
app.config.globalProperties.$t = t;
|
||||
|
||||
// legacy globals for old dashboard
|
||||
// TODO: remove later
|
||||
|
||||
@ -13,6 +13,7 @@ import { fetchPlans } from './data/plans.js';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import { session } from './data/session.js';
|
||||
import { unreadNotificationsCount } from './data/notifications.js';
|
||||
import { initI18n } from './utils/i18n';
|
||||
import './vendor/posthog.js';
|
||||
|
||||
const request = (options) => {
|
||||
@ -36,7 +37,10 @@ setConfig('defaultDocDeleteUrl', 'jcloud.api.client.delete');
|
||||
let app;
|
||||
let socket;
|
||||
|
||||
getInitialData().then(() => {
|
||||
getInitialData().then(async () => {
|
||||
// 初始化 i18n(在创建 app 之前)
|
||||
await initI18n();
|
||||
|
||||
app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(resourcesPlugin);
|
||||
|
||||
93
dashboard/src/utils/i18n.js
Normal file
93
dashboard/src/utils/i18n.js
Normal 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 };
|
||||
|
||||
@ -9,6 +9,9 @@ import Icons from 'unplugin-icons/vite';
|
||||
import IconsResolver from 'unplugin-icons/resolver';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
|
||||
// 语言配置:通过环境变量 DASHBOARD_LOCALE 设置,默认为 'en'
|
||||
const locale = process.env.DASHBOARD_LOCALE || 'en';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
@ -61,6 +64,10 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
// 将语言配置注入到代码中,构建时使用
|
||||
'import.meta.env.DASHBOARD_LOCALE': JSON.stringify(locale)
|
||||
},
|
||||
// @ts-ignore
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user