带变量的HTML国际化/本地化编码风格
我已经在网页开发上工作了一段时间了,但我一直在寻找一个干净的解决方案,来处理我在国际化(i18n)HTML字符串时遇到的问题,尤其是链接标签。
首先,让我给你展示一个典型的问题例子。这是HTML模板中常见的一段字符串:
Welcome to my site. Check out our cool <a href="/products">products</a>
you should not miss.
我该如何翻译这段字符串,同时保持以下几个特性:
- 动态生成网址(比如使用路由)
- 可翻译的字符串要尽量易读(这样翻译人员可以在不看代码的情况下进行翻译)
- 因为字符串中包含HTML,我可能想要对某些部分进行转义(比如网址),这样如果这个网址包含用户输入,就不会让我面临XSS攻击的风险
- 代码看起来也要尽量美观
当你的字符串包含动态内容和HTML时,你是如何翻译的呢?
1 个回答
现在我想把国际化(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')
为了结合这些可能性,我必须生成一些非常丑陋的代码(注意这使用了 translationstring
的 mapping
关键字参数(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')
,所以我们必须手动提取
所以就这些,这就是我对这个问题的解决方法。也许我做得太复杂了,你们有更好的想法,或者这可能是一个依赖于特定可翻译文本类型的问题(需要选择合适的方法)。
我有没有遗漏什么解决方案,或者有什么可以改进我方法的建议?