Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Pinia 3、Element Plus、Tailwind CSS 4 和 Sass;工作流画布基于 LogicFl
| [VIEW_README.md](src/views/VIEW_README.md) | 页面职责、功能本地组织与业务约束 |
| [WORKFLOW_README.md](src/workflow-canvas/WORKFLOW_README.md) | 画布内核、节点、资源查询与接入接口 |
| [CONSTANT_README.md](src/constants/CONSTANT_README.md) | 共享展示常量 |
| [LOCALE_README.md](src/locales/LOCALE_README.md) | 国际化分类、语言状态与使用约定 |
| [UTILS_README.md](src/utils/UTILS_README.md) | 共享工具函数与组合式函数 |

新增专题规则文档时补充此索引;仅在目录、公开接口或使用规则变化时同步对应章节。
Expand All @@ -43,7 +44,7 @@ Pinia 3、Element Plus、Tailwind CSS 4 和 Sass;工作流画布基于 LogicFl
| `src/constants/`、`src/utils/` | 跨功能常量与工具 |
| `src/assets/`、`public/` | 源码导入资源/需要保留原文件名和 URL 的静态文件 |

国际化尚未集成,未创建 `src/locales/`。文件放入现有功能边界,不创建平行的顶层结构。
国际化基础及分类规则见 [LOCALE_README.md](src/locales/LOCALE_README.md)。文件放入现有功能边界,不创建平行的顶层结构。
各入口复用自己的 Pinia:Admin 使用 `@/stores/admin`,Chat 使用 `@/stores/chat` 的 `useStore()`;
Store 内部协调状态时可直接引用同入口的具体 Store,避免聚合入口循环依赖。
`src/stores/index.ts` 当前是 Admin 兼容入口,不能用它获取 Chat Store。
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.10",
"vue-draggable-plus": "^0.6.1",
"vue-i18n": "^11.4.18",
"vue-router": "^5.1.0"
},
"devDependencies": {
Expand Down
518 changes: 410 additions & 108 deletions ui/pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion ui/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import zhTw from 'element-plus/es/locale/lang/zh-tw'
import { normalizeLocale } from '@/locales/config'
import { RouterView } from 'vue-router'
import { TOOLTIP_SHOW_DELAY } from '@/components/global/mk-tooltip/constants'

const { locale } = useI18n({ useScope: 'global' })
const elementLocales = { 'zh-CN': zhCn, 'en-US': en, 'zh-Hant': zhTw }
const elementLocale = computed(() => elementLocales[normalizeLocale(locale.value)])

const tableConfig = { tooltipOptions: { showAfter: TOOLTIP_SHOW_DELAY } }
</script>

<template>
<el-config-provider :table="tableConfig">
<el-config-provider :table="tableConfig" :locale="elementLocale">
<RouterView />
</el-config-provider>
</template>
7 changes: 4 additions & 3 deletions ui/src/chat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

import App from './App.vue'
import { configureMarkdownEditor } from '@/components/global/markdown-editor/config'
import router from '@/router/chat'
import { pinia } from '@/stores/chat'
import { pinia, useStore } from '@/stores/chat'
import { createAppI18n } from '@/locales'
import 'element-plus/dist/index.css'
import '@/styles/tailwind.css'
import '@/styles/index.scss'
Expand All @@ -15,7 +15,8 @@ configureMarkdownEditor()
const app = createApp(App)

app.use(pinia)
app.use(createAppI18n(useStore().user))
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.use(ElementPlus)

app.mount('#app')
1 change: 1 addition & 0 deletions ui/src/constants/CONSTANT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ API 枚举值。例如角色类型和登录方式以 `src/api/enums` 为唯一

`document.ts` 的 `DOCUMENT_HIT_HANDLING_LABELS` 维护文档命中处理方式的展示文案,
映射键引用 API 枚举 `DOCUMENT_HIT_HANDLING`。
`DOCUMENT_SPLIT_PATTERN_OPTIONS` 维护文档分段标识的完整选项与正则值,知识库文档策略表单和工作流分段节点共同引用。

`file-type.ts` 维护图片、文档、视频和音频的文件扩展名常量,扩展名统一使用大写。
使用方判断文件类型时先将后缀转换为大写,再与对应常量匹配。
Expand Down
56 changes: 56 additions & 0 deletions ui/src/constants/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,59 @@ export const DOCUMENT_HIT_HANDLING_LABELS = {
[DOCUMENT_HIT_HANDLING.OPTIMIZATION]: '模型优化',
[DOCUMENT_HIT_HANDLING.DIRECTLY_RETURN]: '直接回答',
} as const

// 与后端 DocumentSerializers.SplitPattern 的固定标识保持一致;顺序由用户选择决定。
export const DOCUMENT_SPLIT_PATTERN_OPTIONS = [
{
label: '#',
value: '(?<=^)# .*|(?<=\\n)# .*',
},
{
label: '##',
value: '(?<=\\n)(?<!#)## (?!#).*|(?<=^)(?<!#)## (?!#).*',
},
{
label: '###',
value: '(?<=\\n)(?<!#)### (?!#).*|(?<=^)(?<!#)### (?!#).*',
},
{
label: '####',
value: '(?<=\\n)(?<!#)#### (?!#).*|(?<=^)(?<!#)#### (?!#).*',
},
{
label: '#####',
value: '(?<=\\n)(?<!#)##### (?!#).*|(?<=^)(?<!#)##### (?!#).*',
},
{
label: '######',
value: '(?<=\\n)(?<!#)###### (?!#).*|(?<=^)(?<!#)###### (?!#).*',
},
{
label: '-',
value: '(?<! )- .*',
},
{
label: '空格',
value: '(?<! ) (?! )',
},
{
label: '分号',
value: '(?<!;);(?!;)',
},
{
label: '逗号',
value: '(?<!,),(?!,)',
},
{
label: '句号',
value: '(?<!。)。(?!。)',
},
{
label: '回车',
value: '(?<!\\n)\\n(?!\\n)',
},
{
label: '空行',
value: '(?<!\\n)\\n\\n(?!\\n)',
},
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the review findings:

Potential Issues

1. - list item regex is too broad

'(?<! )- .*'

This only guards against a preceding space, so it will incorrectly match hyphen-minus in contexts like:

  • Negative numbers: value - 5, x - y
  • YAML-like text or inline math
  • A -- flag with a following word

It also lacks a lookbehind for \n or start-of-line anchor pairing, unlike the heading patterns. Consider aligning it with the heading style (match line-start or after \n).

2. Missing anchor consistency
Heading patterns use (?<=^) but ^ in a lookbehind without the multiline flag only matches string start. If this regex runs on multi-line text without m flag, (?<=^) never fires except at position 0. The (?<=\n) branch covers most cases, but this is fragile — precompiled regexes should ideally use the m flag. Verify how the backend applies these patterns.

3. Heading negative lookaheads can mismatch across branches
For ##: (?<=\n)(?<!#)## (?!#).*|(?<=^)(?<!#)## (?!#).*. The (?<!#) before ## correctly prevents matching ###, but the two alternatives duplicate logic unnecessarily. Could be simplified to (?<=^|\n)(?<!#)## (?!#).* only if variable-length lookbehind is supported (it's not in JS), so keeping two branches is acceptable — just note the duplication.

4. Space pattern (?<! ) (?! ) is greedy on whitespace-normalized input
This matches any single space between non-spaces, including spaces inside markdown links, code, or English words. For CJK text splitting that may be intended, but confirm it won't break URLs/code blocks.

5. Labels are Chinese while the file mixes conventions
Existing labels are Chinese (模型优化, 直接回答) so this is consistent — no issue, just noting for reviewers.

Optimization Suggestions

  • Deduplicate the heading generation with a small helper:
const headingPattern = (n: number) => {
  const hashes = '#'.repeat(n);
  const guard = `(?<!${'#'.repeat(n - 1) === '' ? '\\b' : '#'})`;
  // simpler: build explicit lookarounds
};

Or generate programmatically from a loop over 1–6 to reduce repetition and copy-paste risk.

  • Add as const to keep literal types, consistent with the constants block above:
] as const;
  • Consider m flag / documenting flags: export whether these strings are used with new RegExp(value, 'gm') so consumers know expected behavior.

  • Add a type/interface for the option shape ({ label: string; value: string }[]) for reuse and clarity.

  • Comment the lack of anchors for -, 空格, etc., since they behave differently from headings.

Summary

Main concerns: the - pattern over-matching and reliance on ^ without the multiline flag. The rest is stylistic/repetition cleanup.

12 changes: 3 additions & 9 deletions ui/src/layout/avatar-dropdown/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, useTemplateRef } from 'vue'
import { useRouter } from 'vue-router'
import LoginApi from '@/api/admin/auth/login'
import { LOGIN_METHOD } from '@/api/enums'
import { LOCALE_OPTIONS, type Locale } from '@/locales/config'
import { useStore } from '@/stores'
import ChangePasswordDialog from './ChangePasswordDialog.vue'
import AboutDialog from './AboutDialog.vue'
Expand All @@ -11,16 +12,9 @@ import perm from '@/permission/index.ts'

