实时多语言 WPF 演示
使用 Google AJAX Language API 实时翻译 WPF 用户界面。

引言
在应用程序开发周期的早期阶段,您需要考虑的一个问题是是否希望为最终用户提供多语言支持。我们在 Code Project 上找到了两篇使用各种方法解决此问题的文章(有关更多信息,请参阅 使用 Locbaml 本地化 WPF 应用程序[^] 和 运行时 WPF 多语言[^])。随着我们最新产品 Vidyano 的开发,我们的目标是为开发人员提供一套工具,让他们能够更快地创建完整的 WPF 应用程序。这两种方法都未能真正满足我们的需求,因为它们对我们设想的方式来说过于复杂。有鉴于此,我们采用了完全不同的方法,并希望与社区分享,希望能有更多的应用程序提供多语言用户界面支持。
重复造轮子?
有了当今的现代翻译软件,我们已经拥有了翻译应用程序所需的所有工具,而无需聘请独立的翻译机构。今年三月,Google 推出了一个名为 Google AJAX Language API[^] 的新在线服务。该服务允许我们在网页或外部应用程序中翻译文本块。这使我们开始思考从应用程序内部提供翻译服务的整个概念。如果我们能依赖此服务为最终用户提供他们当前使用的应用程序的快速粗略翻译,那不是很棒吗?当然,目前这种翻译几乎永远不会完全符合您的要求,但它可以让您对应用程序进行基本翻译,并在后期进行微调。我们甚至可以将此基本翻译交给独立的翻译机构,让他们进行清理。
API 页面上有几个示例,展示了如何从非 Javascript 语言调用该服务。要开始,我们只需要在任何 .NET 语言中实现此功能。
进行调用
要获得语言 API 响应,我们需要调用的 URL 如下: https://ajax.googleapis.ac.cn/ajax/services/language/translate?v=1.0&q=Hello%World&langpair=en%7Cfr。
上面 URL 中的“Hello%20World”是我们想要翻译的文本,以及最后的语言对,我们查询该服务以将此文本块从英语翻译成法语。截至撰写本文时,已有不少于 24 种可用语言。
让我们用 C# 来调用它
// Create a WebRequest, passing in the text to translate along with
// the source and target language code
var req = (HttpWebRequest)WebRequest.Create(string.Format(
"https://ajax.googleapis.ac.cn/ajax/services/language/translate?v=1.0&q={0}&langpair={1}%7C{2}",str,
source.Code,
target.Code));
// req.Referer = Get your Google API Key at
// http://code.google.com/apis/ajaxsearch/key.html (Note: You must supply a valid
// referer header !)
...
WebResponse response = req.GetResponse();
var streamReader = new StreamReader(response.GetResponseStream());
此 streamReader
将保存我们请求的响应。对于 Google AJAX API,此响应以 JSON 格式返回。幸运的是,.NET 中已经有一个名为 JSON.NET[^] 的 JSON 项目。我们可以使用此库将响应反序列化为 .NET 对象。
我们需要两个类来存储响应数据
/// <summary>
/// JSON Response Class.
/// </summary>
class TranslationResponse
{
[JsonProperty("responseData")]
public Translation Data { get; set; }
[JsonProperty("responseDetails")]
public string Details { get; set; }
[JsonProperty("responseStatus")]
public int Status { get; set; }
}
/// <summary>
/// JSON Translation Response.
/// </summary>
class Translation
{
[JsonProperty("translatedText")]
public string TranslatedText { get; set; }
}
现在我们有了可以保存响应数据的类定义,我们可以使用 JSON.NET 反序列化器来获取 TranslationResponse
的实例。
var serializer = new JsonSerializer();
var translationResponse = (TranslationResponse)serializer.Deserialize(
new StringReader(streamReader.ReadToEnd()), typeof(TranslationResponse));
translationResponse.TranslatedText
属性将返回我们请求的翻译文本。
应用扩展方法
我们刚刚编写的代码可以很容易地写入字符串类的扩展方法中。我们的静态 GoogleTranslateExtensions
类实现了这个扩展方法。
public static string Translate(this string str, Languages.Language source,
Languages.Language target)
Languages.Language
是一个嵌套类,包含语言描述、ISO 代码以及定义该语言是 LeftToRight
还是 RightToLeft
的标志。
public class Language
{
internal Language(string desc, string code) :
this(desc, code, false) { }
internal Language(string desc, string code, bool rightToLeft)
{
Description = desc;
Code = code;
RightToLeft = rightToLeft;
}
public string Description { get; private set; }
public string Code { get; private set; }
public bool RightToLeft { get; private set; }
}
静态 Languages
类是为了方便而存在的。它为每种可用语言返回一个 Language 类的静态实例。
public static class Languages
{
static Languages()
{
English = new Language("English", "en"); languages.Add(English);
...
}
public static Language English { get; private set; }
...
}
这样,我们就可以非常轻松地在应用程序中翻译字符串,例如通过键入以下代码
public static void Main()
{
// You may have to set your proxy here first
// GoogleTranslateExtensions.Proxy = new WebProxy("xxx.xxx.xxx.xxx", 8080);
// Will write "Bonjour monde" to the console window.
Console.WriteLine("Hello World".Translate(Languages.English, Languages.French));
}
请注意 GoogleTranslateExtensions
类上的 Proxy
。您可能需要在此处设置代理。
迁移到 Windows Presentation Foundation
迁移到 WPF 时,我们需要一种方法来翻译我们在 XAML 页面中定义的所有硬编码字符串。有几种方法可以实现这一点,例如,我们可以使用方法绑定并将字符串作为参数传递。但是,我们决定通过提供一个标记扩展来实现此功能,该扩展可以轻松地包装需要翻译的字符串。这就是 Vidyano.Presentation 项目中定义的 TranslateExtension
类。此标记扩展在构造函数中接受一个字符串作为参数。
<TextBlock Text="{vi:Translate Hello World}" />
TranslateExtension
类继承自抽象的 MarkupExtension 类。此标记扩展将在 .NET 运行时调用其 ProvideValue 方法时返回一个 BindingExpression,因此其定义如下
[MarkupExtensionReturnType(typeof(BindingExpression))]
public class TranslateExtension : MarkupExtension
在我们深入研究这个类之前,我想先向您展示另一个类,即 LanguageSelector
类。这个类是一个自定义的 ContentControl 控件类,并定义了我们翻译的范围。在其内容中的每个对象都可以定义 {vi:Translate ... }
标记扩展。这个包装器控件的原因是标记扩展需要知道从哪种语言翻译到哪种语言。这是通过在 LanguageSelector
类上添加两个附加属性来实现的:SourceLanguageProperty
和 TargetLanguageProperty
。通过使用 FrameworkPropertyMetadataOptions.Inherits
选项定义这些附加属性,我们有效地允许树中的所有子对象查询当前源语言和目标语言。
所以,以下 XAML 代码是您用来翻译 TextBlock 中某些文本的代码。
<vi:LanguageSelector xml:lang="en-US">
<TextBlock Text="{vi:Translate Hello World}" />
</vi:LanguageSelector>
xml:lang
属性定义此范围的源语言。现在让我们回到我们的标记扩展。
正如我之前提到的,TranslateExtension
类从其 ProvideValue
方法返回一个 BindingExpression
。您可能会想,为什么我们不直接返回翻译后的文本。这是因为我们希望在更改 LanguageSelector
父控件上的目标语言时更新文本。为了实现这一点,我们需要在返回的绑定上设置一个转换器。这就是 TranslateConverter
类。然而,这个转换器类比一般的转换器要复杂一些。
与任何转换器类一样,此类实现 IValueConverter
接口。这个转换器类与普通转换器不同的是它继承自 FrameworkElement
类。这允许我们在转换器上定义两个依赖属性:源语言和目标语言,我们将它们绑定到 LanguageSelector
上的附加属性。现在,当 Convert
方法在我们的转换器上被调用时,转换器将使用其源语言和目标语言依赖属性来调用我们在文章开头创建的 Translate
扩展方法。
Languages.Language sourceLang = Languages.FromString(SourceLanguage);
Languages.Language targetLang = Languages.FromString(TargetLanguage);
if (sourceLang != null && targetLang != null && sourceLang != targetLang)
{
// Asynchronously invoke the Translate method
Action translate = () => translation = text.Translate(sourceLang, targetLang);
translate.BeginInvoke(Translated, null);
// Return "Loading..." as long as the translation is in progress
return LanguageSelector.GetLoadingString(targetObject);
}
return text;
这里有一个窍门,Convert
方法只被调用一次,只要文本不改变。所以我们需要找到一种方法来触发这个转换器的失效,只要源语言或目标语言发生变化。这是通过钩入我们依赖属性的更改处理程序来实现的:SourceLanguageChanged
和 TargetLanguageChanged
。
在这些处理程序方法中,我们确保调用以下代码
var converter = obj as TranslateConverter;
if (converter != null)
{
// Invalidate the binding on our target object
BindingExpressionBase expression = BindingOperations.GetBindingExpressionBase(
converter.targetObject, converter.targetProperty);
if (expression != null)
expression.UpdateTarget();
}
targetObject
和 targetProperty
在构造函数中被传递给我们。让我们来看看我们转换器的创建。
// Get the TargetObject and TargetProperty via the IProvideValueTarget service
var provideValueService = (IProvideValueTarget)serviceProvider.GetService(
typeof(IProvideValueTarget));
if (provideValueService == null)
return null;
var targetObject = provideValueService.TargetObject as DependencyObject;
var targetProperty = provideValueService.TargetProperty as DependencyProperty;
if (targetObject != null && targetProperty != null)
{
// There might already be a Binding
if (Binding == null)
Binding = new Binding();
// Create the Converter, passing the targetObject and targetProperty
var converter = new TranslateConverter(targetObject, targetProperty);
Binding.Converter = converter;
// Text may be string.Empty if a the markup extension is created with a Binding
Binding.ConverterParameter = Text;
// Bind the converter's SourceLanguageProperty and TargetLanguageProperty
// to the attached properties
var sourceLanguageBinding = new Binding
{
Path = new PropertyPath("(0)", LanguageSelector.SourceLanguageProperty),
Source = targetObject
};
var targetLanguageBinding = new Binding
{
Path = new PropertyPath("(0)", LanguageSelector.TargetLanguageProperty),
Source = targetObject
};
converter.SetBinding(TranslateConverter.SourceLanguageProperty,
sourceLanguageBinding);
converter.SetBinding(TranslateConverter.TargetLanguageProperty,
targetLanguageBinding);
// Return the new/updated binding
return Binding.ProvideValue(serviceProvider);
}
return null;
首先要注意的是 Binding
。您也可以在具有绑定而不是硬编码字符串的标记扩展中使用。这允许您超越简单的 UI 翻译,还可以为您的数据提供翻译。
<vi:LanguageSelector xml:lang="en-US">
<TextBlock Text="{vi:Translate Binding={Binding Description}}" />
</vi:LanguageSelector>
在上面的示例中,文本块将绑定到当前 DataContext
对象上的 Description
属性。每当目标语言或源语言发生变化,或者 Description
属性发生变化时,文本块都会刷新其 Text。
这就是我们现在需要分享的内容,以便实现实时多语言应用程序。我只想补充一点。
缓存和微调翻译
为了减轻语言 API 的网络请求压力,我们在项目中添加了一个小缓存。需要翻译的文本将首先在小型 SQL Compact 数据库文件中进行检查,该文件将在程序开始翻译时复制到您的工作目录(如果尚不存在)。除了缓存功能外,此本地存储还为您的应用程序添加了另一项强大功能。
众所周知,有时翻译会有点“离谱”。通过打开 WPF 窗口并选择目标语言,所有翻译都会写入此缓存。这意味着如果您更改本地缓存中的行,您可以微调您的翻译。下次打开应用程序或更改目标语言时,您将从缓存中获取更准确的值,而不是 Google AJAX Language API。
lock (Cache)
{
// Look for the text block in the Source table.
langSource = Cache.Source.FirstOrDefault(s => s.LangCode == source.Code &&
s.Value == str);
if (langSource != null)
{
// Get the translation for the text block and the target language from
// the Translations table.
Translations trans = langSource.Translations.FirstOrDefault(
t => t.LangCode == target.Code);
if (trans != null)
return trans.Value;
}
else
{
// Insert the text block in the Source table.
langSource = new Source { LangCode = source.Code, Value = str };
Cache.Source.InsertOnSubmit(langSource);
Cache.SubmitChanges();
}
}
...
lock (Cache)
{
// Some other thread might already added this information, so check first.
Translations trans = langSource.Translations.FirstOrDefault(
t => t.LangCode == target.Code);
if (trans == null)
{
// Add the new translation for the text block.
langSource.Translations.Add(new Translations {
LangCode = target.Code, Value = translationResponse.Data.TranslatedText });
Cache.SubmitChanges();
}
}
结论
这就是我们这次想与您分享的内容。当然,这只是多语言支持的一个非常基本实现,但我们希望您能看到它带来的强大功能。 Vidyano 的下一个 CTP 版本将比这走得更远。