i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是 “国际化”的简称。
对于一些跨国项目来说,国际化是尤为重要的,那什么是国际化呢❓国际化的意思就是将我们写的项目,能够根据不同国家的语言,进行翻译,进行切换,方便不同国家的客户使用。
本文示例基于vue-i18n
实现,「vue-i18n」顾名思义就是 Vue.js 的国际化插件, 它可以轻松地将一些本地化功能集成到你的 Vue.js 应用程序中。
基本思路:
- 定义语言包:需要几种语言展示,就定义几个语言包
- 创建对象,对语言包进行整合,对象的 key 为语言包的引用,值为语言包对象
- 创建 vue-i18n 类的对象,同时为其 messages 属性为第②步创建的对象,为其 locale 属性赋值为第②步中语言对象对应的 key
- 在创建 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
在 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;
import { createApp } from 'vue';
import App from '@/App.vue';
import i18n from '@/languages/i18n';
const app = createApp(App);
app.use(i18n);
app.mount('#app');
👉 组件封装
封装 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>