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:
jeremywu 2021-07-25 15:26:00 +08:00 committed by GitHub
parent b609145f6a
commit c8d65bf43d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 307 additions and 2 deletions

View 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,
)
})
})
})

View 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()
},
})

View File

@ -0,0 +1,4 @@
import withInstall from '@element-plus/utils/with-install'
import { ConfigProvider } from './config-provider'
export default withInstall(ConfigProvider)

View 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"
}
}

View File

@ -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'

View File

@ -0,0 +1 @@
export * from '@element-plus/hooks'

View File

@ -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

View 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()
})
})

View File

@ -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'

View 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)

View File

@ -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>

View 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>
}