使用AI本地化应用
无论你是在考虑本地化你的项目,还是只是学习如何做到这一点,人工智能可能是一个很好的开始。它为实验和自动化提供了一个具有成本效益的切入点。
在这篇文章中,我们将通过一个这样的实验。我们将:
- 选择一个开源应用程序
- 审查和实施先决条件
- 使用AI自动化翻译阶段
如果你从来没有处理过本地化,并希望学习,这可能是一个好主意,从这里开始。除了一些技术细节之外,该方法在很大程度上是通用的,您可以将其应用于其他类型的项目。
拿到项目
创建一个应用程序只是为了本地化实验将是矫枉过正,所以让我们分叉一些开源项目。我选择了Spring Petclinic,这是一个用于展示Springfor Java框架的示例Web应用程序。
分叉和克隆Petclinic(需要GitHub CLI):
bash
gh repo fork https://github.com/spring-projects/spring-petclinic --clone=true
如果您以前没有使用过Spring,那么您可能对其中的一些代码片段并不熟悉,但是,正如我已经提到的,本文的讨论与技术无关。无论使用哪种语言和框架,步骤都大致相同。
本地化优惠
在应用程序可以本地化之前,它必须被国际化。
国际化(也拼写为i18n)是调整软件以支持不同语言的过程。它通常从将UI字符串外部化到特殊文件(通常称为资源包)开始。
资源包包含不同语言的文本值:
en.json:
json
{
"greeting": "Hello!",
"farewell": "Goodbye!"
}
es.json:
json
{
"greeting": "¡Hola!",
"farewell": "¡Adiós!"
}
为了使这些值进入UI,必须显式地对UI进行编程以使用这些文件。
这通常涉及国际化库或内置语言特性,其目的是将UI文本替换为给定区域设置的正确值。这些库的例子包括i18 next(JavaScript)、Babel(Python)和go-i18 n(Go)。
Java支持开箱即用的国际化,因此我们不需要在项目中引入额外的依赖项。
查看来源
Java使用扩展名为.properties的文件来存储用户界面的本地化字符串。
幸运的是,这个项目中已经有一群人了。例如,以下是我们为英语和西班牙语提供的内容:
messages.properties
ini
welcome=Welcome
required=is required
notFound=has not been found
duplicate=is already in use
nonNumeric=must be all numeric
duplicateFormSubmission=Duplicate form submission is not allowed
typeMismatch.date=invalid date
typeMismatch.birthDate=invalid date
messages_es.properties:
ini
welcome=Bienvenido
required=Es requerido
notFound=No ha sido encontrado
duplicate=Ya se encuentra en uso
nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida
外部化UI字符串并不是所有项目都要做的事情。有些项目可能将这些文本直接硬编码到应用程序逻辑中。
提示:外部化UI文本是一个很好的实践,它的优点超出了国际化。它使代码更容易维护,并促进UI消息的一致性。如果您正在启动一个项目,请考虑尽早实施i18n。
测试运行
让我们添加一种通过URL参数更改区域设置的方法。这将使我们能够测试所有内容是否完全外部化并至少翻译为一种语言。
为此,我们添加以下类来管理locale参数:
WebConfig.java
java
import java.util.Locale;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
现在我们可以测试不同的语言环境,我们运行服务器,并比较主页的几个语言环境参数:
更改区域设置会反映在UI中,这是个好消息。然而,改变语言环境似乎只影响了一部分文本。对于西班牙语,Welcome已更改为Bienvenido,但标题中的链接保持不变,其他页面仍为英文。这意味着我们有一些工作要做。
准备本地化
Petclinic项目使用Thymeleaf模板生成页面,因此让我们检查模板文件。
实际上,有些文本是硬编码的,所以我们需要修改代码来引用资源包。
幸运的是,Thymeleaf对Java .properties
文件有很好的支持,所以我们可以在模板中合并对相应资源包键的引用。
Before(findOwners.html):
css
<h2>Find Owners</h2>
后
(findOwners.html):
css
<h2 th:text='#{heading.find.owners}'>Find Owners</h2>
(messages.properties):
ini
heading.find.owners=Find Owners
以前硬编码的文本仍然存在,但现在它作为一个回退值,只有在检索正确的本地化消息时出错时才使用。
其余的文本以类似的方式外化,然而,有几个地方需要特别注意。例如,一些警告来自验证引擎,必须使用Java注释参数指定:
之前(Person.java):
less
@Column(name = "first_name")
@NotBlank
private String firstName;
After(Person.java):
less
@Column(name = "first_name")
@NotBlank(message = "{field.validation.notblank}")
private String firstName;
在一些地方,逻辑必须改变:
createOrUpdatePetForm.html:
less
<h2>
<th:block th:if="${pet['new']}">New </th:block>Pet
</h2>
在上面的例子中,模板使用了一个条件。如果存在new
属性,则将New添加到UI文本。因此,结果文本是New Pet还是Pet取决于属性的存在。
由于名词和形容词之间的一致性,这可能会破坏某些区域设置的本地化。例如,在西班牙语中,形容词将是Nuevo
或Nueva
,这取决于名词的性别,现有的逻辑并没有考虑到这种区别。
这种情况的一个可能的解决方案是使逻辑更加复杂。一般来说,尽可能避免复杂的逻辑是一个好主意,所以我选择了解耦分支:
createOrUpdatePetForm.html:
less
<h2>
<th:block th:if="${pet['new']}" th:text="#{pet.new}">New Pet</th:block>
<th:block th:unless="${pet['new']}" th:text="#{pet.update}">Pet</th:block>
</h2>
独立的分支也将简化翻译过程和代码库的未来维护。
新的宠物形式也有一个技巧。它的Type下拉列表是通过将pet类型的集合传递给selectField.html
模板来创建的:
css
<input th:replace="~{fragments/selectField :: select (#{pet.type}, 'type', ${types})}" />
与其他UI文本不同,pet类型是应用程序数据模型的一部分。它们在运行时来自数据库。这些数据的动态特性使我们无法直接将文本提取到属性包中。
同样有几种方法来处理这个问题。一种方法是在模板中动态构造属性包键:
之前(selectField.html):
ini
<option th:each="item : ${items}"
th:value="${item}"
th:text="${item}">dog</option>
After(selectField.html):
ini
<option th:each="item : ${items}"
th:value="${item}"
th:text="#{'pettype.' + ${item}}">dog</option>
在这种方法中,我们不是直接在UI中呈现cat
,而是在它前面加上pettype
,这会导致pettype.cat
。然后我们使用这个字符串作为键来检索本地化的UI文本:
messages.properties
ini
pettype.bird=bird
pettype.cat=cat
pettype.dog=dog
messages_es.properties:
ini
pettype.bird=pájaro
pettype.cat=gato
pettype.dog=perro
您可能已经注意到,我们刚刚修改了一个可重用组件的模板。由于可重用组件旨在为多个客户端提供服务,因此将客户端逻辑带入其中是不正确的。
在这个特殊的例子中,下拉列表组件被绑定到宠物类型,这对于任何想要将其用于其他任何事情的人来说都是有问题的。
这个缺陷从一开始就存在-请参阅dog
作为选项的默认文本。我们只是进一步传播了这个缺陷。这不应该在真实的项目中完成,需要重构。
当然,还有更多的项目代码需要国际化;然而,其余的大多与上面的示例一致。要完整查看我的所有更改,欢迎您检查[我的fork中的提交(https://github.com/flounder4130/spring-petclinic)。
添加缺少的关键点
在将所有UI文本替换为对属性包键的引用之后,我们必须确保引入所有这些新键。此时我们不需要翻译任何东西,只需将密钥和原始文本添加到messages.properties
文件中即可。
IntelliJ IDEA有很好的Thymeleaf支持。它可以检测模板是否引用了缺失的属性,因此您可以在没有大量手动检查的情况下发现缺失的属性:
所有的准备工作完成后,我们进入了工作中最有趣的部分。我们有所有的钥匙,我们有英语的所有价值观。我们从哪里获得其他语言的值?
翻译文本
为了翻译文本,我们将创建一个使用外部翻译服务的脚本。有很多可用的翻译服务,以及许多编写此类脚本的方法。
我为实现做出了以下选择:
- Python作为编程语言,因为它允许你非常快地编写小任务
- DeepL作为翻译服务。 最初,我计划使用OpenAI的GPT3.5 Turbo,但由于它不是严格的翻译模型,因此需要额外的努力来配置提示符。而且,结果往往不太稳定,所以我选择了一个专门的翻译服务,第一次出现在脑海中
我没有做广泛的研究,所以这些选择有点武断。请随意尝试并发现最适合您的产品。
注意事项:
如果你决定使用下面的脚本,你需要在DeepL创建一个账户,并通过DEEPL_KEY
环境变量将你的个人API密钥传递给脚本。
这是脚本:
python
import os
import requests
import json
deepl_key = os.getenv('DEEPL_KEY')
properties_directory = "../src/main/resources/messages/"
def extract_properties(text):
properties = {}
for line in text:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key_value = line.split('=')
key = key_value[0].strip()
value = key_value[1].strip()
if key and value:
properties[key] = value
return properties
def missing_properties(properties_file, properties_checklist):
with open(properties_file, 'r') as f:
text = f.readlines()
present_properties = extract_properties(text)
missing = {k: v for k, v in properties_checklist.items() if k not in present_properties.keys()}
return missing
def translate_property(value, target_lang):
headers = {
'Content-Type': 'application/json',
'Authorization': f'DeepL-Auth-Key {deepl_key}',
'User-Agent': 'LocalizationScript/1.0'
}
url = 'https://api-free.deepl.com/v2/translate'
data = {
'text': [value],
'source_lang': 'EN',
'target_lang': target_lang,
'preserve_formatting': True
}
response = requests.post(url, headers=headers, data=json.dumps(data))
return response.json()["translations"][0]["text"]
def populate_properties(file_path, properties_checklist, target_lang):
with open(file_path, 'a+') as file:
properties_to_translate = missing_properties(file_path, properties_checklist)
for key, value in properties_to_translate.items():
new_value = translate_property(value, target_lang)
property_line = f"{key}={new_value}\n"
print(property_line)
file.write(property_line)
with open(properties_directory + 'messages.properties') as base_properties_file:
base_properties = extract_properties(base_properties_file)
languages = [
# configure languages here
"nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "fi"
]
for language in languages:
populate_properties(properties_directory + f"messages_{language}.properties", base_properties, language)
该脚本从默认属性包(messages.properties
)中提取键,并在特定于区域设置的包中查找它们的翻译。如果它发现某个键缺少翻译,脚本将从DeepL API请求翻译并将其添加到属性包中。
我指定了10种目标语言,但你可以修改列表或添加你的首选语言,只要DeepL支持它们。
该脚本可以进一步优化,以50个为一批发送文本进行翻译。我在这里做不是为了让事情简单化。
运行10种语言的脚本花了我大约5分钟。使用仪表板显示8348个字符,如果我们使用付费计划,这将花费€0.16。
运行脚本后,将显示以下文件:
messages_fi.properties
messages_fr.properties
messages_it.properties
messages_ja.properties
messages_nl.properties
messages_pt.properties
messages_ru.properties
messages_zh.properties
此外,缺少的属性将添加到:
messages_de.properties
messages_es.properties
但是结果呢?我们可以看了吗?
检查结果
让我们重新启动应用程序,并使用不同的lang
参数值进行测试。举例来说:
- http://localhost:8080/?lang=es
- http://localhost:8080/?lang=nl
- http://localhost:8080/?lang=zh
- http://localhost:8080/?lang=fr
就我个人而言,看到每个页面都被正确地本地化是非常令人满意的。我们付出了一些努力,现在得到了回报:
解决这些问题
结果令人印象深刻。然而,如果你仔细观察,你可能会发现由于缺少上下文而产生的错误。举例来说:
ini
visit.update = Visit
Visit
既可以是名词,也可以是动词。如果没有额外的上下文,翻译服务会在某些语言中产生不正确的翻译。
这可以通过手动编辑或调整翻译工作流程来解决。一个可能的解决方案是使用注释在.properties
文件中提供上下文:
ini
# Noun. Heading. Displayed on the page that allows the user to edit details of a veterinary visit
visit.update = Visit
然后我们可以修改翻译脚本来解析这样的注释,并使用context
参数传递它们:
kotlin
url = 'https://api-free.deepl.com/v2/translate'
data = {
'text': [value],
'source_lang': 'EN',
'target_lang': target_lang,
'preserve_formatting': True,
'context': context
}
随着我们深入研究并考虑更多的语言,我们可能会遇到更多需要改进的东西。这是一个反复的过程。
如果在这个过程中有一件事是必不可少的,那就是审查和测试。无论是提高自动化程度还是编辑其输出,我们都会发现有必要进行质量控制和评估。
超范围
Spring Petclinic是一个简单而现实的项目,就像我们刚刚解决的问题一样。当然,本地化带来了许多挑战,这些挑战超出了本文的范围,包括:
- 使模板适应目标语法规则
- 货币、日期和数字格式
- 不同的阅读模式,如RTL
- 调整UI以适应不同的文本长度
总结
好了,现在我们已经完成了应用程序的本地化,是时候反思一下我们学到的东西了:
- 本地化不仅涉及文本翻译,还影响相关资产、子系统和流程
- 虽然人工智能在某些本地化阶段非常有效,但要达到最佳效果,人工监督和测试仍然是必要的
- 自动翻译的质量取决于各种因素,包括上下文的可用性,以及在LLM的情况下,适当的书面提示