diff --git a/docs/agents/frontend.md b/docs/agents/frontend.md
index f1e3b72cc..7e17efdc2 100644
--- a/docs/agents/frontend.md
+++ b/docs/agents/frontend.md
@@ -73,6 +73,7 @@ request.get('/path', {
- `userStore.isAdmin` 与 `isSpaceAdmin` 的路由差异;
- xpack 静态脚本加载失败路径:`LicenseGenerator` 是无类型声明的 window 全局,登录/改密加密(`sqlbotEncrypt`)和动态路由注册都硬依赖它,脚本加载失败只提示并中断导航,没有降级。
- 新路由必须配置名称、标题 i18n 和正确父布局;不要绕过已有访问控制。
+- 菜单路由的翻译键存入 `meta.titleKey`,由布局的 `useMenuTitle` 在渲染时翻译;不要在路由模块初始化时调用 `t()` 固定标题。扩展路由提供的 `meta.title` 作为直接显示的标题兼容保留。
## 视图、组件与样式
diff --git a/frontend/src/components/layout/MenuItem.vue b/frontend/src/components/layout/MenuItem.vue
index 279030569..33c3dff9a 100644
--- a/frontend/src/components/layout/MenuItem.vue
+++ b/frontend/src/components/layout/MenuItem.vue
@@ -2,6 +2,7 @@
import { h, defineComponent } from 'vue'
import { ElMenuItem, ElSubMenu, ElIcon } from 'element-plus-secondary'
import { useRouter, useRoute } from 'vue-router'
+import { useMenuTitle } from './useMenuTitle'
import chat from '@/assets/svg/menu/icon_chat_filled.svg'
import noChat from '@/assets/svg/menu/icon_chat_outlined.svg'
import dashboard from '@/assets/svg/menu/icon_dashboard_filled.svg'
@@ -52,6 +53,7 @@ const MenuItem = defineComponent({
},
},
setup(props) {
+ const getMenuTitle = useMenuTitle()
const router = useRouter()
const route = useRoute()
const titleWithIcon = (props: any) => {
@@ -78,7 +80,8 @@ const MenuItem = defineComponent({
}
if (children?.length) {
- const { title, iconDeActive, iconActive } = props.menu?.meta || {}
+ const { iconDeActive, iconActive } = props.menu?.meta || {}
+ const title = getMenuTitle(props.menu?.meta)
const icon = route.path.startsWith(path) ? iconActive : iconDeActive
return h(
ElSubMenu,
@@ -93,7 +96,8 @@ const MenuItem = defineComponent({
)
}
- const { title, iconDeActive, iconActive } = props.menu?.meta || {}
+ const { iconDeActive, iconActive } = props.menu?.meta || {}
+ const title = getMenuTitle(props.menu?.meta)
const icon = route.path === path ? iconActive : iconDeActive
const iconCom: any = iconMap[icon] ? ElIcon : null
return h(
diff --git a/frontend/src/components/layout/index.vue b/frontend/src/components/layout/index.vue
index 4a0097e41..66a310410 100644
--- a/frontend/src/components/layout/index.vue
+++ b/frontend/src/components/layout/index.vue
@@ -45,7 +45,7 @@
- {{ t(`menu.${item.meta.title}`) }}
+ {{ getMenuTitle(item.meta) }}
@@ -140,7 +140,7 @@
- {{ t(`menu.${item.meta.title}`) }}
+ {{ getMenuTitle(item.meta) }}
@@ -172,11 +172,13 @@ import icon_ai from '@/assets/svg/icon_ai.svg'
import { ArrowLeftBold } from '@element-plus/icons-vue'
import { useCache } from '@/utils/useCache'
import { useI18n } from 'vue-i18n'
+import { useMenuTitle } from './useMenuTitle'
import LanguageSelector from '@/components/Language-selector/index.vue'
import AboutDialog from '@/components/about/index.vue'
const aboutRef = ref()
const { t } = useI18n()
+const getMenuTitle = useMenuTitle()
const { wsCache } = useCache()
const topLayout = ref(false)
const router = useRouter()
@@ -216,9 +218,9 @@ const showSubmenu = computed(() => {
] */
const currentPageTitle = computed(() => {
if (route.path.includes('/system')) {
- return 'System Settings'
+ return t('system.system_settings')
}
- return route.meta.title || 'Dashboard'
+ return getMenuTitle(route.meta) || t('dashboard.dashboard')
})
const resolveIcon = (iconName: any) => {
const icons: Record = {
diff --git a/frontend/src/components/layout/useMenuTitle.ts b/frontend/src/components/layout/useMenuTitle.ts
new file mode 100644
index 000000000..8bc60e4a9
--- /dev/null
+++ b/frontend/src/components/layout/useMenuTitle.ts
@@ -0,0 +1,13 @@
+import { useI18n } from 'vue-i18n'
+import type { RouteMeta } from 'vue-router'
+
+export const useMenuTitle = () => {
+ const { t } = useI18n()
+ return (meta: RouteMeta = {}): string => {
+ if (typeof meta.titleKey === 'string') {
+ return t(meta.titleKey)
+ }
+ // Extension routes may supply an already formatted title.
+ return typeof meta.title === 'string' ? meta.title : ''
+ }
+}
diff --git a/frontend/src/router/dynamic.ts b/frontend/src/router/dynamic.ts
index 7555741d0..7f64eb1b7 100644
--- a/frontend/src/router/dynamic.ts
+++ b/frontend/src/router/dynamic.ts
@@ -2,11 +2,9 @@ import LayoutDsl from '@/components/layout/LayoutDsl.vue'
import Datasource from '@/views/ds/Datasource.vue'
import SetAssistant from '@/views/system/embedded/iframe.vue'
-import { i18n } from '@/i18n'
import { useUserStore } from '@/stores/user'
import type { Router } from 'vue-router'
const userStore = useUserStore()
-const t = i18n.global.t
const dynamicRouterList = [
{
@@ -19,7 +17,7 @@ const dynamicRouterList = [
path: 'index',
name: 'ds',
component: Datasource,
- meta: { title: t('menu.Data Connections'), iconActive: 'ds', iconDeActive: 'noDs' },
+ meta: { titleKey: 'menu.Data Connections', iconActive: 'ds', iconDeActive: 'noDs' },
},
],
},
@@ -34,13 +32,13 @@ const dynamicRouterList = [
name: 'as',
component: SetAssistant,
meta: {
- title: t('embedded.assistant_app'),
+ titleKey: 'embedded.assistant_app',
iconActive: 'embedded',
iconDeActive: 'noEmbedded',
},
},
],
- meta: { title: t('embedded.assistant_app') },
+ meta: { titleKey: 'embedded.assistant_app' },
},
] as any[]
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index b1c266752..8d1fadf9b 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -33,10 +33,8 @@ import Workspace from '@/views/system/workspace/index.vue'
import Page401 from '@/views/error/index.vue'
import ChatPreview from '@/views/chat/preview.vue'
-import { i18n } from '@/i18n'
import { watchRouter } from './watch'
-const t = i18n.global.t
export const routes = [
{
path: '/login',
@@ -55,7 +53,7 @@ export const routes = [
props: (route: any) => {
return { startChatDsId: route.query.start_chat }
},
- meta: { title: t('menu.Data Q&A'), iconActive: 'chat', iconDeActive: 'noChat' },
+ meta: { titleKey: 'menu.Data Q&A', iconActive: 'chat', iconDeActive: 'noChat' },
},
],
},
@@ -81,7 +79,7 @@ export const routes = [
path: 'index',
name: 'ds',
component: Datasource,
- meta: { title: t('menu.Data Connections'), iconActive: 'ds', iconDeActive: 'noDs' },
+ meta: { titleKey: 'menu.Data Connections', iconActive: 'ds', iconDeActive: 'noDs' },
},
],
}, */
@@ -95,7 +93,7 @@ export const routes = [
name: 'dashboard',
component: Dashboard,
meta: {
- title: t('dashboard.dashboard'),
+ titleKey: 'dashboard.dashboard',
iconActive: 'dashboard',
iconDeActive: 'noDashboard',
},
@@ -107,43 +105,43 @@ export const routes = [
name: 'set',
component: LayoutDsl,
redirect: '/set/member',
- meta: { title: t('workspace.set'), iconActive: 'set', iconDeActive: 'noSet' },
+ meta: { titleKey: 'workspace.set', iconActive: 'set', iconDeActive: 'noSet' },
children: [
{
path: '/set/member',
name: 'member',
component: Member,
- meta: { title: t('workspace.member_management') },
+ meta: { titleKey: 'workspace.member_management' },
},
{
path: '/set/permission',
name: 'permission',
component: Permission,
- meta: { title: t('workspace.permission_configuration') },
+ meta: { titleKey: 'workspace.permission_configuration' },
},
/* {
path: '/set/assistant',
name: 'setAssistant',
component: SetAssistant,
- meta: { title: t('embedded.assistant_app') },
+ meta: { titleKey: 'embedded.assistant_app' },
}, */
{
path: '/set/professional',
name: 'professional',
component: Professional,
- meta: { title: t('professional.professional_terminology') },
+ meta: { titleKey: 'professional.professional_terminology' },
},
{
path: '/set/training',
name: 'training',
component: Training,
- meta: { title: t('training.data_training') },
+ meta: { titleKey: 'training.data_training' },
},
{
path: '/set/prompt',
name: 'prompt',
component: Prompt,
- meta: { title: t('prompt.customize_prompt_words') },
+ meta: { titleKey: 'prompt.customize_prompt_words' },
},
],
},
@@ -170,14 +168,14 @@ export const routes = [
path: 'user',
name: 'user',
component: User,
- meta: { title: t('user.user_management'), iconActive: 'user', iconDeActive: 'noUser' },
+ meta: { titleKey: 'user.user_management', iconActive: 'user', iconDeActive: 'noUser' },
},
{
path: 'workspace',
name: 'workspace',
component: Workspace,
meta: {
- title: t('user.workspace'),
+ titleKey: 'user.workspace',
iconActive: 'workspace',
iconDeActive: 'noWorkspace',
},
@@ -187,7 +185,7 @@ export const routes = [
name: 'model',
component: Model,
meta: {
- title: t('model.ai_model_configuration'),
+ titleKey: 'model.ai_model_configuration',
iconActive: 'model',
iconDeActive: 'noModel',
},
@@ -197,14 +195,14 @@ export const routes = [
name: 'embedded',
component: SystemEmbedded,
meta: {
- title: t('embedded.embedded_management'),
+ titleKey: 'embedded.embedded_management',
iconActive: 'embedded',
iconDeActive: 'noEmbedded',
},
},
{
path: 'setting',
- meta: { title: t('system.system_settings'), iconActive: 'set', iconDeActive: 'noSet' },
+ meta: { titleKey: 'system.system_settings', iconActive: 'set', iconDeActive: 'noSet' },
redirect: 'system_/appearance',
name: 'setting',
children: [
@@ -212,31 +210,31 @@ export const routes = [
path: 'appearance',
name: 'appearance',
component: Appearance,
- meta: { title: t('system.appearance_settings') },
+ meta: { titleKey: 'system.appearance_settings' },
},
{
path: 'parameter',
name: 'parameter',
component: Parameter,
- meta: { title: t('parameter.parameter_configuration') },
+ meta: { titleKey: 'parameter.parameter_configuration' },
},
{
path: 'variables',
name: 'variables',
component: Variables,
- meta: { title: t('variables.system_variables') },
+ meta: { titleKey: 'variables.system_variables' },
},
{
path: 'authentication',
name: 'authentication',
component: Authentication,
- meta: { title: t('system.authentication_settings') },
+ meta: { titleKey: 'system.authentication_settings' },
},
{
path: 'platform',
name: 'platform',
component: Platform,
- meta: { title: t('platform.title') },
+ meta: { titleKey: 'platform.title' },
},
],
},
@@ -244,7 +242,7 @@ export const routes = [
path: 'audit',
name: 'audit',
component: Audit,
- meta: { title: t('audit.system_log'), iconActive: 'log', iconDeActive: 'noLog' },
+ meta: { titleKey: 'audit.system_log', iconActive: 'log', iconDeActive: 'noLog' },
},
],
},
diff --git a/tests/browser/README.md b/tests/browser/README.md
index 0b0740604..e904bec37 100644
--- a/tests/browser/README.md
+++ b/tests/browser/README.md
@@ -100,3 +100,16 @@ npm --prefix tests/browser test -- --config=password.config.mjs
需要先安装 `frontend` 依赖。独立 Vite 夹具监听 `127.0.0.1:4208`,加载当前 checkout 的真实 `PwdForm.vue`、`userApi` 和 HTTP 请求封装,替换会话/Store 边界,并通过 Playwright 拦截改密响应。覆盖中英文策略拒绝提示、保留表单、修改后重试成功、允许保存及网络失败提示。测试使用合成密码,不访问业务数据库;它不验证真实系统参数保存、鉴权、数据库事务或重新登录。
后端对应离线测试为 `backend/tests/test_initial_password_policy.py`,提取实际函数并替换配置、用户和 Session 边界,验证开启/关闭策略、普通用户/管理员、旧密码与格式校验、拒绝时不写入。真实环境验收仍需在隔离账号上完成“开启策略 → 改密拒绝 → 合法改密 → 重新登录”。
+
+## 菜单语言与切换账号回归
+
+```sh
+npm --prefix tests/browser test -- --config=menu-language.config.mjs
+```
+
+需要 Node.js 20+、本机 Chrome 及已安装的 `frontend` 依赖。独立 Vite 夹具仅监听
+`127.0.0.1:4217`,加载当前 checkout 的真实静态/动态路由、用户 Store、i18n、
+`Menu.vue` 和 `MenuItem.vue`,替换账号 API、缓存、页面内容及导航守卫。
+测试模拟英语账号退出后另一账号登录,覆盖五种语言的普通导航、设置子菜单、
+切回英语、管理员菜单即时切换语言、普通成员的动态菜单过滤及扩展直接标题兼容。
+账号均为合成数据,不连接业务后端;本测试不替代真实登录、权限鉴权、xpack 路由或完整布局验收。
diff --git a/tests/browser/fixtures/menu-language/bridge.mjs b/tests/browser/fixtures/menu-language/bridge.mjs
new file mode 100644
index 000000000..65038c7e2
--- /dev/null
+++ b/tests/browser/fixtures/menu-language/bridge.mjs
@@ -0,0 +1,20 @@
+// Only account/network, storage and unrelated URL helpers are isolated.
+// Menu, route definitions, Pinia actions and translations use production code.
+const values = new Map([['user.language', 'en']])
+export const useCache = () => ({ wsCache: {
+ get: key => values.get(key),
+ set: (key, value) => values.set(key, value),
+ delete: key => values.delete(key)
+} })
+export const getBrowserLocale = () => 'en'
+export const getCurrentRouter = () => ''
+export const getQueryString = () => ''
+export const getSQLBotAddr = () => ''
+export const isPlatform = () => false
+let account = { id: 41, language: 'en', weight: 1 }
+export const selectAccount = value => { account = value }
+export const AuthApi = {
+ login: async () => ({ access_token: 'synthetic-menu-test-token' }),
+ logout: async () => null,
+ info: async () => ({ account: 'synthetic', name: 'Test', oid: 1, origin: 0, exp: 0, time: 0, ...account })
+}
diff --git a/tests/browser/fixtures/menu-language/index.html b/tests/browser/fixtures/menu-language/index.html
new file mode 100644
index 000000000..1f0360658
--- /dev/null
+++ b/tests/browser/fixtures/menu-language/index.html
@@ -0,0 +1,2 @@
+
+Menu language regression
diff --git a/tests/browser/fixtures/menu-language/main.mjs b/tests/browser/fixtures/menu-language/main.mjs
new file mode 100644
index 000000000..a0d84265d
--- /dev/null
+++ b/tests/browser/fixtures/menu-language/main.mjs
@@ -0,0 +1,36 @@
+import { createApp, h, nextTick } from 'vue'
+import { i18n } from '@/i18n'
+import { store } from '@/stores'
+import { useUserStore } from '@/stores/user'
+import router from '@/router'
+import { generateDynamicRouters } from '@/router/dynamic'
+import Menu from '@/components/layout/Menu.vue'
+import { selectAccount } from './bridge.mjs'
+
+const user = useUserStore()
+// Extension routes are registered before mounting the layout, as in the app.
+router.addRoute({
+ path: '/extension', name: 'extension', component: { render: () => null },
+ meta: { title: 'Extension title', iconActive: 'chat', iconDeActive: 'noChat' }
+})
+async function signIn(account) {
+ selectAccount(account)
+ await user.login({ username: 'synthetic', password: 'synthetic' })
+ await user.info()
+ generateDynamicRouters(router)
+ await router.push('/chat/index')
+ await nextTick()
+}
+await signIn({ id: 41, language: 'en', weight: 1 })
+const app = createApp({ render: () => h('main', [
+ h('p', { id: 'language' }, user.getLanguage),
+ h('h1', i18n.global.t('menu.Data Q&A')),
+ router.currentRoute.value.path !== '/login' ? h(Menu) : null
+]) })
+app.use(store).use(i18n).use(router).mount('#app')
+window.menuFixture = {
+ signIn,
+ async signOut() { await user.logout(); await router.push('/login'); await nextTick() },
+ async setLanguage(language) { user.setLanguage(language); await nextTick() },
+ async go(path) { await router.push(path); await nextTick() }
+}
diff --git a/tests/browser/fixtures/menu-language/route-stub.mjs b/tests/browser/fixtures/menu-language/route-stub.mjs
new file mode 100644
index 000000000..f4252a421
--- /dev/null
+++ b/tests/browser/fixtures/menu-language/route-stub.mjs
@@ -0,0 +1,3 @@
+// Page bodies and navigation guards are outside this component regression.
+export const watchRouter = () => {}
+export default { render: () => null }
diff --git a/tests/browser/fixtures/menu-language/server.mjs b/tests/browser/fixtures/menu-language/server.mjs
new file mode 100644
index 000000000..7f0977379
--- /dev/null
+++ b/tests/browser/fixtures/menu-language/server.mjs
@@ -0,0 +1,42 @@
+import { createRequire } from 'node:module'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const root = path.dirname(fileURLToPath(import.meta.url))
+const frontend = path.resolve(root, '../../../../frontend')
+const require = createRequire(path.join(frontend, 'package.json'))
+const { createServer } = await import(require.resolve('vite'))
+const { default: vue } = await import(require.resolve('@vitejs/plugin-vue'))
+const { default: svg } = await import(require.resolve('vite-svg-loader'))
+const bridge = path.join(root, 'bridge.mjs')
+const server = await createServer({
+ configFile: false,
+ root,
+ cacheDir: path.resolve(root, '../../output/menu-language-vite'),
+ plugins: [
+ {
+ name: 'isolate-pages',
+ enforce: 'pre',
+ resolveId(id, importer) {
+ if ((id.endsWith('.vue') && importer?.includes('/src/router/')) ||
+ (id === './watch' && importer?.endsWith('/router/index.ts'))) {
+ return path.join(root, 'route-stub.mjs')
+ }
+ }
+ },
+ vue(),
+ svg()
+ ],
+ resolve: { alias: [
+ ...['@/api/login', '@/utils/useCache', '@/utils/utils'].map(find => ({ find, replacement: bridge })),
+ { find: '@', replacement: path.join(frontend, 'src') },
+ ...['vue', 'vue-router', 'vue-i18n', 'pinia', 'element-plus-secondary', 'mitt'].map(name => ({
+ find: name, replacement: path.join(frontend, 'node_modules', name)
+ }))
+ ] },
+ server: { host: '127.0.0.1', port: 4217, strictPort: true, fs: { allow: [root, frontend] } }
+})
+await server.listen()
+for (const signal of ['SIGINT', 'SIGTERM']) {
+ process.on(signal, async () => { await server.close(); process.exit() })
+}
diff --git a/tests/browser/menu-language.config.mjs b/tests/browser/menu-language.config.mjs
new file mode 100644
index 000000000..7faaf0aba
--- /dev/null
+++ b/tests/browser/menu-language.config.mjs
@@ -0,0 +1,14 @@
+import { defineConfig } from '@playwright/test'
+import base from './playwright.config.mjs'
+
+export default defineConfig({
+ ...base,
+ webServer: {
+ command: 'node fixtures/menu-language/server.mjs',
+ url: 'http://127.0.0.1:4217',
+ reuseExistingServer: false,
+ timeout: 30000
+ },
+ projects: [{ name: 'menu-language', testMatch: 'menu-language.spec.mjs', use: { baseURL: 'http://127.0.0.1:4217' } }],
+ outputDir: 'output/menu-language-results'
+})
diff --git a/tests/browser/specs/menu-language.spec.mjs b/tests/browser/specs/menu-language.spec.mjs
new file mode 100644
index 000000000..98ea4eb91
--- /dev/null
+++ b/tests/browser/specs/menu-language.spec.mjs
@@ -0,0 +1,65 @@
+import { readFileSync } from 'node:fs'
+import { test, expect } from '@playwright/test'
+
+const locales = ['zh-TW', 'zh-CN', 'en', 'ko-KR', 'th-TH']
+const messages = Object.fromEntries(locales.map(locale => [locale,
+ JSON.parse(readFileSync(new URL(`../../../frontend/src/i18n/${locale}.json`, import.meta.url), 'utf8'))
+]))
+const text = (locale, key) => key.split('.').reduce((value, part) => value[part], messages[locale])
+const primary = ['menu.Data Q&A', 'dashboard.dashboard', 'menu.Data Connections', 'embedded.assistant_app', 'workspace.set']
+const settings = ['workspace.member_management', 'workspace.permission_configuration', 'professional.professional_terminology', 'training.data_training', 'prompt.customize_prompt_words']
+const admin = ['user.user_management', 'user.workspace', 'model.ai_model_configuration', 'embedded.embedded_management', 'system.system_settings', 'audit.system_log']
+const systemSettings = ['system.appearance_settings', 'parameter.parameter_configuration', 'variables.system_variables', 'system.authentication_settings', 'platform.title']
+
+async function expectLabels(page, locale, keys) {
+ for (const key of keys) {
+ await expect(page.getByRole('menubar').getByText(text(locale, key), { exact: true }).first()).toBeVisible()
+ }
+}
+
+for (const locale of locales) {
+ test(`切换账号后普通导航和设置子菜单跟随 ${locale}`, async ({ page }) => {
+ const errors = []
+ page.on('pageerror', error => errors.push(error.message))
+ await page.goto('/')
+ await expectLabels(page, 'en', primary)
+ await page.evaluate(() => window.menuFixture.signOut())
+ await expect(page.getByRole('menubar')).toHaveCount(0)
+ await page.evaluate(language => window.menuFixture.signIn({ id: 42, language, weight: 1 }), locale)
+ await expect(page.locator('#language')).toHaveText(locale)
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText(text(locale, 'menu.Data Q&A'))
+ await expectLabels(page, locale, primary)
+ await page.getByText(text(locale, 'workspace.set'), { exact: true }).first().click()
+ await expectLabels(page, locale, settings)
+ await page.evaluate(() => window.menuFixture.signOut())
+ await page.evaluate(() => window.menuFixture.signIn({ id: 41, language: 'en', weight: 1 }))
+ await expectLabels(page, 'en', primary)
+ expect(errors).toEqual([])
+ })
+
+ test(`管理员菜单在不刷新页面时跟随 ${locale}`, async ({ page }) => {
+ await page.goto('/')
+ await page.evaluate(() => window.menuFixture.signOut())
+ await page.evaluate(() => window.menuFixture.signIn({ id: 1, language: 'en', weight: 1 }))
+ await page.evaluate(() => window.menuFixture.go('/system/user'))
+ await expectLabels(page, 'en', admin)
+ await page.evaluate(language => window.menuFixture.setLanguage(language), locale)
+ await expectLabels(page, locale, admin)
+ await page.getByText(text(locale, 'system.system_settings'), { exact: true }).first().click()
+ await expectLabels(page, locale, systemSettings)
+ })
+}
+
+test('普通成员保持动态导航权限,扩展的直接标题保持可见', async ({ page }) => {
+ await page.goto('/')
+ await page.evaluate(() => window.menuFixture.signOut())
+ await page.evaluate(() => window.menuFixture.signIn({ id: 43, language: 'zh-TW', weight: 0 }))
+ await expectLabels(page, 'zh-TW', primary.slice(0, 2))
+ for (const key of primary.slice(2)) {
+ await expect(page.getByRole('menubar').getByText(text('zh-TW', key), { exact: true })).toHaveCount(0)
+ }
+ await page.evaluate(() => window.menuFixture.go('/extension'))
+ await expect(page.getByRole('menuitem', { name: 'Extension title' })).toBeVisible()
+ await page.evaluate(() => window.menuFixture.setLanguage('ko-KR'))
+ await expect(page.getByRole('menuitem', { name: 'Extension title' })).toBeVisible()
+})