mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
feat(locale): locale-provider (#2662)
- Add a component for providing configs - Add `use-locale` hooks into hooks for composable usage - Add `use-locale` for `config-provider` to allow user to provide locale config via `react-like` API - Add test cases against these new features
This commit is contained in:
parent
b609145f6a
commit
c8d65bf43d
90
packages/config-provider/__tests__/config-provider.spec.ts
Normal file
90
packages/config-provider/__tests__/config-provider.spec.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { h, ref, inject } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { LocaleInjectionKey } from '@element-plus/hooks'
|
||||
import Chinese from '@element-plus/locale/lang/zh-cn'
|
||||
import English from '@element-plus/locale/lang/en'
|
||||
import { ConfigProvider } from '../config-provider'
|
||||
|
||||
import type { Language } from '@element-plus/locale'
|
||||
|
||||
const TestComp = {
|
||||
setup() {
|
||||
const locale = inject(LocaleInjectionKey)
|
||||
return () => {
|
||||
return h('div', locale.t('el.popconfirm.confirmButtonText'))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
describe('config-provider', () => {
|
||||
describe('locale-provider', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount({
|
||||
components: {
|
||||
'el-test': TestComp,
|
||||
[ConfigProvider.name]: ConfigProvider,
|
||||
},
|
||||
setup() {
|
||||
const currentLocale = ref<Language>(English)
|
||||
const oppositeLocale = ref<Language>(Chinese)
|
||||
return {
|
||||
currentLocale,
|
||||
oppositeLocale,
|
||||
toEn() {
|
||||
currentLocale.value = English
|
||||
oppositeLocale.value = Chinese
|
||||
},
|
||||
toZh() {
|
||||
currentLocale.value = Chinese
|
||||
oppositeLocale.value = English
|
||||
},
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<el-config-provider :locale="currentLocale">
|
||||
<el-test class="current-locale" />
|
||||
<el-config-provider :locale="oppositeLocale">
|
||||
<el-test class="opposite-locale" />
|
||||
</el-config-provider>
|
||||
</el-config-provider>
|
||||
|
||||
<button @click="toEn" class="to-en">toEn</button>
|
||||
<button @click="toZh" class="to-zh">toZh</button>
|
||||
`,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should provide locale properly', async () => {
|
||||
expect(wrapper.find('.current-locale').text()).toBe(
|
||||
English.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
expect(wrapper.find('.opposite-locale').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
})
|
||||
|
||||
it('should reactively update the text on page', async () => {
|
||||
expect(wrapper.find('.current-locale').text()).toBe(
|
||||
English.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
expect(wrapper.find('.opposite-locale').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
|
||||
await wrapper.find('.to-zh').trigger('click')
|
||||
|
||||
expect(wrapper.find('.current-locale').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
expect(wrapper.find('.opposite-locale').text()).toBe(
|
||||
English.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
19
packages/config-provider/config-provider.ts
Normal file
19
packages/config-provider/config-provider.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import { useLocaleProps, useLocale } from '@element-plus/hooks'
|
||||
|
||||
export const ConfigProvider = defineComponent({
|
||||
name: 'ElConfigProvider',
|
||||
props: {
|
||||
...useLocaleProps,
|
||||
// Add more configs
|
||||
},
|
||||
|
||||
setup(_, { slots }) {
|
||||
|
||||
useLocale()
|
||||
|
||||
return () => slots.default()
|
||||
},
|
||||
})
|
||||
|
4
packages/config-provider/index.ts
Normal file
4
packages/config-provider/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import withInstall from '@element-plus/utils/with-install'
|
||||
import { ConfigProvider } from './config-provider'
|
||||
|
||||
export default withInstall(ConfigProvider)
|
12
packages/config-provider/package.json
Normal file
12
packages/config-provider/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/config-provider",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.3"
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ export { default as ElCollapseItem } from '@element-plus/collapse-item'
|
||||
export { default as ElCollapseTransition } from '@element-plus/collapse-transition'
|
||||
export { default as ElColorPicker } from '@element-plus/color-picker'
|
||||
export { default as ElContainer } from '@element-plus/container'
|
||||
export { default as ElConfigProvider } from '@element-plus/config-provider'
|
||||
export { default as ElDatePicker } from '@element-plus/date-picker'
|
||||
export { default as ElDialog } from '@element-plus/dialog'
|
||||
export { default as ElDivider } from '@element-plus/divider'
|
||||
|
1
packages/element-plus/composables.ts
Normal file
1
packages/element-plus/composables.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@element-plus/hooks'
|
@ -14,6 +14,7 @@ import type { InstallOptions } from '@element-plus/utils/config'
|
||||
|
||||
export * from './components'
|
||||
export * from './plugins'
|
||||
export * from './composables'
|
||||
|
||||
type DWindow = Window & typeof globalThis & {
|
||||
dayjs?: typeof dayjs
|
||||
|
80
packages/hooks/__tests__/use-locale.spec.ts
Normal file
80
packages/hooks/__tests__/use-locale.spec.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { inject, h, nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Chinese from '@element-plus/locale/lang/zh-cn'
|
||||
import English from '@element-plus/locale/lang/en'
|
||||
import { useLocale, useLocaleProps, LocaleInjectionKey } from '../use-locale'
|
||||
|
||||
const TestComp = {
|
||||
setup() {
|
||||
const locale = inject(LocaleInjectionKey)
|
||||
return () => {
|
||||
return h('div', { class: 'locale-manifest' }, locale.t('el.popconfirm.confirmButtonText'))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
describe('use-locale', () => {
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
wrapper = mount({
|
||||
props: useLocaleProps,
|
||||
components: {
|
||||
'el-test': TestComp,
|
||||
},
|
||||
setup(_, { slots }) {
|
||||
useLocale()
|
||||
return () => slots.default()
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
locale: Chinese,
|
||||
},
|
||||
slots: {
|
||||
default: () => h(TestComp),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should provide locale correctly', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.find('.locale-manifest').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
})
|
||||
|
||||
it('should update the text reactively', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.find('.locale-manifest').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
await wrapper.setProps({
|
||||
locale: English,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.locale-manifest').text()).toBe(
|
||||
English.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
})
|
||||
|
||||
it('should be able to use external translator', async () => {
|
||||
await nextTick()
|
||||
expect(wrapper.find('.locale-manifest').text()).toBe(
|
||||
Chinese.el.popconfirm.confirmButtonText,
|
||||
)
|
||||
|
||||
const translator = jest.fn().mockImplementation(k => k)
|
||||
await wrapper.setProps({
|
||||
i18n: translator,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.locale-manifest').text()).toBe(
|
||||
'el.popconfirm.confirmButtonText',
|
||||
)
|
||||
expect(translator).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@ -12,3 +12,4 @@ export { default as useTimeout } from './use-timeout'
|
||||
export * from './use-model-toggle'
|
||||
export * from './use-popper'
|
||||
export * from './use-css-var'
|
||||
export * from './use-locale'
|
||||
|
83
packages/hooks/use-locale/index.ts
Normal file
83
packages/hooks/use-locale/index.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { getCurrentInstance, provide, reactive, watch } from 'vue'
|
||||
import English from '@element-plus/locale/lang/en'
|
||||
|
||||
import type { InjectionKey, PropType } from 'vue'
|
||||
import type { Language } from '@element-plus/locale'
|
||||
|
||||
export const useLocaleProps = {
|
||||
locale: {
|
||||
type: Object as PropType<Language>,
|
||||
},
|
||||
i18n: {
|
||||
type: Function as PropType<Translator>,
|
||||
},
|
||||
}
|
||||
|
||||
type Translator = (...args: any[]) => string
|
||||
|
||||
export type LocaleContext = {
|
||||
locale: Language
|
||||
lang: string
|
||||
t: Translator
|
||||
}
|
||||
|
||||
export const LocaleInjectionKey = 'ElLocaleInjection' as unknown as InjectionKey<LocaleContext>
|
||||
|
||||
export const useLocale = () => {
|
||||
const vm = getCurrentInstance()
|
||||
const props = vm.props as {
|
||||
locale: Language
|
||||
i18n: Translator
|
||||
}
|
||||
const localeContext = reactive({
|
||||
...props,
|
||||
lang: props.locale.name,
|
||||
t: translatorGenerator(props.locale, props.i18n),
|
||||
})
|
||||
|
||||
provide(LocaleInjectionKey, localeContext)
|
||||
|
||||
watch(() => props, ({ locale, i18n }) => {
|
||||
// dayjs.locale(locale.name)
|
||||
localeContext.locale = locale
|
||||
localeContext.lang = locale.name
|
||||
localeContext.t = translatorGenerator(locale, i18n)
|
||||
}, { deep: true })
|
||||
}
|
||||
|
||||
|
||||
function template(str: string, option) {
|
||||
if (!str || !option) return str
|
||||
|
||||
return str.replace(/\{(\w+)\}/g, (_, key) => {
|
||||
return option[key]
|
||||
})
|
||||
}
|
||||
|
||||
function translatorGenerator(
|
||||
locale: Language,
|
||||
externalTranslator?: Translator,
|
||||
): Translator {
|
||||
return (...args) => {
|
||||
const defaultTranslator = (...args: any[]) => {
|
||||
const [path, option] = args
|
||||
let value
|
||||
const array = path.split('.')
|
||||
let current = locale
|
||||
for (let i = 0, j = array.length; i < j; i++) {
|
||||
const property = array[i]
|
||||
value = current[property]
|
||||
if (i === j - 1) return template(value, option)
|
||||
if (!value) return ''
|
||||
current = value
|
||||
}
|
||||
}
|
||||
|
||||
if (externalTranslator) {
|
||||
return externalTranslator(...args) || defaultTranslator(...args)
|
||||
}
|
||||
return defaultTranslator(...args)
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultTranslator = translatorGenerator(English)
|
@ -1,4 +1,4 @@
|
||||
import type { App } from 'vue'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
type OptionalKeys<T extends Record<string, unknown>> = {
|
||||
[K in keyof T]: T extends Record<K, T[K]>
|
||||
@ -19,6 +19,6 @@ export type AnyFunction<T> = (...args: any[]) => T
|
||||
|
||||
export type PartialReturnType<T extends (...args: unknown[]) => unknown> = Partial<ReturnType<T>>
|
||||
|
||||
export type SFCWithInstall<T> = T & { install(app: App): void; }
|
||||
export type SFCWithInstall<T> = T & Plugin
|
||||
|
||||
export type RefElement = Nullable<HTMLElement>
|
||||
|
13
packages/utils/with-install.ts
Normal file
13
packages/utils/with-install.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { App, DefineComponent } from 'vue'
|
||||
import type { SFCWithInstall } from './types'
|
||||
|
||||
/* istanbul ignore next */
|
||||
export default <T>(component: T) => {
|
||||
(component as any).install = (app: App) => {
|
||||
app.component(
|
||||
((component as unknown) as DefineComponent).name,
|
||||
component,
|
||||
)
|
||||
}
|
||||
return component as SFCWithInstall<T>
|
||||
}
|
Loading…
Reference in New Issue
Block a user