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"
|
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;
|
||||||
|
|||||||
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 { 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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user