diff --git a/packages/config-provider/__tests__/config-provider.spec.ts b/packages/config-provider/__tests__/config-provider.spec.ts new file mode 100644 index 0000000000..5ed1b35ce3 --- /dev/null +++ b/packages/config-provider/__tests__/config-provider.spec.ts @@ -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(English) + const oppositeLocale = ref(Chinese) + return { + currentLocale, + oppositeLocale, + toEn() { + currentLocale.value = English + oppositeLocale.value = Chinese + }, + toZh() { + currentLocale.value = Chinese + oppositeLocale.value = English + }, + } + }, + template: ` + + + + + + + + + + `, + }) + }) + + 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, + ) + }) + }) +}) diff --git a/packages/config-provider/config-provider.ts b/packages/config-provider/config-provider.ts new file mode 100644 index 0000000000..2891f052fd --- /dev/null +++ b/packages/config-provider/config-provider.ts @@ -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() + }, +}) + diff --git a/packages/config-provider/index.ts b/packages/config-provider/index.ts new file mode 100644 index 0000000000..bb82b655c6 --- /dev/null +++ b/packages/config-provider/index.ts @@ -0,0 +1,4 @@ +import withInstall from '@element-plus/utils/with-install' +import { ConfigProvider } from './config-provider' + +export default withInstall(ConfigProvider) diff --git a/packages/config-provider/package.json b/packages/config-provider/package.json new file mode 100644 index 0000000000..701d5c839a --- /dev/null +++ b/packages/config-provider/package.json @@ -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" + } +} diff --git a/packages/element-plus/components.ts b/packages/element-plus/components.ts index b9ee8300a6..8d63d78a9e 100644 --- a/packages/element-plus/components.ts +++ b/packages/element-plus/components.ts @@ -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' diff --git a/packages/element-plus/composables.ts b/packages/element-plus/composables.ts new file mode 100644 index 0000000000..59d941acca --- /dev/null +++ b/packages/element-plus/composables.ts @@ -0,0 +1 @@ +export * from '@element-plus/hooks' diff --git a/packages/element-plus/index.ts b/packages/element-plus/index.ts index ef12500c2f..8d208ff8d4 100644 --- a/packages/element-plus/index.ts +++ b/packages/element-plus/index.ts @@ -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 diff --git a/packages/hooks/__tests__/use-locale.spec.ts b/packages/hooks/__tests__/use-locale.spec.ts new file mode 100644 index 0000000000..c31f489235 --- /dev/null +++ b/packages/hooks/__tests__/use-locale.spec.ts @@ -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() + }) +}) diff --git a/packages/hooks/index.ts b/packages/hooks/index.ts index 3c1ebcd382..03bcb5b3eb 100644 --- a/packages/hooks/index.ts +++ b/packages/hooks/index.ts @@ -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' diff --git a/packages/hooks/use-locale/index.ts b/packages/hooks/use-locale/index.ts new file mode 100644 index 0000000000..ee76cab6f5 --- /dev/null +++ b/packages/hooks/use-locale/index.ts @@ -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, + }, + i18n: { + type: Function as PropType, + }, +} + +type Translator = (...args: any[]) => string + +export type LocaleContext = { + locale: Language + lang: string + t: Translator +} + +export const LocaleInjectionKey = 'ElLocaleInjection' as unknown as InjectionKey + +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) diff --git a/packages/utils/types.ts b/packages/utils/types.ts index 116e619a46..fe26b2a729 100644 --- a/packages/utils/types.ts +++ b/packages/utils/types.ts @@ -1,4 +1,4 @@ -import type { App } from 'vue' +import type { Plugin } from 'vue' type OptionalKeys> = { [K in keyof T]: T extends Record @@ -19,6 +19,6 @@ export type AnyFunction = (...args: any[]) => T export type PartialReturnType unknown> = Partial> -export type SFCWithInstall = T & { install(app: App): void; } +export type SFCWithInstall = T & Plugin export type RefElement = Nullable diff --git a/packages/utils/with-install.ts b/packages/utils/with-install.ts new file mode 100644 index 0000000000..a3040ad414 --- /dev/null +++ b/packages/utils/with-install.ts @@ -0,0 +1,13 @@ +import type { App, DefineComponent } from 'vue' +import type { SFCWithInstall } from './types' + +/* istanbul ignore next */ +export default (component: T) => { + (component as any).install = (app: App) => { + app.component( + ((component as unknown) as DefineComponent).name, + component, + ) + } + return component as SFCWithInstall +}