diff --git a/change/@acedatacloud-nexior-5b01d761-ee5b-4ae1-b0dd-c4f4171d87ec.json b/change/@acedatacloud-nexior-5b01d761-ee5b-4ae1-b0dd-c4f4171d87ec.json new file mode 100644 index 00000000..08f7ca73 --- /dev/null +++ b/change/@acedatacloud-nexior-5b01d761-ee5b-4ae1-b0dd-c4f4171d87ec.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "refine i18n support", + "packageName": "@acedatacloud/nexior", + "email": "cqc@cuiqingcai.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index 56384030..55a8bdfe 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "scripts": { "dev": "vite --host", "build": "npm run build:web", - "build:web": "cross-env VITE_SURFACE=web vite build", + "build:web": "cross-env VITE_SURFACE=web vue-tsc -b && vite build", "build:android": "cross-env VITE_SURFACE=android vite build", "build:ios": "cross-env VITE_SURFACE=ios vite build", "pack:android": "npm run build:android && npx cap copy android", "pack:ios": "npm run build:ios && npx cap copy ios", - "serve": "vite preview", + "preview": "vite preview", "translate": "transmart", "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx", "prettier": "prettier --write .", @@ -64,7 +64,7 @@ "uuid": "^8.3.2", "vue": "^3.5.17", "vue-codemirror": "^6.1.1", - "vue-i18n": "^9.1.7", + "vue-i18n": "^11.1.11", "vue-qrcode": "^2.2.0", "vue-router": "^4.5.1", "vuex": "^4.1.0", diff --git a/src/App.vue b/src/App.vue index 7d7a8b62..550ee77c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,7 @@ import { defineComponent } from 'vue'; import { ElConfigProvider, ElTag } from 'element-plus'; import AuthPanel from './components/common/AuthPanel.vue'; -import locale from 'element-plus/dist/locale/zh-cn.mjs'; +import locale from 'element-plus/es/locale/lang/en'; import { isTest } from '@/constants/endpoint'; export default defineComponent({ diff --git a/src/components/common/LocaleSelector.vue b/src/components/common/LocaleSelector.vue deleted file mode 100644 index 7ac1cc97..00000000 --- a/src/components/common/LocaleSelector.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/src/components/order/WechatPay.vue b/src/components/order/WechatPay.vue index 8451c14a..825f5177 100644 --- a/src/components/order/WechatPay.vue +++ b/src/components/order/WechatPay.vue @@ -3,7 +3,14 @@
- +

{{ $t('order.message.wechatPayHelp') }}