defineOptions({ name: 'AvatarDropdown' })

type Language = 'en' | 'zh-CN' | 'zh-TW'

const currentLanguage = ref<Language>('zh-CN')
const currentLanguage = ref<Locale>('zh-CN')
const router = useRouter()
const { auth, user, theme } = useStore()
const languages: Array<{ label: string; value: Language }> = [
{ label: 'English', value: 'en' },
{ label: '简体中文', value: 'zh-CN' },
{ label: '繁體中文', value: 'zh-TW' },
]

// 修改当前账号密码。
const changePasswordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>()
Expand Down Expand Up @@ -104,7 +98,7 @@ function toUrl(url?: string) {
<template #dropdown>
<MkDropdownMenu class="w-52">
<MkDropdownItem
v-for="language in languages"
v-for="language in LOCALE_OPTIONS"
:key="language.value"
selectable
:selected="currentLanguage === language.value"
Expand Down
59 changes: 59 additions & 0 deletions ui/src/locales/LOCALE_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 国际化规则

`src/locales` 负责界面语言、语言包和入口初始化。当前支持 `zh-CN`、`en-US` 与 `zh-Hant`,
默认及缺失文案回退为简体中文。语言包暂随入口加载,新增语言时再评估按需加载。

## 分类

按“业务域 → 对象或场景 → 文案用途”组织;文件按业务域划分,不对应组件目录。
当前只建立实际使用的 common.json,不预建空业务包。

| 业务域 | 内容 |
| ----------- | ------------------------------------------------ |
| common | 无业务含义的操作、基础校验、界面偏好和通用错误页 |
| auth | 登录、退出、找回密码、身份认证 |
| navigation | 导航菜单、面包屑、浏览器页面标题 |
| application | 智能体创建、配置、发布、版本 |
| knowledge | 知识库、文档、分段、问题、标签、检索 |
| model | 模型、供应商、模型参数与连接测试 |
| tool | 工具、MCP、Skill 管理 |
| workflow | 画布、节点、变量、调试与执行展示 |
| chat | 对话输入、消息操作、历史会话与对话状态 |
| workspace | 工作空间、成员、空间级设置 |
| system | 用户、角色、系统配置、许可证 |

业务域内按实际对象拆分,如 knowledge.document;末级按用途使用:
`title`、`description`、`actions`、`fields`、`validation`、`messages`、`dialogs`、
`states`、`options`、`empty`。fields 内使用字段名下的 label、placeholder、description。
common.errors 用于通用错误场景,例如 common.errors.notFound.description。

归属以业务含义判断:知识库检索模式归 knowledge,知识库检索节点归 workflow。
系统资源管理复用原业务域,不复制到 system。多处使用不等于通用;相同中文但业务语义
不同可以使用不同 key。用户内容、提示词、协议枚举和已保存节点名称不进入语言包。

## 使用约定

- key 使用表达业务含义的英文 camelCase,不使用中文、数字序号或 misc 等模糊分类。
- JSON 文件不重复包裹业务域,由 index.ts 挂载命名空间;各语言保持相同 key 与插值参数。
- 完整句子整体翻译,动态内容使用命名参数;不用片段拼接句子。
- 组件显式导入 useI18n,并使用全局作用域的 t;不使用自动注入的 $t。
- 依赖语言的选项和校验规则在 computed 或执行时翻译,不在模块初始化时固化翻译结果。
- 新增业务包时更新 index.ts 的所有语言及 MessageSchema,以中文结构约束翻译 key。
- 非组件代码通过调用方传入翻译能力,不依赖某个入口的全局国际化单例。

## 语言状态

Admin 和 Chat 分别调用 createAppI18n,连接自己的 User Store;该模块不导入任何 Store。
Store 是语言状态来源,通过 setLanguage 更新,不能直接修改 Vue I18n 的 locale。
App.vue 通过当前 locale 配置 Element Plus,初始化同时维护 html.lang。

两个入口继续共用 MaxKB-locale。初始化按本地缓存、浏览器语言、默认中文选择;
英语变体归一为 en-US;zh-Hant、zh-TW、zh-HK、zh-MO 归一为 zh-Hant,
显式 zh-Hans 优先使用简体,其余语言回退 zh-CN。Element Plus 的繁体映射为 zh-tw。Admin 加载账号后继续采用账号语言。
跨标签页缓存变化同步到当前 Store。请求头复用原请求层的 user.language。
当前语言选择只保存本地,不写回账号;账号偏好优先级在账号设置模块中处理。

## 分批迁移

404 页面为当前样例,提供简体、英文及繁体切换。其余界面、路由标题、编辑器及日期格式专项
按模块接入,允许过渡期保留原中文。每批完成后等待用户检查,未经确认不扩展下一批。
26 changes: 26 additions & 0 deletions ui/src/locales/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** 定义当前支持的界面语言与偏好存储规则。 */
export const LANGUAGE_STORAGE_KEY = 'MaxKB-locale'
export const DEFAULT_LOCALE = 'zh-CN'
export const LOCALE_OPTIONS = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'en-US', label: 'English' },
{ value: 'zh-Hant', label: '繁體中文' },
] as const
export type Locale = (typeof LOCALE_OPTIONS)[number]['value']

