Skip to content

Latest commit

 

History

History
496 lines (424 loc) · 11.3 KB

025. i18n.md

File metadata and controls

496 lines (424 loc) · 11.3 KB

概述

i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是 “国际化”的简称。

对于一些跨国项目来说,国际化是尤为重要的,那什么是国际化呢❓国际化的意思就是将我们写的项目,能够根据不同国家的语言,进行翻译,进行切换,方便不同国家的客户使用。

本文示例基于vue-i18n 实现,「vue-i18n」顾名思义就是 Vue.js 的国际化插件, 它可以轻松地将一些本地化功能集成到你的 Vue.js 应用程序中。

基本思路:

  1. 定义语言包:需要几种语言展示,就定义几个语言包
  2. 创建对象,对语言包进行整合,对象的 key 为语言包的引用,值为语言包对象
  3. 创建 vue-i18n 类的对象,同时为其 messages 属性为第②步创建的对象,为其 locale 属性赋值为第②步中语言对象对应的 key
  4. 在创建 Vue 实例对象时,挂载 vue-i18n 类的对象

开始

开始之前,我们先来看一下最终实现的示例效果:

示例效果

示例代码相关版本:

  • vue:3.x
  • vue-i18n:9.x
  • 示例使用 Composition API.

创建项目

$ npm create vite@latest i18n-learns -- --template vue-ts
$ cd i18n-learns && npm install vue-i18n

配置i18n

在 src 目录中创建 languages 目录管理语言包:

.
├── /src  
	  ├──	/components
	      └── /PickerLanguage → 选择语言组件
	  	  		└── index.vue 
	  ├── /languages → i18n相关配置
		    ├── en-US.ts 
		    ├── es.ts
		    ├── pl.ts    
		    ├── pt.ts
		    ├── zh-CN.ts
		    ├── i18n.ts
		    └── index.ts
	  ├──	main.ts
├── ....

其中,

  • PickerLanguage:封装的切换语言的组件

  • en-US / es / pl / pt / zh-CN:语言包文件,示例罗列了5种语言,当然你可以根据你的需求来。

  • i18n:构建 i18n 实例

  • index:合成语言包

各文件代码:

zh-CN.ts

// 👉 简体中文 
export default {
  message: {
    hello: '你好,世界!',
  },
};

en-US.ts

// 👉 英语
export default {
  message: {
    hello: 'Hello, World!',
  },
};

es.ts

// 👉 西班牙语 
export default {
  message: {
    hello: '¡Hola Mundo!',
  },
};

pl.ts

// 👉 波兰语 
export default {
  message: {
    hello: 'Witaj świecie!',
  },
};

pt.ts

// 👉 葡萄牙语 
export default {
  message: {
    hello: 'Olá Mundo!',
  },
};

index.ts

import en from './en-US';
import zh from './zh-CN';
import pl from './pl';
import pt from './pt';
import es from './es';

export default { en, zh, pl, pt, es };

i18n.ts

import { createI18n } from 'vue-i18n';
import languages from './index';

/**
 * 获取本地语言
 * -- 首先判断本地是否记录用户上次切换的语言,如果没有,则获取浏览器首选语言
 * -- 否则使用默认语言 ‘en-US’
 */
export const getLocaleLanguage = (
  options: {
    /** 默认语言:en-US */
    defaultLang?: string;
    /** 是否需要完整格式:语言-国家 → zh-CN */
    isIntegrated?: boolean;
  } = {}
): string => {
  // -- 解构参数
  const { defaultLang = 'en-US', isIntegrated = false } = options;
  // -- 读取本地存储的数据(本地存储时必须存储完整格式「语言-国家」)
  const lanForStorage = localStorage.getItem('i18n-Lang');
  // -- 读取浏览器首选语言
  const lanForNavigator = navigator.language;
  // -- 临时语言
  let lanForTemp = lanForStorage || lanForNavigator;
  // -- 校验i18n是否配置该语言
  // -- 由于本地存储和默认获取的是完整的「语言-国家」代码,如“zh-CN”
  // -- 而i18n配置时使用的是「语言」代码,如“zh”
  // -- 所以需截取前面两个字符(即语言代码)来校验
  const isSupported = Object.keys(languages).includes(lanForTemp.slice(0, 2));
  // -- 条件返回所需代码
  if (isSupported) {
    return isIntegrated ? lanForTemp : lanForTemp.slice(0, 2);
  }
  return isIntegrated ? defaultLang : defaultLang.slice(0, 2);
};

