国际化利器 Intl Messageformat
<blockquote><p>我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。</p>
</blockquote>
<blockquote>
<p>本文作者:霜序</p>
</blockquote>
<blockquote>
<p>Formats ICU Message strings with number, date, plural, and select placeholders to create localized messages.</p>
</blockquote>
<h1 id="icu-信息语法">ICU 信息语法</h1>
<p>ICU 是 International Components for Unicode 的简称。处理多语言和复杂文本模板提供了一种标准化的语法。它的主要用途是通过模板描述消息内容,并根据上下文(如语言、复数规则、性别等)动态生成格式化的字符串。</p>
<h3 id="核心功能">核心功能</h3>
<h4 id="动态插值">动态插值</h4>
<p>在模板中插入变量值,格式为<code>{key, type, format}</code></p>
<pre><code class="language-plain">Hello, {name}!
I have {workNum, number} things to do
Almost {pctBlack, number, percent} of them are black.
</code></pre>
<p>变量<code>{key}</code>会被实际值替换,例如:<code>Hello, FBB!</code></p>
<h4 id="复数规则plurals">复数规则(Plurals)</h4>
<p>处理与数量相关的消息变化,格式为<code>{key, plural, matches}</code></p>
<pre><code class="language-plain">{count, plural, =0{no items} one {1 item} other {# items}}
</code></pre>
<p>会根据传入的 count 会动态处理成合适的文本</p>
<pre><code class="language-plain">count = 0 ===> no items
count = 1 ===> 1 item
count = 4 ===> 4 items
</code></pre>
<h4 id="条件选择select">条件选择(Select)</h4>
<p>基于条件选择消息内容,格式为<code>{key, select, matches}</code></p>
<pre><code class="language-plain">{gender, select, male {He} female {She} other {They}} liked your post.
</code></pre>
<p>会根据 gender 的传入动态输出</p>
<pre><code class="language-plain">gender = male ===> He liked your post.
gender = female ===> She liked your post.
gender = other ===> They liked your post.
</code></pre>
<h4 id="日期与时间格式化">日期与时间格式化</h4>
<p>格式化日期和时间值</p>
<pre><code class="language-plain">The event is scheduled on {date, date, long}.
</code></pre>
<p>输出格式依赖于区域设置,如: <code>January 1, 2024</code></p>
<h3 id="总结">总结</h3>
<ul>
<li>强大的模版支持,提供了多种模式供用户选择</li>
<li>跨平台支持,可以用于不同语言</li>
</ul>
<h2 id="intl-messageformat"><strong>Intl Messageformat</strong></h2>
<p>Intl MessageFormat 是一个基于 JavaScript 实现的库,用于处理多语言国际化场景,主要功能是动态格式化文本消息。</p>
<pre><code class="language-bash">pnpm add intl-messageformat
</code></pre>
<h3 id="基础使用">基础使用</h3>
<pre><code class="language-jsx">import { IntlMessageFormat } from "intl-messageformat";
const formatter = new IntlMessageFormat("Hello, {name}!");
const message = formatter.format({ name: "World" });
</code></pre>
<pre><code class="language-jsx">const formatter = new IntlMessageFormat(
"{count, plural, =0{no items} one {1 item} other {# items}}"
);
const message = formatter.format({ count: 0 });
const message1 = formatter.format({ count: 1 });
const message2 = formatter.format({ count: 10 });
console.log(message, message1, message2); // no items 1 items 10 items
</code></pre>
<p>发现了一个小小的问题,为什么在<code>{ count: 1 }</code>的时候,输出的是<code>1 items</code>?</p>
<p><code>IntlMessageFormat</code>还接受其他的参数,第二个参数为<code>locales</code>用于指定当前的语言。如果不特殊指定会通过<code>new Intl.NumberFormat().resolvedOptions().locale</code>获取默认值,此时的默认值为<code>zh-CN</code>,无法处理<code>one</code>这条规则,因此匹配到了<code>other</code>这条规则。在<code>locale</code>为<code>en</code>的语言下能够成为我们的期望值。</p>
<p>那么我们只能更改传入的<code>message</code></p>
<pre><code class="language-jsx">const formatter = new IntlMessageFormat(
"{count, plural, =0{no items} =1{1 item} other {# items}}"
);
const message = formatter.format({ count: 0 });
const message1 = formatter.format({ count: 1 });
const message2 = formatter.format({ count: 10 });
console.log(message, message1, message2); // no items 1 item 10 items
</code></pre>
<p>在我们的产品中会有这样的场景,每月 x 号,每周星期 x 的情况,当我们做国际化的时候就可以使用 ICU 信息来做。</p>
<pre><code class="language-jsx">const message = `The {day, selectordinal,
one {#st}
two {#nd}
few {#rd}
other {#th}
} day of month`;
const formatter = new IntlMessageFormat(message, "en");
console.log(formatter.format({ day: 22 }));// The 22nd day of month
console.log(formatter.format({ day: 26 }));// The 26th day of month
</code></pre>
<p>这里又出来一种新的类型<code>selectordinal</code>,主要用于处理序数词,适用于描述顺序或排名。</p>
<p><code>one/two/few/other</code> 分别对应着不同的序数形式。<code>one</code>通常用于以1结尾的数字除了11),<code>two</code> 用于以2结尾的数字除了12),<code>few</code>用于以3结尾的数字(除了13),而<code>other</code>则用于所有其他情况。</p>
<h4 id="嵌套-dom-的情况">嵌套 dom 的情况</h4>
<p>常见例子,我们产品中需要使用 showTotal 中的 total 需要用 <span> 包裹做样式更改,最后可能提出的文本为</span></p>
<pre><code class="language-diff">showTotal={(total) => (
<span>
共<span className="text-primary">{total}</span>
条数据,每页显示{pagination.pageSize}条
</span>
)}
{
W: '共',
X: '条数据,每页显示',
Y: '条',
}
</code></pre>
<p>一句完整的话被切分的乱七八糟,翻译成英文的时候也会出现语序问题。</p>
<p>这个时候就需要更改当前的方法,用 IntlMessageFormat 的 tag 模式来支持</p>
<pre><code class="language-diff">共 <BlueText>{val1}</BlueText> 条数据<Fragment>,每页显示 {val2} 条</Fragment>
</code></pre>
<h3 id="自定义-formatter">自定义 formatter</h3>
<p>在<code>intl-messageformat</code>中,您可以为消息模板中的自定义类型定义自己的格式化逻辑。例如,默认支持<code>number</code>、<code>date</code>和<code>time</code>类型,但通过自定义<code>formatters</code>,您可以添加自己的格式化类型或扩展现有类型。</p>
<pre><code class="language-jsx">const customFormatters = {
getDateTimeFormat(locales, options) {
const originalFormatter = new Intl.DateTimeFormat(locales, options);
return {
format: (value) => `📅 ${originalFormatter.format(value)}`,
};
},
};
const message = new IntlMessageFormat(
"The event is on {eventDate, date}",
"en",
{},
{ formatters: customFormatters }
);
const msg = message.format({ eventDate: new Date("2024-01-01") });
console.log(msg); // The event is on 📅 1/1/2024
</code></pre>
<p>在<code>formatters</code>中只能定义<code>getNumberFormat/getDateTimeFormat/getPluralRules</code>三种类型方法</p>
<p>在前面的时候我们说到<code>{pctBlack, number, percent}/{date, date, long}</code>,最后一个称为<code>style</code>可以用于扩展内置的格式化功能</p>
<pre><code class="language-tsx">const customFormatters = {
getDateTimeFormat(locales, style) {
if (style === "customDate") {
const originalFormatter = new Intl.DateTimeFormat(locales, style);
return {
format: (value) => `📅 ${originalFormatter.format(value)} 📅`,
};
}
return Intl.DateTimeFormat(locales, style);
},
};
const message = new IntlMessageFormat(
"The event is on {eventDate, date, customDate}",
"en",
{ date: { customDate: "customDate" } },
{ formatters: customFormatters }
);
const msg = message.format({ eventDate: new Date("2024-01-01") });
console.log(msg); // The event is on 📅 1/1/2024 📅
</code></pre>
<h3 id="总结-1">总结</h3>
<p>在我们后续如果要去国际化的时候,遇到一些需要做单复数/数词的时候,应该去修改我们的英文 JSON,最后在子产品内部调用<code>I18N.get</code>方法即可。因为在<code>get</code>方法使用了<code>IntlMessageFormat</code>去做转换</p>
<h2 id="intl-messageformat-的实现原理"><strong>Intl Messageformat 的实现原理</strong></h2>
<p>Intl Messageformat 需要将传入的<code>message</code>做一个拆解,获取到对应<code>{}</code>中的数据,在 <code>format</code>的时候通过传入的数据做填充。</p>
<h3 id="icu-messageformat-parser">icu-m<strong>essageformat-parser</strong></h3>
<p>针对于<code>icu message string</code>官方提供了对应的<code>parser</code>来解析,和我们生成<code>AST</code>一样,会生成固定类型的数据。</p>
<pre><code class="language-jsx">import { parse } from "@formatjs/icu-messageformat-parser";
const ast = parse(
"Hello, {name}! You have {count, plural, one {# message} other {# messages}}."
);
// [
// { type: 0, value: "Hello, " },
// { type: 1, value: "name" },
// { type: 0, value: "! You have " },
// {
// type: 6,
// value: "count",
// options: {
// "=0": { value: [{ type: 0, value: "no items" }] },
// one: { value: [{ type: 0, value: "1 item" }] },
// other: { value: [{ type: 7 }, { type: 0, value: " items" }] },
// },
// offset: 0,
// pluralType: "cardinal",
// },
// { type: 0, value: "." },
// ]
</code></pre>
<h3 id="format-方法">format 方法</h3>
<pre><code class="language-jsx">const message = "Hello, {name}! You have {count, plural, =0{no items} one {1 item} other {# items}}.";
const formatter = new IntlMessageFormat(message, "en");
formatter.format({ name: "World", count: 0 });
</code></pre>
<p>当我们调用<code>format</code>的时候,其实就是遍历上述的<code>AST</code>,针对于不同的<code>type</code>使用不同的<code>formatter</code></p>
<p>处理好数据。</p>
<pre><code class="language-jsx">// 处理普通文本
if (isArgumentElement(el)) {
if (!value || typeof value === 'string' || typeof value === 'number') {
value =
typeof value === 'string' || typeof value === 'number'
? String(value)
: ''
}
result.push({
type: typeof value === 'string' ? PART_TYPE.literal : PART_TYPE.object,
value,
} as ObjectPart<T>)
}
// 处理 Date
if (isDateElement(el)) {
const style =
typeof el.style === 'string'
? formats.date
: isDateTimeSkeleton(el.style)
? el.style.parsedOptions
: undefined
result.push({
type: PART_TYPE.literal,
value: formatters.getDateTimeFormat(locales, style).format(value as number),
})
}
</code></pre>
<h2 id="总结-2">总结</h2>
<p><code>Intl MessageFormat</code>是一个功能强大且成熟的国际化工具。通过结合<code>ICU</code>信息语法和JS的<code>Intl API</code>,它能够为多语言应用提供高效、灵活的消息格式化解决方案</p>
<h3 id="最后">最后</h3>
<p>欢迎关注【袋鼠云数栈UED团队】~<br>
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star</p>
<ul>
<li><strong>大数据分布式任务调度系统——Taier</strong></li>
<li><strong>轻量级的 Web IDE UI 框架——Molecule</strong></li>
<li><strong>针对大数据领域的 SQL Parser 项目——dt-sql-parser</strong></li>
<li><strong>袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices</strong></li>
<li><strong>一个速度更快、配置更灵活、使用更简单的模块打包器——ko</strong></li>
<li><strong>一个针对 antd 的组件测试工具库——ant-design-testing</strong></li>
</ul><br><br>
来源:https://www.cnblogs.com/dtux/p/18868379
頁:
[1]