/** 将浏览器或账号语言映射到已支持的界面语言。 */
export function normalizeLocale(language: string | null | undefined): Locale {
const [languageCode, ...subtags] = language?.replaceAll('_', '-').toLowerCase().split('-') ?? []
if (languageCode === 'en') return 'en-US'
if (languageCode === 'zh') {
// 显式文字体系优先于地区,例如 zh-Hans-HK 仍使用简体。
if (subtags.includes('hans')) return 'zh-CN'
if (subtags.includes('hant') || subtags.some((subtag) => ['tw', 'hk', 'mo'].includes(subtag))) return 'zh-Hant'
}
return DEFAULT_LOCALE
}

/** 优先读取本地偏好,其次使用浏览器语言,最终回退简体中文。 */
export function getDefaultLanguage(): Locale {
return normalizeLocale(localStorage.getItem(LANGUAGE_STORAGE_KEY) || navigator.language)
}
5 changes: 5 additions & 0 deletions ui/src/locales/en-US/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"actions": { "goHome": "Back to home" },
"fields": { "language": { "label": "Interface language" } },
"errors": { "notFound": { "description": "This page does not exist or has been deleted." } }
}
52 changes: 52 additions & 0 deletions ui/src/locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** 为每个应用入口创建独立的国际化实例,并连接该入口的语言状态。 */
import { watch } from 'vue'
import { createI18n } from 'vue-i18n'
import { DEFAULT_LOCALE, LANGUAGE_STORAGE_KEY, normalizeLocale, type Locale } from './config'
import zhCommon from './zh-CN/common.json'
import enCommon from './en-US/common.json'
import hantCommon from './zh-Hant/common.json'

