mirror of https://github.com/YunYouJun/valaxy
583 lines
14 KiB
Markdown
583 lines
14 KiB
Markdown
---
|
||
title: How to realize CSS i18n?
|
||
title_zh: 如何实现 CSS i18n?
|
||
date: 2022-04-09
|
||
categories: Valaxy 开发笔记
|
||
tags:
|
||
- valaxy
|
||
- i18n
|
||
- 笔记
|
||
end: true
|
||
---
|
||
|
||
::: tip
|
||
|
||
You can click this button to toggle locales.
|
||
|
||
:::
|
||
|
||
<div class="text-center">
|
||
<PressToggleLocale class="btn shadow" />
|
||
</div>
|
||
|
||
::: zh-CN
|
||
|
||
> 在一个页面中实现 i18n
|
||
|
||
为了使 [Valaxy](https://github.com/YunYouJun/valaxy) 成为一个国际化的项目,i18n 是必不可少的。
|
||
|
||
常见的 i18n 方案为采用不同的路径(如 `/zh-CN/`)或解析不同的域名(`cn.xxx.xxx`)来分别维护。
|
||
|
||
> 此外还可使用 [crowdin](https://crowdin.com/) 平台辅助用户进行多语言翻译。
|
||
|
||
但对于博客来说,这显然都很麻烦。
|
||
当你需要 i18n 时,你不得不同时维护多个目录下的文章。
|
||
当文章间存在相同的示例时,你还需要维护相同的内容。非常不优雅。
|
||
|
||
Valaxy 中,
|
||
站点的独立字段部分(如文章目录:Table of Contents)基于 [vue-i18n](https://vue-i18n.intlify.dev/) 实现,
|
||
而文章内容部分的大段文本则采用另一种 CSS i18n 的方案。
|
||
|
||
[我想先看看效果](#result)
|
||
|
||
:::
|
||
|
||
<!-- more -->
|
||
|
||
::: en
|
||
|
||
> **i18n in One Page**
|
||
|
||
In order to make [Valaxy](https://github.com/YunYouJun/valaxy) an international project, i18n is essential.
|
||
|
||
Common i18n schemes are maintained separately using different paths (e.g. `/zh-CN/`) or resolving different domain names (`cn.xxx.xxx`).
|
||
|
||
> In addition, the [crowdin](https://crowdin.com/) platform can be used to assist users with multilingual translations.
|
||
|
||
But for blogs, this is obviously all a hassle.
|
||
When you need i18n, you have to maintain articles in multiple directories at the same time.
|
||
You also have to maintain the same content when the same examples exist between articles.
|
||
Very inelegant.
|
||
|
||
In Valaxy, the
|
||
The standalone fields of the site (e.g. Table of Contents) are implemented based on [vue-i18n](https://vue-i18n.intlify.dev/).
|
||
The large text sections of the article content section use a different CSS i18n scheme.
|
||
|
||
[I want to see the result first.](#result)
|
||
|
||
:::
|
||
|
||
## Vue-i18n
|
||
|
||
::: zh-CN
|
||
配置 Vite Vue-i18n 插件 [@intlify/unplugin-vue-i18n](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n):
|
||
:::
|
||
|
||
::: en
|
||
Config Vite Vue-i18n plugin [@intlify/unplugin-vue-i18n](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n):
|
||
:::
|
||
|
||
```ts
|
||
import path from 'node:path'
|
||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||
import { defineConfig } from 'vite'
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
VueI18n({
|
||
runtimeOnly: true,
|
||
compositionOnly: true,
|
||
include: [path.resolve(__dirname, 'locales/**')],
|
||
}),
|
||
],
|
||
})
|
||
```
|
||
|
||
::: zh-CN
|
||
在 `locales` 目录下配置 `zh-CN.yml` 与 `en.yml`:
|
||
:::
|
||
|
||
::: en
|
||
Write `zh-CN.yml` and `en.yml` in `locales`.
|
||
:::
|
||
|
||
```yaml
|
||
# zh-CN.yml
|
||
sidebar:
|
||
toc: 文章目录
|
||
```
|
||
|
||
```yaml
|
||
# en.yml
|
||
sidebar:
|
||
toc: Table of Contents
|
||
```
|
||
|
||
::: zh-CN
|
||
并在主入口文件(如 `main.ts`)中初始化:
|
||
:::
|
||
|
||
::: en
|
||
and initialized in the main entry file (e.g. `main.ts`).
|
||
:::
|
||
|
||
```ts
|
||
/*
|
||
* All i18n resources specified in the plugin `include` option can be loaded
|
||
* at once using the import syntax
|
||
*/
|
||
import messages from '@intlify/unplugin-vue-i18n/messages'
|
||
|
||
// import { createApp } from 'vue'
|
||
// import App from './App.vue'
|
||
|
||
import { createI18n } from 'vue-i18n'
|
||
|
||
const i18n = createI18n({
|
||
legacy: false,
|
||
locale: 'en',
|
||
messages,
|
||
})
|
||
|
||
// const app = createApp(App)
|
||
app.use(i18n)
|
||
```
|
||
|
||
::: zh-CN
|
||
此时即可在 Vue 中使用 `t('')` 来翻译对应字段文本。
|
||
:::
|
||
|
||
::: en
|
||
You can then use `t('')` in Vue to translate the text of the corresponding field.
|
||
:::
|
||
|
||
```vue
|
||
<script lang="ts" setup>
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
const { t } = useI18n()
|
||
</script>
|
||
|
||
<template>
|
||
<h2> {{ t("sidebar.toc") }} </h2>
|
||
</template>
|
||
```
|
||
|
||
### Messages when SSG
|
||
|
||
::: zh-CN
|
||
`vue-i18n` 支持使用虚拟模块 `@intlify/unplugin-vue-i18n/messages` 的方式来导入多语言。
|
||
|
||
可惜的是,它并没有完美地支持 SSR。[#78 | intlify/bundle-tools](https://github.com/intlify/bundle-tools/issues/78)
|
||
|
||
而 Vite 的 `import.meta.globEager` 导入必须使用静态字符串。
|
||
:::
|
||
|
||
::: en
|
||
`vue-i18n` supports importing multiple languages by using the virtual module `@intlify/unplugin-vue-i18n/messages`.
|
||
|
||
Unfortunately, it doesn't support SSR perfectly.[#78 | intlify/bundle-tools](https://github.com/intlify/bundle-tools/issues/78)
|
||
|
||
And Vite's `import.meta.globEager` import must use a static string.
|
||
:::
|
||
|
||
```ts {3}
|
||
const messages = Object.fromEntries(
|
||
Object.entries(
|
||
import.meta.globEager('../../locales/*.y(a)?ml')
|
||
)
|
||
.map(([key, value]) => {
|
||
const yaml = key.endsWith('.yaml')
|
||
return [key.slice(14, yaml ? -5 : -4), value.default]
|
||
}),
|
||
)
|
||
```
|
||
|
||
::: zh-CN
|
||
当拥有确定目录时,它是奏效的,但 Valaxy 还需要将 Valaxy 自身的 `locales` 与主题的 `locales` 以及用户自定义的 `locales` 进行合并。
|
||
这意味着我们不能使用变量来拼接字符串进行导入,对于不同包管理器的目录结构不同,我们很难确定这些 `locales` 处于何处的相对位置。
|
||
|
||
因此我采用插件虚拟模块(`@valaxyjs/locales`)的形式实现(依次导入各目录下的 locales 数据并合并):
|
||
|
||
> Vite 虚拟模块的原理其实就是拼接字符串。
|
||
:::
|
||
|
||
::: en
|
||
It works when there is a defined directory, but Valaxy also needs to merge Valaxy's own `locales` with the theme's `locales` and user-defined `locales`.
|
||
This means that we cannot use variables to splice strings for import, and it is difficult to determine the relative location of where these `locales` are for different package managers with different directory structures.
|
||
|
||
So I implemented it in the form of a plugin virtual module (`@valaxyjs/locales`):
|
||
|
||
> The principle of the Vite virtual module is actually a spliced string.
|
||
:::
|
||
|
||
```ts
|
||
import type { Plugin } from 'vite'
|
||
|
||
// import the locales data in each directory in turn and merge them
|
||
function generateLocales(roots: string[]) {
|
||
const imports: string[] = [
|
||
'const messages = { "zh-CN": {}, en: {} }',
|
||
]
|
||
const languages = ['zh-CN', 'en']
|
||
|
||
roots.forEach((root, i) => {
|
||
languages.forEach((lang) => {
|
||
const langYml = `${root}/locales/${lang}.yml`
|
||
if (fs.existsSync(langYml) && fs.readFileSync(langYml, 'utf-8')) {
|
||
const varName = lang.replace('-', '') + i
|
||
// in windows, you need to change slash
|
||
// more info you can refer 'packages/valaxy/src/node/plugins/index.ts'
|
||
imports.push(`import ${varName} from "${langYml}"`)
|
||
imports.push(`Object.assign(messages['${lang}'], ${varName})`)
|
||
}
|
||
})
|
||
})
|
||
|
||
imports.push('export default messages')
|
||
return imports.join('\n')
|
||
}
|
||
|
||
export function createValaxyPlugin(options: ResolvedValaxyOptions): Plugin {
|
||
// ...
|
||
const roots = [options.clientRoot, options.themeRoot, options.userRoot]
|
||
|
||
return {
|
||
name: 'Valaxy',
|
||
|
||
load(id) {
|
||
// ...
|
||
if (id === '/@valaxyjs/locales')
|
||
return generateLocales(roots)
|
||
},
|
||
|
||
async handleHotUpdate(ctx) {
|
||
// ...
|
||
},
|
||
}
|
||
}
|
||
```
|
||
|
||
::: zh-CN
|
||
最后在 i18n 的初始化文件加载:
|
||
:::
|
||
|
||
::: en
|
||
Finally load in the i18n initialization file:
|
||
:::
|
||
|
||
```ts
|
||
// i18n.ts
|
||
import messages from '/@valaxyjs/locales'
|
||
|
||
const i18n = createI18n({
|
||
legacy: false,
|
||
locale: 'en',
|
||
messages,
|
||
})
|
||
app.use(i18n)
|
||
```
|
||
|
||
## CSS i18n - Another solution
|
||
|
||
::: zh-CN
|
||
> CSS i18n - 另一种互补解决方案
|
||
:::
|
||
|
||
::: en
|
||
> CSS i18n - Another complementary solution
|
||
:::
|
||
|
||
::: zh-CN
|
||
文章部分拥有大段的文本,而 `vue-i18n` 的场景则在于一些独立的字段翻译。
|
||
|
||
而传统的分文件独立管理的方式,对于博客来说其实并不方便。
|
||
大多数情况,你并不会想专门建立一个文件夹来管理它。
|
||
|
||
因此我尝试使用纯 CSS 解决该问题。
|
||
:::
|
||
|
||
::: en
|
||
While the article section has large sections of text, the scenario of `vue-i18n` lies in some separate field translations.
|
||
|
||
And the traditional way of managing them independently in separate files is not really convenient for blogs.
|
||
In most cases, you don't want to create a dedicated folder to manage it.
|
||
|
||
So I tried to solve the problem using pure CSS.
|
||
:::
|
||
|
||
<div lang="zh-CN">
|
||
|
||
::: tip 思路
|
||
即借助 CSS 规则,根据对应语言,显示对应区块内容。
|
||
大体方案:通过 [markdown-it-container](https://github.com/markdown-it/markdown-it-container) 设置 fence 预编译 Markdown,
|
||
为需要进行 i18n 的段落包裹新的 `<div lang="zh-CN"></div>`,并使用 CSS 默认隐藏它们。
|
||
当页面初始化或切换语言时,为 html 添加对应语言类,编写对应 CSS 以在该类下显示对应语言的区块。
|
||
:::
|
||
|
||
</div>
|
||
|
||
<div lang="en">
|
||
|
||
::: tip IDEA
|
||
That is, with the help of CSS rules, the content of the corresponding block is displayed according to the corresponding language.
|
||
The general solution: set fence to pre-compile Markdown via [markdown-it-container](https://github.com/markdown-it/markdown-it-container).
|
||
Wrap new `<div lang="zh-CN"></div>`s for the paragraphs that need to be i18n and hide them by default with CSS.
|
||
When the page initializes or switches languages, add the corresponding language class to html and write the corresponding CSS to display the corresponding language block under that class.
|
||
:::
|
||
|
||
</div>
|
||
|
||
::: zh-CN
|
||
**优势**:
|
||
|
||
- 可在同一个 Markdown 文件中进行维护,书写便捷
|
||
- 预加载与实时切换
|
||
- URL 不变,便于管理与分享,且切换无需刷新页面
|
||
- 渐进式翻译(只翻译部分内容并可共用示例内容等)
|
||
- 当你在同一个文件编写文档时,GitHub Copilot (VSCode 插件) 甚至很容易帮助你补全翻译!
|
||
|
||
**劣势**:
|
||
|
||
- 多语言内容被渲染在同一页面中,增加冗余(但我觉得这微小的体积完全是可以接受的)
|
||
:::
|
||
|
||
::: en
|
||
**Advantages**:
|
||
|
||
- Can be maintained in the same Markdown file, easy to write
|
||
- Pre-loading and real-time switching
|
||
- URLs remain unchanged, easy to manage and share, and switch without refreshing the page
|
||
- Progressive translation (only part of the content is translated and can share example content, etc.)
|
||
- When you are writing a document in the same file, GitHub Copilot (VSCode Extension) can even help you complete the translation!
|
||
|
||
**Disadvantages**:
|
||
|
||
- Multi-language content is rendered in the same page, adding redundancy (but I think the tiny size is perfectly acceptable)
|
||
:::
|
||
|
||
### Result
|
||
|
||
::: zh-CN
|
||
**效果如下**(点击按钮切换):
|
||
:::
|
||
|
||
::: en
|
||
**The effect is as follows** (click the button to switch).
|
||
:::
|
||
|
||
<PressToggleLocale class="shadow" />
|
||
|
||
::: zh-CN
|
||
另一种 i18n 方案。
|
||
|
||
> 更多内容:...
|
||
:::
|
||
|
||
::: en
|
||
Another i18n method.
|
||
|
||
> More info...
|
||
:::
|
||
|
||
::: zh-CN
|
||
中文
|
||
:::
|
||
|
||
::: en
|
||
English
|
||
:::
|
||
|
||
---
|
||
|
||
::: zh-CN
|
||
**书写方式**如下:
|
||
:::
|
||
|
||
::: en
|
||
**Written like this**:
|
||
:::
|
||
|
||
```md
|
||
::: zh-CN
|
||
另一种 i18n 方案。
|
||
|
||
更多内容:...
|
||
:::
|
||
|
||
::: en
|
||
Another i18n method.
|
||
|
||
More info...
|
||
:::
|
||
|
||
::: zh-CN
|
||
中文
|
||
:::
|
||
|
||
::: en
|
||
English
|
||
:::
|
||
```
|
||
|
||
### Steps
|
||
|
||
::: zh-CN
|
||
**实现步骤**
|
||
:::
|
||
|
||
::: zh-CN
|
||
为了能够借助 CSS 处理 i18n,我们借助 markdown-it-container 的 fence 包裹 Markdown 中需要参与 i18n 的内容。
|
||
:::
|
||
|
||
::: en
|
||
To be able to handle i18n with CSS, we use markdown-it-container's fence to wrap Markdown content that needs to participate in i18n.
|
||
:::
|
||
|
||
```ts
|
||
export function containerPlugin(md: MarkdownIt) {
|
||
// ...
|
||
const languages = ['zh-CN', 'en']
|
||
|
||
languages.forEach((lang) => {
|
||
md.use(container, lang, {
|
||
render: (tokens: Token[], idx: number) => tokens[idx].nesting === 1 ? `<div lang="${lang}">\n` : '</div>\n',
|
||
})
|
||
})
|
||
}
|
||
```
|
||
|
||
::: zh-CN
|
||
这可以使:
|
||
:::
|
||
|
||
::: en
|
||
This allows:
|
||
:::
|
||
|
||
```md
|
||
::: zh-CN
|
||
中文
|
||
:::
|
||
```
|
||
|
||
::: zh-CN
|
||
变成 `<div lang="zh-CN"></div>` 的形式。
|
||
|
||
> [lang](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) 是 HTML 的一个标准字段。
|
||
|
||
为避免 class 命名冲突,我们可以采用 CSS attribute 的查询方式。
|
||
|
||
首先将 i18n 全部隐藏:
|
||
:::
|
||
|
||
::: en
|
||
Be `<div lang="zh-CN"></div>`.
|
||
|
||
> [lang](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) is a standard field in HTML.
|
||
|
||
To avoid class naming conflicts, we can use the CSS attribute query.
|
||
|
||
First, hide all i18n:
|
||
:::
|
||
|
||
```scss
|
||
html[lang] {
|
||
.markdown-body {
|
||
div[lang] {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
::: zh-CN
|
||
编写 CSS/SCSS 规则,设定 html `lang` 为对应语言时,显示对应语言的元素即可。
|
||
:::
|
||
|
||
::: en
|
||
Write CSS/SCSS rules and set html `lang` to display elements in the corresponding language when it is the corresponding language.
|
||
:::
|
||
|
||
```scss
|
||
$languages: zh-CN, en;
|
||
|
||
@each $lang in $languages {
|
||
html[lang="#{$lang}"] {
|
||
// only for markdown
|
||
.markdown-body {
|
||
div[lang="#{$lang}"] {
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
::: zh-CN
|
||
为了帮助用户记住自己的语言,还请不要忘记初始化。
|
||
:::
|
||
|
||
::: en
|
||
To help users remember their language, please also don't forget to initialize.
|
||
:::
|
||
|
||
```html {9}
|
||
<!DOCTYPE html>
|
||
<html lang="en" class="i18n">
|
||
|
||
<head>
|
||
<!-- ... -->
|
||
<script>
|
||
(function() {
|
||
const locale = localStorage.getItem('valaxy-locale') || 'en'
|
||
document.documentElement.setAttribute('lang', locale)
|
||
})()
|
||
</script>
|
||
</head>
|
||
<body>...</body>
|
||
</html>
|
||
```
|
||
|
||
::: zh-CN
|
||
切换语言时则可做如下处理:
|
||
:::
|
||
|
||
::: en
|
||
When switching languages, the following can be done.
|
||
:::
|
||
|
||
```ts
|
||
function toggleLocales(lang: val) {
|
||
// ...
|
||
// save locale
|
||
localStorage.setItem('valaxy-locale', lang)
|
||
// set html lang
|
||
document.documentElement.setAttribute('lang', lang)
|
||
}
|
||
```
|
||
|
||
::: zh-CN
|
||
值得一提的是,在查看 `lang` 文档时,我意外地发现 `:lang` 也是一种支持的选择器。
|
||
因此上述的 CSS 中 `[lang="xxx"]` 也可以替换为 `:lang(xxx)`。
|
||
|
||
但是 `:lang()` 也会命中默认语言的 `div`(没有 lang 字段,但处于含有 lang 的标签中),因此为了安全,我们还是应该使用 class 的属性查询。
|
||
:::
|
||
|
||
::: en
|
||
It's worth mentioning that when looking at the `lang` documentation, I accidentally found that `:lang` is also a supported selector.
|
||
So `[lang="xxx"]` in the CSS above could also be replaced with `:lang(xxx)`.
|
||
|
||
However, `:lang()` will also hit the default language `div` (which has no lang field but is in a tag containing lang), so to be safe we should still use the class attribute query.
|
||
:::
|
||
|
||
::: zh-CN
|
||
我认为 vue-i18n 与 CSS i18n 的互补,可以非常好地解决单页内的 i18n 切换。
|
||
不妨一试?
|
||
:::
|
||
|
||
::: en
|
||
I think vue-i18n complements CSS i18n and could be a very good solution for i18n switching within a single page.
|
||
Why not give it a try?
|
||
:::
|