在我之前的文章中,我介绍了 Stimulus——一个由 Basecamp 创建的适度的javascript框架。今天我将讨论国际化 Stimulus 应用程序,因为该框架不提供任何开箱即用的 I18n 工具。国际化是重要的一步,尤其是当您的应用程序被来自世界各地的人们使用时,因此对如何做的基本了解可能真的会派上用场。
当然,由您决定实施哪种国际化解决方案,无论是jquery.I18n、Polyglot还是其他。在本教程中,我想向您展示一个名为I18next的流行 I18n 框架,它具有许多很酷的功能,并提供了许多额外的第三方插件来进一步简化开发过程。即使具有所有这些功能,I18next 也不是一个复杂的工具,您无需学习大量文档即可开始使用。
在本文中,您将了解如何借助 I18next 库在 Stimulus 应用程序中启用 I18n 支持。具体来说,我们将讨论:
I18下一个配置
翻译文件并异步加载
一次执行翻译并翻译整个页面
处理复数和性别信息
在语言环境之间切换并在 GET 参数中保留所选的语言环境
根据用户的喜好设置语言环境
源代码可在教程 GitHub 存储库中找到。
引导刺激应用程序
为了开始,让我们克隆 Stimulus Starter 项目并使用Yarn 包管理器安装所有依赖项:
git clone https://github.com/stimulusjs/stimulus-starter.git cd stimulus-starter yarn install
我们将构建一个简单的 Web 应用程序来加载有关注册用户的信息。对于每个用户,我们将显示他/她的登录信息以及到目前为止他或她上传的照片数量(这些照片是什么并不重要)。
此外,我们将在页面顶部展示一个语言切换器。选择语言后,应立即翻译界面,而无需重新加载页面。此外,URL 应该附加一个?locale
GET 参数,指定当前正在使用的语言环境。当然,如果页面加载时已提供此参数,则应自动设置正确的语言。
好的,让我们继续渲染我们的用户。将以下代码行添加到public/index.html文件中:
<div data-controller="users" data-users-url="/api/users/index.json"></div>
在这里,我们使用users
控制器并提供一个 URL 来加载我们的用户。在现实世界的应用程序中,我们可能会有一个从数据库中获取es 用户并以 JSON 响应的服务器端脚本。然而,对于本教程,让我们简单地将所有必要的数据放入public/api/users/index.json文件中:
[ { "login": "johndoe", "photos_count": "15", "gender": "male" }, { "login": "annsmith", "photos_count": "20", "gender": "female" } ]
现在创建一个新的src/controllers/users_controller.js文件:
import { Controller } from "stimulus" export default class extends Controller { connect() { this.loadUsers() } }
一旦控制器连接到dom,我们就在方法的帮助下异步加载我们的用户loadUsers()
:
loadUsers() { fetch(this.data.get("url")) .then(response => response.text()) .then(json => { this.renderUsers(json) }) }
这个方法向给定的 URL 发送一个fetch 请求,获取响应,最后呈现用户:
renderUsers(users) { let content = '' JSON.parse(users).forEach((user) => { content += `<div>Login: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` }) this.element.innerHTML = content }
renderUsers()
依次解析 JSON,构造一个包含所有内容的新字符串,最后在页面上显示此内容(this.element
将返回控制器连接到的实际 DOM节点,div
在我们的例子中)。
I18next
现在我们将继续将 I18next 集成到我们的应用程序中。向我们的项目添加两个库:I18next 本身和一个插件,以实现从后端异步加载翻译文件:
yarn add i18next i18next-xhr-backend
我们将把所有与 I18next 相关的东西存储在一个单独的src/i18n/config.js文件中,所以现在创建它:
import i18next from 'i18next' import I18nXHR from 'i18next-xhr-backend' const i18n = i18next.use(I18nXHR).init({ fallbackLng: 'en', whitelist: ['en', 'ru'], preload: ['en', 'ru'], ns: 'users', defaultNS: 'users', fallbackNS: false, debug: true, backend: { loadPath: '/i18n/{{lng}}/{{ns}}.json', } }, function(err, t) { if (err) return console.error(err) }); export { i18n as i18n }
让我们从上到下了解这里发生了什么:
use(I18nXHR)
启用 i18next-xhr-backend 插件。fallbackLng
告诉它使用英语作为备用语言。whitelist
只允许设置英语和俄语。当然,您可以选择任何其他语言。preload
指示从服务器预加载翻译文件,而不是在选择相应语言时加载它们。ns
表示“命名空间”并接受字符串或数组。在这个例子中,我们只有一个命名空间,但对于较大的应用程序,您可以引入其他命名空间,如admin
、cart
、profile
等。对于每个命名空间,应该创建一个单独的翻译文件。defaultNS
设置users
为默认命名空间。fallbackNS
禁用命名空间回退。debug
允许调试信息显示在浏览器的控制台中。具体来说,它说明加载了哪些翻译文件,选择了哪种语言等。您可能希望在将应用程序部署到生产环境之前禁用此设置。backend
为 I18nXHR 插件提供配置并指定从何处加载翻译。请注意,路径应包含语言环境的标题,而文件应以命名空间命名并具有 .json扩展名function(err, t)
是当 I18next 准备好(或引发错误时)运行的回调。
接下来,让我们制作翻译文件。俄语的翻译应放在public/i18n/ru/users.json文件中:
{ "login": "Логин" }
login
这里是翻译键,而是Логин
要显示的值。
反过来,英文翻译应该转到public/i18n/en/users.json文件:
{ "login": "Login" }
为确保 I18next 正常工作,您可以将以下代码行添加到i18n/config.js文件中的回调:
// config goes here... function(err, t) { if (err) return console.error(err) console.log(i18n.t('login')) }
在这里,我们使用一种称为t
“翻译”的方法。此方法接受翻译键并返回相应的值。
但是,我们可能有很多 UI 部分需要翻译,而使用该t
方法进行翻译会非常繁琐。相反,我建议您使用另一个名为loc-i18next 的插件,它允许您一次翻译多个元素。
一次性翻译
安装 loc-i18next 插件:
yarn add loc-i18next
在src/i18n/config.js文件的顶部导入它 :
import locI18next from 'loc-i18next'
现在提供插件本身的配置:
// other config const loci18n = locI18next.init(i18n, { selectorAttr: 'data-i18n', optionsAttr: 'data-i18n-options', useOptionsAttr: true }); export { loci18n as loci18n, i18n as i18n }
这里有几点需要注意:
locI18next.init(i18n)
根据先前定义的 I18next 实例创建插件的新实例。selectorAttr
指定使用哪个属性来检测需要本地化的元素。基本上, loc-i18next 将搜索此类元素并使用data-i18n
属性的值作为翻译键。optionsAttr
指定哪个属性包含附加翻译选项。useOptionsAttr
指示插件使用附加选项。
我们的用户是异步加载的,所以我们必须等到这个操作完成,然后才执行本地化。现在,让我们简单地设置一个计时器,它应该在调用该方法之前等待两秒钟localize()
——当然,这是一个临时的hack。
import { loci18n } from '../i18n/config' // other code... loadUsers() { fetch(this.data.get("url")) .then(response => response.text()) .then(json => { this.renderUsers(json) setTimeout(() => { // <--- this.localize() }, '2000') }) }
localize()
对方法本身进行编码:
localize() { loci18n('.users') }
如您所见,我们只需要将选择器传递给 loc-i18next 插件。内部的所有元素(具有 data-i18n
属性集)将自动本地化。
现在调整renderUsers
方法。现在,让我们只翻译“登录”这个词:
renderUsers(users) { let content = '' JSON.parse(users).forEach((user) => { content += `<div class="users">ID: ${user.id}<br><span data-i18n="login"></span>: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` }) this.element.innerHTML = content }
好的!重新加载页面,等待两秒钟,并确保每个用户都出现“登录”字样。
复数和性别
我们已经本地化了界面的一部分,这真的很酷。不过,每个用户还有两个字段:上传照片的数量和性别。由于我们无法预测每个用户将拥有多少张照片,因此“照片”这个词应该 根据给定的数量适当地复数。为此,我们需要 data-i18n-options
之前配置的属性。要提供计数,data-i18n-options
应分配以下对象: { "count": YOUR_COUNT }
.
还应考虑性别信息。英文中的“uploaded”一词既可以用于男性也可以用于女性,但在俄语中它变成“загрузил”或“загрузила”,所以我们data-i18n-options
再次需要它,它有 { "context": "GENDER" }
一个值。请注意,顺便说一句,您可以使用此上下文来完成其他任务,而不仅仅是提供性别信息。
renderUsers(users) { let content = '' JSON.parse(users).forEach((user) => { content += `<div class="users"><span data-i18n="login"></span>: ${user.login}<br><span data-i18n="uploaded" data-i18n-options="{ 'context': '${user.gender}' }"></span> <span data-i18n="photos" data-i18n-options="{ 'count': ${user.photos_count} }"></span></div><hr>` }) this.element.innerHTML = content }
现在更新英文翻译:
{ "login": "Login", "uploaded": "Has uploaded", "photos": "one photo", "photos_plural": "{{count}} photos" }
这里没有什么复杂的。由于对于英语我们不关心性别信息(即上下文),因此翻译键应该是简单的uploaded
. 为了提供正确的复数翻译,我们使用 photos
和photos_plural
键。该{{count}}
部分是插值,将用实际数字替换d。
至于俄语,事情就更复杂了:
{ "login": "Логин", "uploaded_male": "Загрузил уже", "uploaded_female": "Загрузила уже", "photos_0": "одну фотографию", "photos_1": "{{count}} фотографии", "photos_2": "{{count}} фотографий" }
首先,请注意,对于两种可能的上下文,我们都有uploaded_male
和uploaded_female
键。其次,俄语的复数规则也比英语更复杂,所以我们必须提供的不是两个,而是三个可能的短语。I18next 支持许多开箱即用的语言,这个小工具可以帮助您了解应该为给定语言指定哪些复数键。
切换语言环境
我们完成了应用程序的翻译,但用户应该能够在语言环境之间切换。因此,在public/index.html文件中添加一个新的“语言切换器”组件:
<ul data-controller="languages"></ul>
在 src/controllers/languages_controller.js文件中制作相应的控制器:
import { Controller } from "stimulus" import { i18n, loci18n } from '../i18n/config' export default class extends Controller { initialize() { let languages = [ {title: 'English', code: 'en'}, {title: 'Русский', code: 'ru'} ] this.element.innerHTML = languages.map((lang) => { return `<li data-action="click->languages#switchLanguage" data-lang="${lang.code}">${lang.title}</li>` }).join('') } }
在这里,我们使用initialize()
回调来显示支持的语言列表。每个li
都有一个data-action
属性,该属性指定switchLanguage
单击元素时应触发的方法(在本例中为 )。
现在添加switchLanguage()
方法:
switchLanguage(e) { this.currentLang = e.target.getAttribute("data-lang") }
它只是获取事件的目标并获取属性的值data-lang
。
我还想为该currentLang
属性添加一个 getter 和 setter:
get currentLang() { return this.data.get("currentLang") } set currentLang(lang) { if(i18n.language !== lang) { i18n.changeLanguage(lang) } if(this.currentLang !== lang) { this.data.set("currentLang", lang) loci18n('body') this.highlightCurrentLang() } }
getter 非常简单——我们获取当前使用的语言的值并返回它。
设置器更复杂。首先,如果当前设置的语言不等于选择的语言,我们使用该changeLanguage
方法。此外,我们将新选择的语言环境存储在data-current-lang
属性下(在 getter 中访问),使用 loc-i18next 插件本地化 HTML 页面的主体,最后突出显示当前使用的语言环境。
让我们编写代码 highlightCurrentLang()
:
highlightCurrentLang() { this.switcherTargets.forEach((el, i) => { el.classList.toggle("current", this.currentLang === el.getAttribute("data-lang")) }) }
在这里,我们迭代了一组语言环境切换器,并将它们的data-lang
属性值与当前使用的语言环境的值进行比较。如果值匹配,则为切换器分配current
css类,否则将删除该类。
为了使this.switcherTargets
构造工作,我们需要通过以下方式定义刺激目标:
static targets = [ "switcher" ]
此外,为s 添加data-target
值为 的属性:switcher
li
initialize() { // ... this.element.innerHTML = languages.map((lang) => { return `<li data-action="click->languages#switchLanguage" data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` }).join('') // ... }
另一个需要考虑的重要事情是翻译文件可能需要一些时间来加载,我们必须等待此操作完成,然后才能切换语言环境。因此,让我们利用loaded
回调:
initialize() { i18n.on('loaded', (loaded) => { // <--- let languages = [ {title: 'English', code: 'en'}, {title: 'Русский', code: 'ru'} ] this.element.innerHTML = languages.map((lang) => { return `<li data-action="click->languages#switchLanguage" data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` }).join('') this.currentLang = i18n.language }) }
最后,不要忘记setTimeout
从loadUsers()
方法中删除:
loadUsers() { fetch(this.data.get("url")) .then(response => response.text()) .then(json => { this.renderUsers(json) this.localize() }) }
在 URL 中保留语言环境
切换区域设置后,我想?lang
在包含所选语言代码的 URL 中添加一个 GET 参数。在History API的帮助下,可以轻松地在不重新加载页面的情况下附加 GET 参数:
set currentLang(lang) { if(i18n.language !== lang) { i18n.changeLanguage(lang) window.history.pushState(null, null, `?lang=${lang}`) // <--- } if(this.currentLang !== lang) { this.data.set("currentLang", lang) loci18n('body') this.highlightCurrentLang() } }
检测语言环境
我们今天要实现的最后一件事是能够根据用户的偏好设置语言环境。一个名为LanguageDetector的插件可以帮助我们解决这个任务。添加一个新的 Yarn 包:
yarn add i18next-browser-languagedetector
LanguageDetector
在 i18n/config.js文件中导入:
import LngDetector from 'i18next-browser-languagedetector'
现在调整配置:
const i18n = i18next.use(I18nXHR).use(LngDetector).init({ // <--- // other options go here... detection: { order: ['querystring', 'navigator', 'htmlTag'], lookupQuerystring: 'lang', } }, function(err, t) { if (err) return console.error(err) });
该order
选项列出了插件应该尝试以“猜测”首选语言环境的所有技术(按重要性排序):
querystring
表示检查包含区域设置代码的 GET 参数。lookupQuerystring
设置要使用的 GET 参数的名称,lang
在我们的例子中。navigator
意味着从用户的请求中获取语言环境数据。htmlTag
涉及从标签的lang
属性中 获取首选语言环境。html
结论
在本文中,我们了解了 I18next——一种用于轻松翻译 JavaScript 应用程序的流行解决方案。您已经学习了如何将 I18next 与 Stimulus 框架集成、配置它并以异步方式加载翻译文件。此外,您还了解了如何在区域设置之间切换并根据用户的偏好设置默认语言。
- 一次性翻译
- 在 URL 中保留语言环境