type MessageSchema = { common: typeof zhCommon }
declare module 'vue-i18n' {
export interface DefineLocaleMessage {
common: MessageSchema['common']
}
}

interface LanguageState {
language: string
setLanguage: (language: string) => void
}

/** 在应用挂载前绑定 Store;不依赖 Admin 或 Chat 的具体实现。 */
export function createAppI18n(user: LanguageState) {
const i18n = createI18n<[MessageSchema], Locale, false>({
legacy: false,
globalInjection: false,
locale: normalizeLocale(user.language),
fallbackLocale: DEFAULT_LOCALE,
messages: {
'zh-CN': { common: zhCommon },
'en-US': { common: enCommon },
'zh-Hant': { common: hantCommon },
},
})

watch(
() => user.language,
(language) => {
const locale = normalizeLocale(language)
i18n.global.locale.value = locale
document.documentElement.lang = locale
},
{ immediate: true, flush: 'sync' },
)

window.addEventListener('storage', (event) => {
if (event.storageArea === localStorage && (event.key === LANGUAGE_STORAGE_KEY || event.key === null)) {
const language = normalizeLocale(event.newValue || navigator.language)
if (user.language !== language) user.setLanguage(language)
}
})
return i18n
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Potential Issues

  1. Event listener leak – The storage event listener on window is never removed. Every call to createAppI18n() adds a new listener (one per app entry: Admin, Chat, etc.), causing memory leaks and duplicate handlers that all call user.setLanguage. Consider returning a cleanup function or using onScopeDispose / onUnmounted.

  2. flush: 'sync' in watch – Synchronous flush can cause bugs when the watched value changes during render/component setup, and disables Vue's batching. Only use it if truly required. 'pre' (default) or 'post' is usually safer.

  3. event.storageArea === localStorage – storageArea may be null in some browsers/tests. Safer check: event.key === ... && (event.storageArea == null || event.storageArea === localStorage). Also consider guarding for SSR (typeof window !== 'undefined').

  4. Initial document.documentElement.lang side-effect – Setting document.documentElement.lang inside a composable couples i18n logic to the DOM. If createAppI18n is called multiple times (Admin + Chat simultaneously), both will write lang. That's acceptable but worth documenting; alternatively, expose a setter.

  5. Cross-tab newValue fallback – When event.key === null (clear), you fall back to navigator.language, ignoring LANGUAGE_STORAGE_KEY removal semantics. Confirm this is intentional; otherwise read from localStorage.getItem(LANGUAGE_STORAGE_KEY) first.

  6. No guard against non-browser env – Module-level imports of vue-i18n are fine, but the function directly touches window, localStorage, navigator, document. If ever used in SSR/SSG or tests (jsdom without storage), this will throw.

  7. MessageSchema['common'] for enUS/hantCommon type assertion – Type inference assumes other locales match zh-CN shape. If a translation file diverges (missing/extra keys), TypeScript won't catch it unless you explicitly type each import as MessageSchema['common']. Worth adding:

    import enCommonRaw from './en-US/common.json'
    const enCommon = enCommonRaw satisfies typeof zhCommon
  8. Listener added after watch with immediate: true – Order is fine, but note the initial language sync relies on user.language being set before mount ("bound before mount" comment acknowledges this). Consider asserting/warning if user.language is empty.

  9. Missing error feedback – normalizeLocale silently falls back; no logging when an unknown locale is persisted. Minor.

Optimization Suggestions

  • Extract the storage listener into a returned cleanup:
    function createAppI18n(user: LanguageState) {
      // ...
      const onStorage = (event: StorageEvent) => { /* ... */ }
      window.addEventListener('storage', onStorage)
      return {
        i18n,
        dispose: () => window.removeEventListener('storage', onStorage),
      }
    }
    Or wrap in effectScope() so consumers can call scope.stop().
  • Cache normalizeLocale(language) result to skip redundant watcher writes (if (i18n.global.locale.value === locale) return).
  • Use import.meta.env.SSR / typeof window guard for portability.
  • Consider lazy-loading zh-Hant messages (or splitting common translations) to reduce initial bundle — three JSON blobs load eagerly now.
  • Add JSDoc noting that user.setLanguage must persist to LANGUAGE_STORAGE_KEY, otherwise cross-tab sync breaks silently.

Minor

  • No blank-line/formatting issue. Comments and typing look clean.
  • globalInjection: false is good; ensure templates use useI18n() composition API consistently.

5 changes: 5 additions & 0 deletions ui/src/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"actions": { "goHome": "返回首页" },
"fields": { "language": { "label": "界面语言" } },
"errors": { "notFound": { "description": "页面不存在或已被删除" } }
}
5 changes: 5 additions & 0 deletions ui/src/locales/zh-Hant/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"actions": { "goHome": "返回首頁" },
"fields": { "language": { "label": "介面語言" } },
"errors": { "notFound": { "description": "頁面不存在或已被刪除" } }
}
7 changes: 4 additions & 3 deletions ui/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

