React 应用的国际化重构:处理化学专业术语在多语言下的排版差异

各位同学,各位程序员,各位未来可能成为诺贝尔奖得主或生物黑客的朋友们,大家晚上好(或者早上好,视你们的时区而定)。

今天我们不聊那个永远改不完的 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 组件内部,不污染国际化消息文件。而国际化文件只负责翻译 HHydrogenOOxygen


第三部分: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)都是基于 SVGCanvas 的。它们不是用 <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>
  );
};

总结

好了,同学们,今天的讲座接近尾声。

我们回顾了一下:

  1. 不要把化学式当成普通字符串:它是数据结构,不是文本。
  2. CSS 是你的朋友:处理下标、字体、布局的关键。
  3. React 组件是架构:将渲染逻辑与翻译逻辑分离。
  4. 自动化是必须的:防止程序员手滑把 H₂O 变成 HdosO。
  5. Canvas 是未来:对于复杂的 3D 或专业绘图,放弃 DOM。

化学是一门精确的科学,而代码是一门精确的艺术。当这两者相遇,你得到的不是“Hello World”,而是“Hello H₂O”。

别再让 react-intl 去处理那些复杂的排版了,那是对它的侮辱。把控制权拿回到组件手里,用 CSS 控制排版,用 i18n 控制语义。这才是资深程序员该干的事。

现在,下课。记得把你的 dangerouslySetInnerHTML 代码审查一遍,还有,别忘了给你的化学公式组件加上单元测试。这年头,谁会相信一个连 H₂O 都渲染不对的 React 应用来做药物研发呢?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注