element-plus源码解读5——国际化系统完整流程

0 阅读4分钟

element-plus的国际化:element-plus.org/zh-CN/guide…

语言包定义 → ConfigProvider 提供 → provide/inject 注入 → useLocale 获取 → translate 翻译 → 组件使用

一、语言包定义

image.png

image.png

Object.assign:Object.assign() - JavaScript | MDN

二、ConfigProvider 组件(配置提供者)

packages/components/config-provider/src/config-provider.ts

其中: renderSlot是vue3提供的函数,用于在渲染函数中渲染插槽内容

在.vue文件中
<template>
  <!-- 渲染默认插槽,并传递 config 数据 -->
  <slot :config="config" />
</template>
但在 .ts 文件中(使用渲染函数),没有 <template>,所以用 renderSlot 来渲染插槽:
return () => renderSlot(slots, 'default', { config: config?.value })
// 功能完全一样!

renderSlot(slots, 'default', { config: config?.value })
//          ↑      ↑         ↑
//          |      |         └─ 传递给插槽的数据(作用域插槽)
//          |      └─ 插槽名称('default' 是默认插槽)
//          └─ 插槽对象(从 setup 的 { slots } 获取)

el-config-provider 组件

  1. 统一配置:为被它包裹的所有子组件提供统一的配置(语言、尺寸、消息位置等)
  2. 避免重复:不需要在每个组件上单独传配置
const ConfigProvider = defineComponent({
  name: 'ElConfigProvider',
  props: configProviderProps,

  setup(props, { slots }) {
    // 将组件接收的props作为全局配置提供给所有子组件
    // 将配置通过 Vue 的 provide/inject 机制传递给所有子组件
    // 子组件通过 inject 获取配置
    const config = provideGlobalConfig(props)
    // 单独监听 message 配置的变化
    // 因为 ElMessage 是全局方法(不是组件),需要特殊处理
    // 将配置合并到全局的 messageConfig 对象中
    watch(
      () => props.message,
      (val) => {
        Object.assign(messageConfig, config?.value?.message ?? {}, val ?? {})
      },
      { immediate: true, deep: true }
    )
    // 渲染被包裹的子组件(<el-config-provider> 标签内的内容)
    return () => renderSlot(slots, 'default', { config: config?.value })
  },
})


<el-config-provider :size="large">
  <el-config-provider :locale="zhCn">
    <!-- 这里会同时使用 large 尺寸和中文 -->
  </el-config-provider>
</el-config-provider>

三、provideGlobalConfig(全局配置提供)

位置:packages/components/config-provider/src/hooks/use-global-config.ts

  • 通过 provide 将语言配置注入到 Vue 的依赖注入系统
  • 使用 localeContextKey 作为注入键
  • 使用 computed 保持响应式
/**
 * 从父级组件或全局获取配置
 * @param key 配置项的键
 * @param defaultValue 默认值
 * @returns 返回配置项的值
 */
export function useGlobalConfig(
  key?: keyof ConfigProviderContext,
  defaultValue = undefined
) {
  // 检查是否在setup中 
  // 在组件中 → 从父级获取配置 
  // 不在组件中 → 使用全局配置
  const config = getCurrentInstance()
    ? inject(configProviderContextKey, globalConfig)
    : globalConfig
  // 如果提供了键,返回计算属性,计算配置项的值
  if (key) {
    return computed(() => config.value?.[key] ?? defaultValue)
  } else {
    return config
  }
}

/**
 * 这个函数用于将配置通过vue的provide/inject机制传递给所有子组件
 * @param config 全局配置对象
 * @param app Vue 应用实例
 * @param global 是否是全局配置
 * @returns 返回配置对象
 */
export const provideGlobalConfig = (
  config: MaybeRef<ConfigProviderContext>,
  app?: App,
  global = false
) => {
  // 检查环境
  const inSetup = !!getCurrentInstance()
  // 如果有旧配置就获取
  const oldConfig = inSetup ? useGlobalConfig() : undefined

  // 确定使用哪个provide函数
  // 优先使用app.provide(应用级),否则在setup中用provide(组件级),都不满足则为undefined
  const provideFn = app?.provide ?? (inSetup ? provide : undefined)
  // 如果没有可用的 provide 函数,警告并退出
  if (!provideFn) {
    debugWarn(
      'provideGlobalConfig',
      'provideGlobalConfig() can only be used inside setup().'
    )
    return
  }

  const context = computed(() => {
    // 获取配置的实际值(如果是 ref)
    const cfg = unref(config)
    // 没有旧配置,直接返回新配置
    if (!oldConfig?.value) return cfg
    // 有旧配置,合并它
    return mergeConfig(oldConfig.value, cfg)
  })
  provideFn(configProviderContextKey, context)
  provideFn(
    localeContextKey,
    computed(() => context.value.locale)
  )

  if (global || !globalConfig.value) {
    globalConfig.value = context.value
  }
  return context
}

四、useLocale Hook(获取语言配置)

位置:packages/hooks/use-locale/index.ts

export const useLocale = (localeOverrides?: Ref<Language | undefined>) => {
  const locale = localeOverrides || inject(localeContextKey, ref())!
  return buildLocaleContext(computed(() => locale.value || English))
}

/**
 * 构建一个语言环境上下文对象,包含语言信息和翻译函数
 * @param locale
 * @returns
 */
export const buildLocaleContext = (
  locale: MaybeRef<Language>
): LocaleContext => {
  // 提取语言名称 如zh-cn、en,如果是ref则取值,否则直接使用
  const lang = computed(() => unref(locale).name)
  // 统一转换为响应式引用,如果已经是 ref,直接使用,如果不是,用 ref() 包装
  const localeRef = isRef(locale) ? locale : ref(locale)
  return {
    lang, // 语言名称(如 'zh-cn')
    locale: localeRef, // 完整的语言对象(响应式)
    t: buildTranslator(locale), // 翻译函数
  }
}

五、translate 函数(翻译核心)

位置:packages/hooks/use-locale/index.ts

/**
 * 根据路径从语言对象中获取翻译文本,并替换其中的占位符。
 * @param path 
 * @param option 
 * @param locale 
 * @returns 
 * 
 * translate('el.colorpicker.confirm', undefined, locale)
 * 步骤1: get(locale, 'el.colorpicker.confirm', 'el.colorpicker.confirm')
 *        → 找到 '确定'
 * 步骤2: '确定'.replace(/\{(\w+)\}/g, ...)
 *        → 没有占位符,直接返回 '确定'
 * 结果: '确定'
 */
export const translate = (
  path: LocaleKeys,
  option: undefined | TranslatorOption,
  locale: Language
): string =>
  (get(locale, path, path) as string).replace(
    /\{(\w+)\}/g,
    (_, key) => `${option?.[key] ?? `{${key}}`}`
  )

六、组件中使用

位置:packages/components/input-number/src/input-number.vue

const { t } = useLocale()
:aria-label="t('el.inputNumber.decrease')"

七、完整流程图

┌─────────────────────────────────────────────────────────────┐
│  1. 定义语言包                                               │
│     packages/locale/lang/zh-cn.ts                           │
│     { name: 'zh-cn', el: { inputNumber: { ... } } }        │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  2. 用户使用 ConfigProvider                                  │
│     <el-config-provider :locale="zhCn">                     │
│       <el-input-number />                                   │
│     </el-config-provider>                                   │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  3. ConfigProvider 调用 provideGlobalConfig                 │
│     provideGlobalConfig(props)                              │
│     ↓                                                        │
│     provideFn(localeContextKey, computed(() => locale))     │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  4. 子组件通过 useLocale 获取                                │
│     const { t } = useLocale()                               │
│     ↓                                                        │
│     inject(localeContextKey) → 获取语言包                   │
│     ↓                                                        │
│     buildLocaleContext() → 构建翻译函数                     │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  5. 使用 t() 函数翻译                                        │
│     t('el.inputNumber.decrease')                            │
│     ↓                                                        │
│     translate() → 从语言包中查找                            │
│     ↓                                                        │
│     返回: '减少' (中文) 或 'Decrease' (英文)                │
└─────────────────────────────────────────────────────────────┘