// -- 注册i18n实例并引入语言文件
const i18n = createI18n({
  legacy: false,
  locale: getLocaleLanguage(),
  messages: languages,
});

export default i18n;

在应用程序中集成 i18n

import { createApp } from 'vue';
import App from '@/App.vue';
import i18n from '@/languages/i18n';

const app = createApp(App);
app.use(i18n);

app.mount('#app');

在应用程序中使用 i18n

👉 组件封装

封装 PickerLanguage 便于切换语言。

提示: 你不必关心组件的具体实现,直接使用即可,当然,为了测试,你也可以随便创建几个按钮实现类似的需求。

代码如下:

<script setup lang="ts">
import { reactive, onMounted } from 'vue';

export interface LangProps {
  label: string /** 语言描述,如“简体中文” */;
  value: string /** 语言代码,如“zh-CN” */;
  icon: string /** 国旗图标 */;
}

interface IProps {
  langs: Array<LangProps> /** 国际化语言列表,用于在菜单栏展示 */;
  defaultValue?: string /** 默认选中的语言代码,支持「语言」/「语言-国家」两种形式,不传则取langs[0] */;
  alignment?: 'left' | 'right' /** 菜单栏出相对于容器的对齐方向 */;
}

interface IState {
  checked: LangProps | null /** 记录当前选中的语言对象 */;
  maskIsOpen: boolean /** 菜单栏显示状态 */;
}

// -- props
const props = withDefaults(defineProps<IProps>(), {
  alignment: 'left',
});
const emits = defineEmits<{
  (e: 'change', lang: LangProps): void;
}>();

// -- state
const state = reactive<IState>({
  checked: null,
  maskIsOpen: false,
});

// -- life circles
onMounted(() => {
  /** 判断defaultValue是否传递,没有传递则取langs第一项 */
  let defaultLang = props.defaultValue
    ? props.langs.find((lang) => {
        return new RegExp(props.defaultValue as string).test(lang.value);
      })
    : undefined;
  state.checked = defaultLang ?? props.langs[0];
});

// -- events
const onLangItemTap = (lang: LangProps) => {
  emits('change', lang);
  state.checked = lang;
  state.maskIsOpen = false;
};
</script>

<template>
  <div class="lg-langs">
    <div class="current" @click="state.maskIsOpen = !state.maskIsOpen">
      <template v-if="state.checked">
        <img :src="state.checked.icon" />
        <span>{{ state.checked.label }}</span>
      </template>
      <b v-else>Languages__</b>
    </div>
    <div :class="['mask', { open: state.maskIsOpen }, props.alignment]">
      <div class="langs-list">
        <template v-for="(item, index) in props.langs" :key="index">
          <div
            :class="['item', { checked: item.value === state.checked?.value }]"
            @click="onLangItemTap(item)"
          >
            <div class="wrap">
              <img :src="item.icon" />
              <span>{{ item.label }}</span>
            </div>
          </div>
        </template>
      </div>
      <div class="close" @click="state.maskIsOpen = false" />
    </div>
  </div>
</template>