diff --git a/src/components/user/Locale.vue b/src/components/user/Locale.vue index d4363d50..d5720353 100644 --- a/src/components/user/Locale.vue +++ b/src/components/user/Locale.vue @@ -18,12 +18,13 @@ import { defineComponent } from 'vue'; import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElIcon } from 'element-plus'; import { ArrowDown } from '@element-plus/icons-vue'; -import { SUPPORTED_LOCALES, setI18nLanguage } from '@/i18n'; import { setCookie } from 'typescript-cookie'; import { getDomain } from '@/utils/initializer'; +import { I18N_SUPPORTED_LOCALES } from '@/constants/i18n'; +import { setI18nLanguage } from '@/i18n'; export default defineComponent({ - name: 'LocaleSelector', + name: 'Locale', components: { ElDropdown, ElDropdownMenu, @@ -33,7 +34,7 @@ export default defineComponent({ }, data() { return { - locales: SUPPORTED_LOCALES + locales: I18N_SUPPORTED_LOCALES }; }, computed: { diff --git a/src/constants/i18n.ts b/src/constants/i18n.ts new file mode 100644 index 00000000..6d3f8e35 --- /dev/null +++ b/src/constants/i18n.ts @@ -0,0 +1,48 @@ +export const I18N_DEFAULT_LOCALE = 'en'; + +export const I18N_SUPPORTED_LOCALES = [ + { value: 'en', label: 'English' }, + { value: 'de', label: 'Deutsch' }, + { value: 'pt', label: 'Português' }, + { value: 'es', label: 'Español' }, + { value: 'fr', label: 'Français' }, + { value: 'zh-CN', label: '简体中文' }, + { value: 'zh-TW', label: '繁體中文' }, + { value: 'it', label: 'Italiano' }, + { value: 'ko', label: '한국어' }, + { value: 'ja', label: '日本語' }, + { value: 'ru', label: 'Русский' }, + { value: 'pl', label: 'Polski' }, + { value: 'fi', label: 'Suomi' }, + { value: 'sv', label: 'Svenska' }, + { value: 'el', label: 'Ελληνικά' }, + { value: 'uk', label: 'Українська' }, + { value: 'ar', label: 'العربية' }, + { value: 'sr', label: 'Српски' } +]; + +export const I18N_SCOPES = [ + 'index', + 'api', + 'application', + 'qrart', + 'luma', + 'pika', + 'kling', + 'flux', + 'hailuo', + 'headshots', + 'suno', + 'common', + 'console', + 'credential', + 'distribution', + 'order', + 'proxy', + 'service', + 'usage', + 'user', + 'site', + 'chat', + 'midjourney' +]; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index ff8090d3..36047ead 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,56 +1,16 @@ -import { createI18n, I18n } from 'vue-i18n'; -import { nextTick } from 'vue'; +import { createI18n } from 'vue-i18n'; +import { I18N_DEFAULT_LOCALE, I18N_SCOPES, I18N_SUPPORTED_LOCALES } from '@/constants/i18n'; import axios from 'axios'; +import { nextTick } from 'vue'; -declare namespace Intl { - // eslint-disable-next-line no-unused-vars - function getCanonicalLocales(locales: string | string[] | undefined): string[]; -} - -export const DEFAULT_LOCALE = 'en'; - -export const SUPPORTED_LOCALES = [ - { value: 'en', label: 'English' }, - { value: 'de', label: 'Deutsch' }, - { value: 'pt', label: 'Português' }, - { value: 'es', label: 'Español' }, - { value: 'fr', label: 'Français' }, - { value: 'zh-CN', label: '简体中文' }, - { value: 'zh-TW', label: '繁體中文' }, - { value: 'it', label: 'Italiano' }, - { value: 'ko', label: '한국어' }, - { value: 'ja', label: '日本語' }, - { value: 'ru', label: 'Русский' }, - { value: 'pl', label: 'Polski' }, - { value: 'fi', label: 'Suomi' }, - { value: 'sv', label: 'Svenska' }, - { value: 'el', label: 'Ελληνικά' }, - { value: 'uk', label: 'Українська' }, - { value: 'ar', label: 'العربية' }, - { value: 'sr', label: 'Српски' } -]; - -export const setupI18n = () => { - // @ts-ignore - const i18n: I18n = createI18n({}); - return i18n; -}; - -export const i18n = setupI18n(); - -export const loadLocalResource = async (name: string, locale: string) => { - try { - const module = await import(`./${locale}/${name}.json`); - return module.default; - } catch (error) { - const module = await import(`./${DEFAULT_LOCALE}/${name}.json`); - return module.default; - } -}; +export const i18n = createI18n({ + legacy: true +}); export const getLocale = (lang?: string): string => { const canonicalLang = Intl.getCanonicalLocales(lang || navigator.language)?.[0]; - const supportedLocales = SUPPORTED_LOCALES.map((locale) => locale.value); + console.debug('canonicalLang', canonicalLang); + const supportedLocales = I18N_SUPPORTED_LOCALES.map((locale) => locale.value); // if the canonical language is supported, use it if (canonicalLang && supportedLocales.includes(canonicalLang)) { console.debug('canonicalLang', canonicalLang); @@ -63,41 +23,35 @@ export const getLocale = (lang?: string): string => { return canonicalLangPrefix; } } - return DEFAULT_LOCALE; + return I18N_DEFAULT_LOCALE; }; -export const loadLocaleMessages = async (i18n: I18n, locale: string) => { - const names = [ - 'index', - 'api', - 'application', - 'qrart', - 'luma', - 'pika', - 'kling', - 'flux', - 'hailuo', - 'headshots', - 'suno', - 'common', - 'console', - 'credential', - 'distribution', - 'order', - 'proxy', - 'service', - 'usage', - 'user', - 'site', - 'chat', - 'midjourney' - ]; +const messageLoaders = import.meta.glob('./**/*.json'); - const promises = names.map((name) => loadLocalResource(name, locale)); +export const loadLocaleResource = async (name: string, locale: string) => { + console.debug('loadLocaleResource', name, locale); + const path = `./${locale}/${name}.json`; + const loader = messageLoaders[path]; + if (!loader) { + console.warn(`[i18n] Missing translation file: ${path}`); + return; + } + + const module: any = await loader(); + console.debug('loaded module', module); + if (!module.default) { + console.warn(`[i18n] Missing default export in translation file: ${path}`); + return; + } + return module.default; +}; + +export const loadLocaleMessages = async (locale: string) => { + const promises = I18N_SCOPES.map((name) => loadLocaleResource(name, locale)); const resources = await Promise.all(promises); const messages: any = {}; - names.forEach((name, index) => { + I18N_SCOPES.forEach((name, index) => { const resource = resources[index]; for (const key in resource) { if (resource.hasOwnProperty(key)) { @@ -116,7 +70,7 @@ export const loadLocaleMessages = async (i18n: I18n, locale: string) => { export const setI18nLanguage = async (locale: string) => { // load locale messages if (!i18n.global.availableLocales.includes(locale)) { - await loadLocaleMessages(i18n, locale); + await loadLocaleMessages(locale); } // set global locale diff --git a/src/pages/chat/Conversation.vue b/src/pages/chat/Conversation.vue index bfa9bc4c..1e2e42fd 100644 --- a/src/pages/chat/Conversation.vue +++ b/src/pages/chat/Conversation.vue @@ -305,7 +305,6 @@ export default defineComponent({ }, async onRestoreConversation(id: string) { console.debug('onRestoreConversation id', id); - this.$router.push({ ...this.$route, params: { id } }); const conversation = this.conversations?.find((conversation: IChatConversation) => conversation.id === id); // change the model and model group const model = conversation?.model; diff --git a/src/pages/distribution/Index.vue b/src/pages/distribution/Index.vue index bcd2e504..a45d7927 100644 --- a/src/pages/distribution/Index.vue +++ b/src/pages/distribution/Index.vue @@ -152,6 +152,8 @@ :size="180" :margin="2" class="block mb-2 ml-auto mr-auto" + type="image/png" + :color="{ dark: '#000000', light: '#ffffff' }" />

{{ $t('distribution.message.distributionQrDescription') }} diff --git a/src/pages/profile/Index.vue b/src/pages/profile/Index.vue index 8c55c464..41a9870b 100644 --- a/src/pages/profile/Index.vue +++ b/src/pages/profile/Index.vue @@ -41,7 +41,6 @@ import { } from '@/router'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { getBaseUrlAuth, getBaseUrlPlatform } from '@/utils'; -import LocaleSelector from '@/components/common/LocaleSelector.vue'; import HelpDialog from '@/components/common/HelpDialog.vue'; interface ILink { @@ -58,8 +57,7 @@ export default defineComponent({ components: { ElImage, HelpDialog, - FontAwesomeIcon, - LocaleSelector + FontAwesomeIcon }, data() { return { diff --git a/src/router/index.ts b/src/router/index.ts index 09442dd6..c10f02e8 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -18,8 +18,9 @@ import site from './site'; import profile from './profile'; import { ROUTE_CHATGPT_CONVERSATION_NEW } from './constants'; -import { DEFAULT_LOCALE, setI18nLanguage } from '@/i18n'; import { getCookie } from 'typescript-cookie'; +import { I18N_DEFAULT_LOCALE } from '@/constants/i18n'; +import { getLocale, setI18nLanguage } from '@/i18n'; const routes = [ { @@ -51,7 +52,7 @@ const router = createRouter({ }); router.beforeEach(async (_to, _from, next) => { - const locale = getCookie('LOCALE') || DEFAULT_LOCALE; + const locale = getLocale(getCookie('LOCALE') || I18N_DEFAULT_LOCALE); await setI18nLanguage(locale); return next(); }); diff --git a/src/shims.d.ts b/src/shims.d.ts index 92081c14..0a3c025c 100644 --- a/src/shims.d.ts +++ b/src/shims.d.ts @@ -6,7 +6,7 @@ import { ComponentCustomProperties } from 'vue'; import { Router } from 'vue-router'; -import { I18n } from 'vue-i18n'; +import { I18n, VueI18n } from 'vue-i18n'; import { Store } from 'vuex'; import { IRootState } from './store/common/models'; import type { Router, RouteLocationNormalizedLoaded } from 'vue-router'; @@ -17,10 +17,7 @@ declare module '*.ico'; declare module '*.css'; declare module '*.scss'; declare module '*.png'; -declare module '*.tpl' { - const value: string; - export default value; -} +declare module '*.tpl'; declare module '*.vue' { import { DefineComponent } from 'vue'; @@ -28,9 +25,14 @@ declare module '*.vue' { export default component; } +// declare namespace Intl { +// function getCanonicalLocales(locales: string | string[] | undefined): string[]; +// } + declare module '@vue/runtime-core' { interface ComponentCustomProperties { - $t: (key: string, ...args: unknown[]) => string; + $t: VueI18n['t']; + $i18n: VueI18n; $store: Store; $router: Router; $route: RouteLocationNormalizedLoaded; diff --git a/src/utils/initializer.ts b/src/utils/initializer.ts index 6c1884d0..aa749891 100644 --- a/src/utils/initializer.ts +++ b/src/utils/initializer.ts @@ -1,12 +1,12 @@ import { getCookie, setCookie } from 'typescript-cookie'; import favicon from '@/assets/images/favicon.ico'; -import { getLocale } from '@/i18n'; import { applyTheme } from './theme'; import store from '@/store'; import { IToken } from '@/models'; import { parse } from 'psl'; import { BASE_HOST_HUB, LOCALE_CURRENCY_MAPPING } from '@/constants'; import { isOfficial, isSubOfficial, isWechatBrowser } from './is'; +import { getLocale } from '@/i18n'; export const getDomain = (host: string = window.location.hostname) => { const parsed = parse(host); diff --git a/yarn.lock b/yarn.lock index bf279777..d09c1999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -503,26 +503,26 @@ resolved "https://registry.yarnpkg.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz#818c048100401620e8112beb2636f10171b195cc" integrity sha512-+QklF255wkfBOabY+xw6FAI0Bwln/RhdwCunNy/9sKdKuChtaU67QZqU67KGAvZUTeeBgsL+yaHHxqfQeGZXEQ== -"@intlify/core-base@9.14.5": - version "9.14.5" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.14.5.tgz#cfed631e98b34f4f6fe1312e6c02dd8ace180197" - integrity sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA== +"@intlify/core-base@11.1.11": + version "11.1.11" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.11.tgz#e36893a7d37a3a75fae30977fc58c1d8cf3c853f" + integrity sha512-1Z0N8jTfkcD2Luq9HNZt+GmjpFe4/4PpZF3AOzoO1u5PTtSuXZcfhwBatywbfE2ieB/B5QHIoOFmCXY2jqVKEQ== dependencies: - "@intlify/message-compiler" "9.14.5" - "@intlify/shared" "9.14.5" + "@intlify/message-compiler" "11.1.11" + "@intlify/shared" "11.1.11" -"@intlify/message-compiler@9.14.5": - version "9.14.5" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.14.5.tgz#03b0a358c93cb2eed95936531c8a5871fd3841b0" - integrity sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ== +"@intlify/message-compiler@11.1.11": + version "11.1.11" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.11.tgz#ba10641f86af0e991ac9def0385bd345c8f150fb" + integrity sha512-7PC6neomoc/z7a8JRjPBbu0T2TzR2MQuY5kn2e049MP7+o32Ve7O8husylkA7K9fQRe4iNXZWTPnDJ6vZdtS1Q== dependencies: - "@intlify/shared" "9.14.5" + "@intlify/shared" "11.1.11" source-map-js "^1.0.2" -"@intlify/shared@9.14.5": - version "9.14.5" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.14.5.tgz#2e42193b8356db20ee28c15d1ee6c8f0fda6c0b9" - integrity sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ== +"@intlify/shared@11.1.11": + version "11.1.11" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.11.tgz#6bba3b86617c05767356e4ca939c9e300563a083" + integrity sha512-RIBFTIqxZSsxUqlcyoR7iiC632bq7kkOwYvZlvcVObHfrF4NhuKc4FKvu8iPCrEO+e3XsY7/UVpfgzg+M7ETzA== "@ionic/cli-framework-output@^2.2.5": version "2.2.8" @@ -5541,13 +5541,13 @@ vue-eslint-parser@^8.0.1: lodash "^4.17.21" semver "^7.3.5" -vue-i18n@^9.1.7: - version "9.14.5" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.14.5.tgz#72fbf4384b83a1c59ec9e01ff5d30224bd9150cf" - integrity sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g== +vue-i18n@^11.1.11: + version "11.1.11" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.11.tgz#b38ed214896540cf7a68932dfa565d9d4fbbffac" + integrity sha512-LvyteQoXeQiuILbzqv13LbyBna/TEv2Ha+4ZWK2AwGHUzZ8+IBaZS0TJkCgn5izSPLcgZwXy9yyTrewCb2u/MA== dependencies: - "@intlify/core-base" "9.14.5" - "@intlify/shared" "9.14.5" + "@intlify/core-base" "11.1.11" + "@intlify/shared" "11.1.11" "@vue/devtools-api" "^6.5.0" vue-qrcode@^2.2.0: