带变量的HTML国际化/本地化编码风格

0 投票
1 回答
1432 浏览
提问于 2025-04-17 18:16

我已经在网页开发上工作了一段时间了,但我一直在寻找一个干净的解决方案,来处理我在国际化(i18n)HTML字符串时遇到的问题,尤其是链接标签。

首先,让我给你展示一个典型的问题例子。这是HTML模板中常见的一段字符串:

Welcome to my site. Check out our cool <a href="/products">products</a> 
you should not miss.

我该如何翻译这段字符串,同时保持以下几个特性:

  • 动态生成网址(比如使用路由)
  • 可翻译的字符串要尽量易读(这样翻译人员可以在不看代码的情况下进行翻译)
  • 因为字符串中包含HTML,我可能想要对某些部分进行转义(比如网址),这样如果这个网址包含用户输入,就不会让我面临XSS攻击的风险
  • 代码看起来也要尽量美观

当你的字符串包含动态内容和HTML时,你是如何翻译的呢?

1 个回答

3

现在我想把国际化(i18n)应用到这个字符串上,我可能会使用 gettext 或者某个框架的函数。因为我之前是做 PHP 和 Joomla! 的,所以我之前用过 JText::_,它的功能和 gettext 很相似。现在在 Python 中,我使用 Babel。这两者都有同样的问题,可能其他语言也有。接下来我分享的代码是我在 Python 中的做法,具体来说是在我的 Mako 模板 中的做法。

当然,问题在于:我们要翻译的字符串中包含 HTML(还有一个 URL)。以下是我的几种选择,我会逐一解释:

  • 直接把原始字符串传给 gettext
  • 把文本分成三部分
  • 用变量包裹链接的词
  • 使用一个单独构建的变量

直接把原始字符串传给 gettext

这似乎是一个人们可能首先想到的方法,如果没有意识到其中的影响。

方法 1:

_('Welcome to my site. Check out our cool <a href="/products">products</a> \
you should not miss.')

对于这个 msgid,你现在可以翻译它,同时保持 HTML 不变。

优点:

  • 代码看起来很干净,容易理解
  • 如果翻译者保持 HTML 不变,就不会出现问题

缺点:

  • 翻译者至少需要懂一点 HTML
  • 字符串完全不灵活,比如如果 URL 变了,所有翻译都得调整
  • 不允许使用路由器动态生成 URL

所以总结来说,虽然我用过这个方法,但很快就遇到了瓶颈。接下来我想到的是:

把文本分成三部分

方法 2:

_('Welcome to my site. Check out our cool ') + '<a href="/products">' +\
_('products') + '</a>' + _(' you should not miss.')

优点:

  • 现在 URL 完全灵活
  • 只包含实际文本供翻译者使用

缺点:

  • 把一个句子分成三部分
  • 翻译者必须知道哪些部分是相关的,否则可能无法生成有意义的句子
  • 代码看起来不太美观
  • 如果 msgid 是一个单词,可能会导致问题(要注意上下文),但可以解决。

我用这个技巧有一段时间,因为我当时不知道 PHP 中的 printf 风格字符串。因为这个看起来太丑了,我尝试了另一种方法:

用变量包裹链接的词

方法 3:

_('Welcome to my site. Check out our cool %sproducts%s you should not miss.' % \
('<a href="/products">', '</a>')

优点:

  • 只需翻译一个字符串,完整的句子
  • 翻译者可以从字符串中获取上下文
  • 代码看起来不那么丑

缺点:

  • 翻译者必须注意不要漏掉 %s(可能会造成混淆,因为它看起来像 sproducts
  • 每个 URL 引入两个格式字符串变量,其中一个仅为 </a>

使用一个单独构建的变量

从这里开始,我有了一些不同的方法,但最后我找到了我现在使用的方法(这可能看起来有点过于复杂,但我目前更喜欢这样)。

方法 4:

_('Welcome to my site. Check out our cool %s \
you should not miss.') % ('<a href="%s">%s</a>' % ('/products', _('products')))

让我花点时间来解释这个(看起来疯狂的)方法。首先,实际的翻译字符串看起来是这样的:

_('Welcome to my site. Checkout our cool ${product_url} \
you should not miss.')

这让翻译者知道插入了什么内容(这是 translationstring 版本)。其次,我想确保我可以手动转义所有插入到 HTML 中的部分。虽然 Mako 提供了 自动转义,但在这样的语句中没有意义:

${'This is a <a href="/">url</a>'}

这会破坏 URL,所以我必须应用 |n 过滤器来移除任何转义。然而,如果任何参数是用户提供的,这也会导致 XSS(跨站脚本攻击),这是我想要避免的。为了不冒任何风险,我可以像好的模板引擎默认那样转义 任何 输入,然后为这个字符串移除 Mako 的转义。因此

'<a href="%s">%s</a>' % ('/products', _('products'))

实际上看起来是

'<a href="%s">%s</a>' % (escape('/products'), _('products'))

其中 escape 是从 markupsafe 导入的(见 Markupsafe)。

最后一部分是通过路由器实现动态 URL: request.route_url('products_view')

为了结合这些可能性,我必须生成一些非常丑陋的代码(注意这使用了 translationstringmapping 关键字参数(translationstring.TranslationString),但这结合了我想要/需要的所有翻译好处):

最终结果:

_('Welcome to my site. Checkout our cool ${product_url} \
you should not miss.', mapping={'product_url': '<a href="%s">%s</a>' %\
(escape(request.route_url('products_view')), _('products'))})

优点:

  • 完全的 HTML 转义
  • 完全动态
  • 非常好的 msgid 供翻译使用

缺点:

  • 模板中构造非常丑陋(或者说程序中 anyway)
  • 语言提取器无法捕捉到 _('products'),所以我们必须手动提取

所以就这些,这就是我对这个问题的解决方法。也许我做得太复杂了,你们有更好的想法,或者这可能是一个依赖于特定可翻译文本类型的问题(需要选择合适的方法)。

我有没有遗漏什么解决方案,或者有什么可以改进我方法的建议?

撰写回答