<style lang="less" scoped>
.lg-langs {
  width: auto;
  font-size: 15px;
  color: #333;
  position: relative;
  img {
    width: 30px;
    height: 20px;
    margin-right: 6px;
  }
  .current {
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    &:hover {
      color: #40a9ff;
    }
  }

  .mask {
    transition: all 0.3s linear;
    position: absolute;
    top: calc(100% + 6px);
    transform-origin: center top;
    transform: scaleY(0);
    background-color: #f9f9f9;
    z-index: 1000;
    &.left {
      left: 0;
    }
    &.right {
      right: 0;
    }
    .langs-list {
      max-height: 300px;
      overflow-y: scroll;
    }
    .item {
      width: 140px;
      height: 36px;
      padding: 0 16px;
      cursor: pointer;
      position: relative;
      &.checked {
        color: #40a9ff;
      }
      .wrap {
        width: inherit;
        height: inherit;
        display: flex;
        align-items: center;
        position: relative;
        z-index: 1;
      }
      &::before {
        content: '';
        z-index: 0;
        width: 0;
        height: inherit;
        background-color: #eeeeee;
        transition: all 0.25s linear;
        position: absolute;
        top: 0;
        left: 0;
      }
      &:not(.checked):hover::before {
        width: 100%;
      }
    }
    &.open {
      transform: scaleY(1);
    }
    .close {
      height: 36px;
      position: relative;
      border-top: 1px solid #eeeeee;
      cursor: pointer;

      &::before,
      &::after {
        content: '';
        display: block;
        width: 3px;
        height: 16px;
        background-color: #444444;
        transition: opacity 0.25s linear;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        margin: auto;
        opacity: 0.75;
      }
      &::before {
        transform: rotateZ(45deg);
      }
      &::after {
        transform: rotateZ(-45deg);
      }
      &:hover::before,
      &:hover::after {
        opacity: 1;
      }
    }
  }
}
</style>

👉 使用i18n

xxx/languages.ts

import { LangProps } from '@/components/PickerLanguage/index.vue';
export const langs: Array<LangProps> = [
  {
    label: '简体中文',
    value: 'zh-CN',
    icon: 'https://static.case-base.net/lang/svg/cn.svg',
  },
  {
    label: 'English',
    value: 'en-US',
    icon: 'https://static.case-base.net/lang/svg/us.svg',
  },
  {
    label: 'Español',
    value: 'es-ES',
    icon: 'https://static.case-base.net/lang/svg/es.svg',
  },
  {
    label: 'Polski',
    value: 'pl-PL',
    icon: 'https://static.case-base.net/lang/svg/pl.svg',
  },
  {
    label: 'Português',
    value: 'pt-PT',
    icon: 'https://static.case-base.net/lang/svg/pt.svg',
  },
];

xxx.vue

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import PickerLanguage, { LangProps } from '@/components/PickerLanguage/index.vue';
import { getLocaleLanguage } from '@/languages/i18n';
import { langs } from './langs';
  
// --  i18n
const { t, locale } = useI18n();

// -- events
const onLangeChange = (lang: LangProps) => {
  // -- 记录用户切换的语言,以便下次加载时可以自动命中
  localStorage.setItem('i18n-Lang', lang.value);
  // -- 注意:
  // -- 1. 组件/本地存储格式 → 「语言-国家」,如”zh-CN“
  // -- 2. i18n 所需格式为 → 「语言」,如 ”zh“
  // -- 3. 所以这里在设置 local.value 时需截取语言代码来赋值
  locale.value = lang.value.slice(0, 2);
  console.log(lang);
};
</script>
<template>
  <div class="page">
    <!-- 选择语言 -->
    <PickerLanguage
      :langs="langs"
      alignment="right"
      :default-value="getLocaleLanguage()"
      @change="onLangeChange"
    />
    <h3 class="message">{{ t('message.hello') }}</h3>
  </div>
</template>

<style lang="less">
.page {
  padding-top: 100px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 18px;
}

.message {
  margin-top: 16px;
}

.lg-langs {
  position: fixed !important;
  top: 20px;
  right: 100px;
}
</style>