import App from './App.vue'
import { configureMarkdownEditor } from '@/components/global/markdown-editor/config'
import router from '@/router/admin'
import { pinia } from '@/stores/admin'
import { pinia, useStore } from '@/stores/admin'
import { createAppI18n } from '@/locales'
import permission from '@/permission/install'
import '@/styles/tailwind.css'
import '@/styles/index.scss'
Expand All @@ -15,8 +15,9 @@ configureMarkdownEditor()
const app = createApp(App)

app.use(pinia)
app.use(createAppI18n(useStore().user))
app.use(router)
app.use(permission)
app.use(ElementPlus, { locale: zhCn })
app.use(ElementPlus)

app.mount('#app')
10 changes: 3 additions & 7 deletions ui/src/stores/admin/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import CurrentUserApi from '@/api/admin/auth/current-user'
import type { CurrentUserInfo } from '@/api/admin/auth/types'
import { ROLE_TYPE } from '@/api/enums'

const LANGUAGE_STORAGE_KEY = 'MaxKB-locale'

function getDefaultLanguage() {
return localStorage.getItem(LANGUAGE_STORAGE_KEY) || navigator.language || 'en-US'
}
import { getDefaultLanguage, LANGUAGE_STORAGE_KEY, normalizeLocale } from '@/locales/config'

interface UserState {
language: string
Expand Down Expand Up @@ -54,8 +50,8 @@ export const useUserStore = defineStore('user', {

/** 更新并持久化当前用户语言。 */
setLanguage(language: string) {
this.language = language
localStorage.setItem(LANGUAGE_STORAGE_KEY, language)
this.language = normalizeLocale(language)
localStorage.setItem(LANGUAGE_STORAGE_KEY, this.language)
},
},
})
Loading
Loading