各位同学,各位程序员,各位未来可能成为诺贝尔奖得主或生物黑客的朋友们,大家晚上好(或者早上好,视你们的时区而定)。
今天我们不聊那个永远改不完的 Bug,不聊那个只会说“404 Not Found”的浏览器,也不聊那个在周会上侃侃而谈却写不出一行 Hello World 的产品经理。今天,我们要聊的是一种更加“硬核”的国际化挑战。
想象一下,如果你的应用里有一个模块是展示化学实验数据的。这听起来很美好,对吧?红蓝绿黄,全是像素。但是,一旦涉及到国际化,这门课就从“前端开发”变成了“化学奥林匹克竞赛”。
为什么?因为化学,尤其是它的书写方式,是个极其傲慢的混蛋。它不讲道理,它不遵循常规的文本流向。
今天,我就要带大家深入 React 的国际化重构腹地,看看如何处理那些令人头皮发麻的化学专业术语排版差异。我们不仅要让代码跑通,还要让 H₂O 变成 H₂O,而不是 HdosO。
准备好了吗?让我们开始这场化学与代码的“联姻”。
第一部分:当翻译遇上化学
首先,让我们回到那个最基础的 React 国际化方案。通常,我们怎么干?
我们会用 react-intl,或者 i18next。这是一个简单的键值对系统。就像这样:
// Simple i18n approach (The Naive Way)
const messages = {
en: {
water: "H2O",
reaction: "Heat to {temp}°C"
},
zh: {
water: "水", // 或者 "H₂O" 看你怎么做
reaction: "加热至 {temp}°C"
}
};
看起来没问题,对吧?但是,一旦化学式稍微复杂一点,这个系统就崩了。
场景一:下标的噩梦
在化学里,数字不仅仅是数字。H₂O 中的 2 必须是下标。如果你把它放在 JSON 里,是个字符串 "H2O"。当你把它渲染出来的时候,它只是一个普通的字符串 H2O。
如果你试图用 CSS 把它变成下标,比如用 sub 标签,然后把这个标签扔进 i18n 的翻译系统里……你会得到什么?你会得到一个 null 或者 undefined,因为 react-intl 的插值机制通常不支持解析 HTML 标签。
场景二:顺序的叛逆
这是更糟糕的情况。在中文里,我们习惯说“水是由氢和氧组成的”。顺序是:水、氢、氧。
但在英文里,化学的书写逻辑变了,通常是“水是 H2O”。顺序是:水、H、2、O。
如果你的应用需要展示:
- 中文:“丙酮在 {liquid} 中溶解。”
- 英文:“Acetone dissolves in {liquid}.”
这里的 {liquid} 如果是一个化学式 C₃H₆O,或者 CH₃COCH₃,你怎么处理?直接用 dangerouslySetInnerHTML?别闹了,那是定时炸弹。React 会阻止你在插值中使用 <sub> 标签。
所以,传统的“翻译字符串”在化学领域就是一场灾难。我们要面对的不是简单的文本替换,而是排版逻辑的重构。
第二部分:CSS 魔法——简单的上标下标
既然我们不能把 HTML 标签塞进 i18n 的消息对象里,那我们就必须把“格式”和“内容”分开。
React 中处理下标最简单的办法是什么?CSS。别笑,这往往是最好的办法。
我们要创建一个专门的组件,专门用来处理这些令人抓狂的数字。
// components/ChemicalText.jsx
import React from 'react';
// 这个组件接收 text,然后把数字包裹起来,变成下标
const ChemicalText = ({ children, className = "" }) => {
// 正则表达式:匹配数字,并捕获它们
const render = (text) => {
// 这里的正则有点高级:d 匹配数字,() 是捕获组
// g 全局匹配,i 忽略大小写
return text.replace(/d+/g, (match) => {
return <sub className="chemical-sub">{match}</sub>;
});
};
return <span className={`chemical-base ${className}`}>{render(children)}</span>;
};
export default ChemicalText;
// styles/chemical.css
.chemical-base {
font-family: 'Times New Roman', serif; // 化学公式通常用衬线体
font-style: italic; // 元素符号通常斜体
}
.chemical-sub {
font-size: 0.7em; // 字体缩小到原来的 70%
vertical-align: sub; // 垂直对齐到下标位置
margin: 0 2px; // 左右留点呼吸空间
}
这解决了 H2O 变成 HdosO 的问题吗?没解决。这只是把 2 变成了下标。但是,如果我们把数据结构变成这样呢?
// data/chemistry.js
const formulas = {
en: {
water: [
{ symbol: 'H', count: 2 },
{ symbol: 'O', count: 1 }
]
},
zh: {
water: [
{ symbol: 'H', count: 2 },
{ symbol: 'O', count: 1 }
]
}
};
这看起来好像还没什么区别?等等,如果我们把翻译逻辑放在 JSON 里,只翻译 symbol 呢?
// advanced approach
const messages = {
en: {
water_components: ['H', 'O'] // 英文里直接写 H 和 O
},
zh: {
water_components: ['氢', '氧'] // 中文翻译成汉字
}
};
重点来了!
在中文 UI 里,我们显示“氢₂ 氧₁”。
在英文 UI 里,我们显示“H₂ O₁”。
在德语 UI 里,也许因为习惯不同,我们会显示“O₂ H”?(别笑,德语有时候就是这么任性)。
这时候,我们的组件必须能够接受“数据”和“翻译后的键名”。
// components/ChemicalFormula.jsx
import React from 'react';
import { useIntl } from 'react-intl';
const ChemicalFormula = ({ formulaKey }) => {
const intl = useIntl();
// 假设我们从后端或本地化文件获取结构化数据
// 结构:[ { element: 'H', count: 2, label: 'Hydrogen' }, ... ]
const structure = getChemicalStructure(formulaKey);
// 这是一个伪函数,假设你有一个解析器或者数据库
return (
<span className="chemical-formula">
{structure.map((part, index) => {
// 翻译元素符号
const elementLabel = intl.formatMessage({ id: `element.${part.element}` });
const count = part.count > 1 ? <sub>{part.count}</sub> : null;
return (
<React.Fragment key={index}>
<span className="element-symbol">{elementLabel}</span>
{count}
{/* 如果是氧,我们可以加个特殊颜色 */}
{part.element === 'O' && <span className="element-color" />}
</React.Fragment>
);
})}
</span>
);
};
这种方法的优点是:完全解耦。
化学式的布局逻辑(下标、颜色、字体)完全在 React 组件内部,不污染国际化消息文件。而国际化文件只负责翻译 H 是 Hydrogen,O 是 Oxygen。
第三部分:DOM 上的“黑客行为”——危险但有效
但是,有些化学式太复杂了。比如乙醇:
- 中文:乙醇 (C₂H₅OH)
- 英文:Ethanol (C₂H₅OH)
- 法文:Éthanol (C₂H₅OH)
有时候它们是一样的,有时候不一样。有时候为了排版紧凑,我们会把括号里的东西放在前面。
这时候,我们可能不得不把 HTML 拿回来。为了安全起见,我们可以使用 react-markdown 或者自定义一个简单的解析器,把化学式字符串变成 JSX。
但这有个大坑:正则表达式的崩溃。
化学式里充满了括号 ( )、中括号 [ ] 和大括号 { }。普通的正则 replace 会让你疯掉,因为括号是嵌套的。
比如:K[Fe(CN)₆]。如果你写一个简单的正则把 Fe 换掉,你可能会破坏括号的平衡。
// 这是一个极其不安全的“黑客”方法,仅用于演示复杂逻辑
// 生产环境请使用专门的库或严谨的解析器
const parseChemicalFormula = (str, locale) => {
let html = str;
// 1. 把数字变成下标
html = html.replace(/d+/g, '<sub>$&</sub>');
// 2. 替换特定元素符号(简单粗暴的替换,不考虑上下文)
if (locale === 'zh') {
html = html.replace(/H/g, '氢');
html = html.replace(/O/g, '氧');
// ... 等等
} else {
html = html.replace(/H/g, 'Hydrogen');
html = html.replace(/O/g, 'Oxygen');
}
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
警告: 使用 dangerouslySetInnerHTML 就像手里拿着一颗手雷,只要你能控制输入(即后端传给你的化学式是安全的,或者你经过了严格的清洗),它就是一把好刀。在化学展示里,这把刀很有用。
但如果你要处理反应方程式呢?
2H₂ + O₂ -> 2H₂O
这里的箭头 -> 在不同语言里可能是 =,可能是 ->,也可能是 →(全角箭头)。更重要的是,箭头两边的系数(2, 1)怎么处理?
这时候,我们需要一个更高级的组件:ChemicalReaction。
第四部分:反应方程式的国际化重构
反应方程式是国际化的噩梦中的噩梦。
中文版:2H₂ + O₂ → 2H₂O
英文版:2 H₂ + O₂ -> 2 H₂O (有时系数前面有空格)
注意到了吗?英文习惯在系数后面加空格,而中文没有。
还有一个问题:反应条件。
中文:Fe + S x86 FeS (加热)
英文:Fe + S x86 FeS (Heat)
这不仅仅是翻译单词的问题,这是HTML 标签和换行符的问题。
我们需要构建一个组件,它能接收“方程式字符串”和“翻译函数”。
// components/ChemicalReaction.jsx
import React from 'react';
import { useIntl } from 'react-intl';
const ChemicalReaction = ({ equation }) => {
const intl = useIntl();
// 这是一个非常简单的分词器,为了演示原理
// 实际上你需要处理 Token 流
const renderTokens = () => {
const tokens = equation.split(/([+-→=])/); // 按 符号 分割
return tokens.map((token, index) => {
const isSymbol = ['+', '-', '→', '=', '→'].includes(token);
if (isSymbol) {
// 翻译箭头
let arrow = token;
if (token === '→') arrow = intl.formatMessage({ id: 'arrow.to' });
if (token === '=') arrow = intl.formatMessage({ id: 'arrow.equal' });
return <span key={index} className="reaction-arrow">{arrow}</span>;
}
// 如果是普通文本(化学式)
return (
<div key={index} className="reaction-side">
{/* 这里递归调用我们之前写的 ChemicalFormula 或者直接渲染 */}
<ChemicalFormula formula={token} />
</div>
);
});
};
return (
<div className="reaction-container">
{renderTokens()}
</div>
);
};
CSS 的艺术:
React 处理这个的关键在于 CSS。反应方程式通常需要垂直对齐。
/* styles/reaction.css */
.reaction-container {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
margin: 1rem 0;
}
.reaction-side {
flex: 1;
text-align: center;
}
.reaction-arrow {
margin: 0 1rem;
font-weight: bold;
}
这样,无论中文还是英文,箭头的位置都是完美的居中。我们通过 i18n 解决了“箭头是什么字符”的问题,通过 CSS 解决了“它在哪里”的问题,通过 React 组件解决了“它周围的结构”的问题。
第五部分:矩阵与手性——色彩的力量
化学不仅仅是文字和数字,化学是空间。
当你展示一个有机分子结构图,或者一个手性异构体时,国际化就变成了颜色管理。
一个手性中心(手性碳),通常用粗线表示,虚线表示。但在打印资料(黑白屏幕)时,我们只能靠颜色区分。
这时候,你的国际化组件需要支持主题。
// components/ChiralCenter.jsx
import React from 'react';
import { useIntl } from 'react-intl';
const ChiralCenter = ({ structure, sideChain, type }) => {
const intl = useIntl();
// 假设 type 是 'R' 或 'S',或者是 '左旋' '右旋'
const typeLabel = intl.formatMessage({
id: `chiral.${type}`,
defaultMessage: type
});
return (
<div className="chiral-molecule">
{/* 这里是化学结构图,假设我们用 SVG 或者 Canvas */}
<svg width="100" height="100" viewBox="0 0 100 100">
{/* 绘制键的代码省略 */}
</svg>
{/* 标签区域 */}
<div className="chiral-label">
<span className="chiral-type" style={{ color: getChiralColor(type) }}>
{typeLabel}
</span>
</div>
</div>
);
};
这里的国际化逻辑变得很微妙。我们不是翻译结构,而是翻译结构的属性。
在英文 UI 里,我们可能显示红色的 “R”。
在日文 UI 里,为了符合学术规范,我们可能显示蓝色的 “R”。
在中文 UI 里,我们可能显示红色的 “右旋”。
颜色代表意义,文字代表定义。这就是 React 国际化的高阶玩法——把语义拆解到组件层级。
第六部分:工具链与工程化——不要让程序员去改 CSS
既然我们知道了化学排版这么麻烦,那我们怎么在生产环境中保证质量?
如果你让前端开发人员去手动给每个数字加 <sub> 标签,三分钟后他们就会离职,或者把 H₂O 改成 HdosO。
我们需要自动化。
方案一:ESLint 规则
我们可以写一个 ESLint 规则,检查化学式字符串。
// .eslintrc.js
module.exports = {
rules: {
// 检查化学式是否包含没有包裹 sub 标签的数字
'no-mixed-spaces-and-tabs': 'off', // 暂时关掉这个
'custom/chemistry-subscript': 'error'
}
};
// rules/chemistry-subscript.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Ensure numbers in chemical formulas are subscripted',
},
messages: {
subscript: 'Chemical number "{{number}}" should be subscripted.',
},
},
create(context) {
return {
JSXAttribute(node) {
// 检查 JSX 属性中的字符串值
if (node.value && node.value.type === 'Literal' && typeof node.value.value === 'string') {
const matches = node.value.value.match(/d+/g);
if (matches) {
matches.forEach(num => {
context.report({
node,
messageId: 'subscript',
data: { number: num },
});
});
}
}
},
};
},
};
方案二:Pre-commit Hook
在代码提交前,运行一个脚本来检查 messages.json 或者组件文件。
- 检查:
H2O-> 报错。必须写H₂O或者组件渲染逻辑处理。 - 检查:
HdosO-> 报错,这是拼写错误。
第七部分:终极解决方案——WebGL 与 Canvas
如果你是那种追求极致体验的开发者,你可能会觉得 DOM 操作化学式太慢了,或者样式控制太笨重。
化学绘图库(如 ChemDraw, MarvinSketch)都是基于 SVG 或 Canvas 的。它们不是用 <div> 和 <span> 堆出来的,而是画出来的。
在 React 中,你可以使用 react-chemdoodle 或者 canvas-confetti(不对,那是搞庆祝的,请忽略)。
对于国际化,Canvas 的挑战在于:你没法直接“翻译” Canvas 上的像素。
你必须把数据传给 Canvas,由 Canvas 渲染。这意味着,所有的国际化逻辑(中英文名、手性颜色、顺序)都必须在 React 层完成,然后把“处理好的”图片 URL 或者渲染好的 SVG 代码传给 Canvas 组件。
// React 层
const Molecule = ({ name, structureData }) => {
const { name: displayName, color } = getLocalizedInfo(name, locale);
return (
<div>
<h2>{displayName}</h2>
<ChemicalRenderer
data={structureData}
themeColor={color} // 把翻译好的颜色传给渲染器
/>
</div>
);
};
总结
好了,同学们,今天的讲座接近尾声。
我们回顾了一下:
- 不要把化学式当成普通字符串:它是数据结构,不是文本。
- CSS 是你的朋友:处理下标、字体、布局的关键。
- React 组件是架构:将渲染逻辑与翻译逻辑分离。
- 自动化是必须的:防止程序员手滑把 H₂O 变成 HdosO。
- Canvas 是未来:对于复杂的 3D 或专业绘图,放弃 DOM。
化学是一门精确的科学,而代码是一门精确的艺术。当这两者相遇,你得到的不是“Hello World”,而是“Hello H₂O”。
别再让 react-intl 去处理那些复杂的排版了,那是对它的侮辱。把控制权拿回到组件手里,用 CSS 控制排版,用 i18n 控制语义。这才是资深程序员该干的事。
现在,下课。记得把你的 dangerouslySetInnerHTML 代码审查一遍,还有,别忘了给你的化学公式组件加上单元测试。这年头,谁会相信一个连 H₂O 都渲染不对的 React 应用来做药物研发呢?