65.9K
CodeProject 正在变化。 阅读更多。
Home

构建多语言 WPF 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (22投票s)

2009 年 8 月 4 日

CPOL

15分钟阅读

viewsIcon

99730

downloadIcon

4504

概述了如何在 WPF 应用程序中构建多语言支持,其中语言可以在运行时动态更改。

A selection of languages in the one application at runtime

目录

引言

我最近一直在进行的一项工作是为 WPF 应用程序构建多语言支持,以提高英语不好的更广泛受众的可用性。我希望在此过程中实现几个目标:

  • 让一个版本的应用程序支持多种语言。这意味着没有单独的英文版、法文版、日文版等。许多电子产品(如电视和数码相机)都支持同一型号的多种语言。您不必购买略有不同的型号或应用固件补丁才能拥有与默认安装不同的语言。
  • 允许在运行时轻松切换界面语言。无需关闭应用程序并配置操作系统区域设置,更不用说重新运行安装程序了。
  • 首次使用时选择正确的语言。首次运行应用程序时,它会将界面语言设置为操作系统指定的系统语言。这很合理——法国用户希望立即安装、运行和使用该软件,而不是在能够使用之前尝试导航一个不熟悉的应用程序来更改语言。
  • 允许 UI 可伸缩以适应翻译后的文本,最大限度地减少文本被截断的可能性。

此外,在用户界面不断增长的情况下,此实现的实现不应增加开发难度。(这一点我吃过亏。)

因此,本文旨在概述我为实现这些目标而开发的解决方案的详细信息,该解决方案基于我为此撰写的一些博客文章(在此在此,和在此)。在此过程中,我将指出示例的相关部分,让您了解所有内容是如何协同工作的。

免责声明:示例中的文本是使用在线自动翻译服务翻译的。尽管已尽最大努力确保其准确性(通过反向翻译检查),但翻译内容可能存在一些不准确或错误。尤其是在使用我无法理解的完全不同的书写系统时。

高层概述

此实现专为遵循模型-视图-视图模型(MVVM)模式的 WPF 应用程序而设计。语言数据存储在嵌入的 XML 文件中,这些文件会在需要时(即界面语言更改时)加载到内存中。这构成了模型的一部分。

视图模型向整个 WPF 应用程序公开了一个包含当前语言语言数据的属性。视图是由 XAML 文件组成的集合,其中包含此语言数据的绑定。为了选择特定文本元素所需的精确值,每个绑定都使用带有转换器参数的自定义值转换器来执行文本键查找。最后,使用自定义标记扩展来抽象绑定细节,因此只需要指定键(即转换器参数)。

示例

为了说明此实现如何实际工作,我围绕此功能创建了一个小型示例应用程序。该应用程序被称为“RePaver”,它可以整理路径标记表达式,并增加了对实际几何图形执行基本旋转、翻转、平移和缩放的功能(即无需布局变换)。在后台,它使用正则表达式提取路径段并对每个段执行就地转换。

举个例子,考虑以下您通常从导出为 XAML 的矢量图形中获得的路径表达式示例(这与我在工作中遇到的一些路径相比不算什么!)

<Path Data="M 470.567,400.914 L 470.578, 
            390.903 L466.551,390.863 L 472.6,384.876 L472.598,400.888 Z" ... />

如果您将 Data 表达式(引号内的内容)复制并粘贴到“输入”框中,然后单击“Go”,您应该会看到以下输出

M 4,16 L 4,6 L 0,6 L 6,0 L 6,16 Z

您还将在右侧获得一个方便的可视化效果,“之前”和“之后”。

随意调整设置 - 您会注意到操作的顺序是旋转/翻转 -> 缩放到 [边界框大小] -> 偏移。当然,随意尝试使用不同的语言。

模型

XML

如前所述,构成用户界面的每一条文本都以本地化形式存储在每个语言的 XML 文件中,所有 XML 文件都编译为嵌入资源。每条文本的父元素包含一个键属性,该属性用作检索本地化文本的查找。下面是英文定义文件LangEN.xml的内容示例

<LangSettings>
    <IsRtl>0</IsRtl>
    <MinFontSize>11</MinFontSize>
    <HeadingFontSize>16</HeadingFontSize>
    <UIText>
        <!-- Menu bar -->
        <Entry key="TransformLabel">Transform</Entry>
        <Entry key="LanguageLabel">Language</Entry>

        <!-- Common Operations -->
        <Entry key="ApplyLabel">Apply</Entry>
        <Entry key="UndoLabel">Undo</Entry>
        <Entry key="CancelLabel">Cancel</Entry>

        <!-- Section Headings -->
        <Entry key="InputLabel">Input</Entry>
        <Entry key="OutputLabel">Output</Entry>
        <Entry key="InfoLabel">Info</Entry>
        <Entry key="TransformPropertiesLabel">Transform</Entry>

        <!-- Item Labels -->
        <Entry key="FlipRotateLabel">Flip / Rotate</Entry>
        <Entry key="OffsetLabel">Offset</Entry>
        <Entry key="ScaleToLabel">Scale To</Entry>
        <Entry key="DimensionsLabel">Dimensions</Entry>
        <Entry key="WidthLabel">Width</Entry>
        <Entry key="HeightLabel">Height</Entry>
        <Entry key="GoLabel">Go</Entry>

    </UIText>
</LangSettings>

在上面的英文示例中,还请注意 IsRtlMinFontSizeHeadingFontSize 元素。字体大小用于确定渲染字体的尺寸,以提高可读性,尤其是在显示日语、韩语和阿拉伯语等文本时。IsRtl 元素表示语言是否从右到左阅读(如阿拉伯语和希伯来语)。

请注意,语言名称未在此 XML 中给出。这是因为本地化语言名称定义在单独的 XML 文件LanguageNames.xml

<LangNames>
    <Language code="en">English</Language>
    <Language code="ar">العربية</Language>
    <Language code="de">Deutsch</Language>
    <Language code="el">Ελληνικά</Language>
    <Language code="es">Español</Language>
    <Language code="fr">Français</Language>
    <Language code="he">עברית</Language>
    <Language code="hi">हिन्दी</Language>
    <Language code="it">Italiano</Language>
    <Language code="jp">日本語</Language>
    <Language code="ko">한국어</Language>
    <Language code="ru">Русский</Language>
    <Language code="sv">Svenska</Language>
</LangNames>

请注意,每个语言定义文件的名称都遵循一个约定:'LangXX.xml',其中 XX 对应于在 LanguageNames.xml 中为每个 Language 元素使用的两字母 ISO 语言代码。此约定可以扩展或改编以处理区域设置(例如,en-NZ、en-US)甚至三字母 ISO 语言代码。

UILanguageDefn 类

活动界面语言的语言定义文件中的数据被加载到一个内部类 UILanguageDefn 中,供应用程序其余部分使用。其主要组件是一个 Dictionary<string, string>,其中包含从文本键到本地化文本值的映射。其他属性公开 IsRtlMinFontSizeHeadingFontSize 值。

使用此类时,通过调用以下方法检索本地化语言文本

/// <summary>
/// Gets the localised text value for the given key.
/// </summary>
/// <param name="key">The key of the localised text to retrieve.</param>
/// <returns>The localised text if found, otherwise an empty string.</returns>
public string GetTextValue(string key)
{
    if (_uiText.ContainsKey(key))
        return _uiText[key];

    return "";
}

除此之外,UILanguageDefn 类还包含一个静态映射,将语言代码映射到从 LanguageNames.xml 加载的本地化语言名称,例如“en”到“English”和“sv”到“Svenska”。这用于填充“语言”选项卡中可用语言的列表,并由另一个列表进行过滤——即应用程序支持的权威语言列表。因此,即使存在语言定义文件和 LanguageNames.xml 中的条目,列表中不存在的任何语言都不会显示在 UI 中。这将在下一节中进一步说明。

加载数据

UILanguageDefn 类仅构成模型的一部分。模型中的第二个主要实体是应用程序范围的状态 MainWindowModel。它包含整个应用程序正在使用的 UILanguageDefn 的权威实例。正是这个实例通过视图模型(ViewModel)绑定到整个 UI 中的文本元素。

MainWindowModel 被构造时,它首先注册权威语言列表,并从 LanguageNames.xml 资源文件加载本地化语言名称,然后加载当前语言。以下是其工作原理的简化视图

public class MainWindowModel
{
    private UILanguageDefn _languageMapping;

    public MainWindowModel(int maxWidth, int maxHeight)
    {
        RegisterLanguages();
        LoadLanguageList();
        
        //Settings are loaded here, where CurrentLanguageCode is decided.

        UpdateLanguageData();
    }
    
    public string CurrentLanguageCode
    {
        get 
        {
            // Retrieves the current language code from
            // the Settings model (abstracted away)
        }
    }
    
    /// <summary>
    /// Registers the languages by their corresponding ISO code.
    /// </summary>
    private void RegisterLanguages()
    {
        // Defined in Constants class
        string[] supportedLanguageCodes =
        {
            "en", "ar", "de", "el", 
            "es", "fr", "ko", "hi", 
            "it", "he", "jp", "ru", "sv"  
        };
        
        foreach(string languageCode in supportedLanguageCodes)
            UILanguageDefn.RegisterSupportedLanguage(languageCode);
    }

    /// <summary>
    /// Loads the list of available languages from the embedded XML resource.
    /// </summary>
    private void LoadLanguageList()
    {
        // Defined in Constants class
        string resourcePath = "RePaverModel.LanguageData.LanguageNames.xml";
        
        System.IO.Stream file =
                Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);

        XmlDocument languageNames = new XmlDocument();
        languageNames.Load(file);

        UILanguageDefn.LoadLanguageNames(languageNames.DocumentElement);
    }

    /// <summary>
    /// Updates the UI language data from that
    /// defined in the corresponding language file.
    /// </summary>
    /// <returns>
    public bool UpdateLanguageData()
    {
        string languageCode = CurrentLanguageCode;
        if (String.IsNullOrEmpty(languageCode)) return false;

        //This follows a convention for language definition files
        //to be named 'LangXX.xml' (or 'LangXX-XX.xml')
        //where XX is the ISO language code.
        string resourcePath = 
          String.Format(Constants.LanguageDefnPathTemplate, languageCode.ToUpper());
        System.IO.Stream file =
            Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);

        XmlDocument languageData = new XmlDocument();
        languageData.Load(file);

        _languageMapping = new UILanguageDefn();
        _languageMapping.LoadLanguageData(languageData.DocumentElement);

        return true;
    }
}

您可能会注意到上面的代码提到了第三个主要实体——设置状态。正是此状态存储了当前正在使用的界面语言以及运行时可能设置的其他设置。几乎所有设置都会在应用程序关闭时持久化到磁盘,并在下次打开应用程序时重新加载。

但是,如果应用程序是首次加载(并且没有设置文件),则必须将这些设置重置为默认值。对于语言,将英语设为默认值是最简单的,但这不用户友好。相反,通过检索查询当前系统语言

CultureInfo.CurrentCulture.TwoLetterISOLanguageName;

并查找相应的语言。如果应用程序不支持该语言,则默认使用英语。这样,只要您的母语受支持,当您首次运行应用程序时,UI 将以您的母语显示。这在代码中的样子,位于设置模型层次结构中

public LanguageSettings()
{
    // Initialise the default language code.
    // In most cases this will be overwritten by the
    // restored value from the saved settings, or that of the current culture.
    _uiLanguageCode = Constants.DefaultLanguageCode;     //"en"

    string languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;

    // If the system language is supported, this will
    // ensure that the application first loads
    // with the UI displayed in that language.
    if (UILanguageDefn.AllSupportedLanguageCodes.Contains(languageCode))
        _uiLanguageCode = languageCode;
}

此类的另一个方法,稍后可能会调用(如果存在已保存的用户设置),它会用从已保存的设置文件中提取的值覆盖 _uiLanguageCode

视图模型

MVVM 实现与 WPF 和 Silverlight 应用程序中的模型-视图-呈现器(MVP)在此处有所不同。在 MVP 中,我们会有一个 Presenter,它将当前语言定义(或本地化文本的各个部分)传递给 View,View 将负责在 UI 中显示/更新文本。鉴于我们使用的是 WPF,文本可以通过数据绑定最轻松地更新;鉴于语言定义将在整个应用程序中使用(即在组件控件或窗口中),我们需要一个共享类来存储“当前语言”属性,以便在数据绑定发生时 UI 的任何部分都可以检索它。

在 MVVM 中,这样的共享类将与(例如 MainWindowViewModel)其他视图模型一起构成视图模型层。该类 CommonViewModel 被实现为单例,以便静态实例属性(Current)可以被分配为 BindingSource 属性。因此,非静态属性通过绑定的 Path 属性进行引用。另一个关键细节是,视图模型实现了 INotifyPropertyChanged 接口,以便在源值更改时 UI 可以自动更新绑定。

以下是 UI 绑定的 CommonViewModel 属性,给定 UILanguageDefn 类中定义的数据

/// <summary>
/// Gets or sets the language definition used by the entire interface.
/// </summary>
/// <value>The language definition.</value>
public UILanguageDefn LanguageDefn
{
    get { return _languageDefn; }
    set
    {
        if (_languageDefn != value)
        {
            _languageDefn = value;
            OnPropertyChanged("LanguageDefn");
            OnPropertyChanged("HeadingFontSize");
            OnPropertyChanged("MinFontSize");
            OnPropertyChanged("IsRightToLeft");
        }
    }
}

public double HeadingFontSize
{
    get 
    {
        if (_languageDefn != null)
            return (double)_languageDefn.HeadingFontSize;

        return (double)UILanguageDefn.DefaultHeadingFontSize;
    }
}

public double MinFontSize
{
    get
    {
        if (_languageDefn != null)
            return (double)_languageDefn.MinFontSize;

        return (double)UILanguageDefn.DefaultMinFontSize;
    }
}

public bool IsRightToLeft
{
    get
    {
        if (_languageDefn != null)
            return _languageDefn.IsRightToLeft;

        return false;
    }
}

MainWindowViewModel 是视图模型层次结构中最顶层的,它负责在 MainWindowModel 中的值发生更改时更新 CommonViewModel 中的当前语言。

/// <summary>
/// Refreshes the UI text to display in the current language.
/// </summary>
public void RefreshUILanguage()
{
    _model.UpdateLanguageData();
    CommonViewModel.Current.LanguageDefn = _model.CurrentLanguage;

    //Notify any other internal logic to prompt a refresh (as necessary)
    if (LanguageChanged != null)
        LanguageChanged(this, new EventArgs());
}

视图

正如我一直在提到的,本地化文本通过数据绑定在视图中显示。但是,WPF 本身不知道如何处理 UILanguageDefn,更不用说如何提取正确的本地化文本值了。这就是解谜的最后一部分。

值转换器

请记住,CommonViewModel.Current.LanguageDefn 是一个 UILanguageDefn,而不是 TextBlockText 属性所期望的 string,因此需要一个值转换器来执行此转换。此转换器使用 ConverterParameter 来指定查找键,以从 UILanguageDefn 实例中检索相应的本地化文本片段。请记住,在界面更改时更改的是 UILanguageDefn

这样做的妙处在于,对于界面中要本地化的每一段新文本,都需要将相应的元素添加到语言 XML 文件中,确保 ConverterParameter 和元素名称匹配。无需在 ViewModel、UILanguageDefn 或模型的其他部分之间定义其他属性。

转换器本身相对直接。它所需要做的就是实现 IValueConverter(在 System.Windows.Data 中),并在类级别指定 ValueConversion 属性

[ValueConversion(typeof(UILanguageDefn), typeof(string))]

并按如下方式实现 Convert 函数

public object Convert(object value, Type targetType, 
                      object parameter, CultureInfo culture)
{
    string key = parameter as string;
    UILanguageDefn defn = value as UILanguageDefn;

    if (defn == null || key == null) return "";

    return defn.GetTextValue(key);
}

绑定

现在我们有了值转换器,就可以将其全部放入 Binding 表达式中

<TextBlock Text="{Binding Path=LanguageDefn,
    Converter={StaticResource UIText}, ConverterParameter=ApplyLabel,
    Source={x:Static vm:CommonViewModel.Current}}" />

为了使此工作正常进行,必须为 vm(指向视图模型的命名空间)定义 XML 命名空间,并定义 UIText 资源(假设 conv 是值转换器的 XML 命名空间)

zlt;conv:UITextLookupConverter x:Key="UIText" />

简洁明了 - 自定义标记扩展

如果您像我一样继续使用它,您会发现重复相同的、冗长的 Binding 表达式在您的大多数 XAML 文件中会变得乏味。更不用说在重构过程中重命名类或属性了!

当然,有一种方法可以使其更加简洁,鉴于这些绑定之间唯一的变化是 ConverterParameter。我选择的解决方案是使用自定义标记扩展。

为此,自定义标记扩展只是一个派生自 MarkupExtension(在 System.Windows.Markup 中)的类,并根据约定命名为 [name]Extension。其核心是需要完成的主要工作是重写 ProvideValue 方法。但这应该做什么?

这个自定义标记扩展的全部意义在于能够像这样在 XAML 中编写

<TextBlock Text="{ext:LocalisedText Key=ApplyLabel}" />

因此,自定义扩展称为 LocalisedTextExtension,并添加了一个名为 Key 的公共 string 属性。因为后台仍然使用 Binding,所以我创建了一个私有的 Binding 字段并在构造函数中实例化它,如下所示

public LocalisedTextExtension()
{
    _lookupBinding = UITextLookupConverter.CreateBinding("");
}

其中静态 CreateBinding 方法在值转换器中定义为

public static Binding CreateBinding(string key)
{
    Binding languageBinding = new Binding("LanguageDefn")
    {
        Source = CommonViewModel.Current,
        Converter = _sharedConverter,
        ConverterParameter = key,
    };
    return languageBinding;
}

因此,在定义了 Binding 之后,Key 属性只需获取和设置 ConverterParameter。这样就剩下 ProvideValue 方法来施展其魔力

public override object ProvideValue(IServiceProvider serviceProvider)
{
    return _lookupBinding.ProvideValue(serviceProvider);
}

请记住,Binding *是*一个 MarkupExtension,因此它有自己的 ProvideValue 方法,我们可以利用它。

反复进行 - 字体大小和流向

对于某些语言,字符集包含复杂的字形,以至于在对于我们基于拉丁字母的字母表来说可以接受的字号下很难阅读。您会注意到 CommonViewModel 公开了 HeadingFontSizeMinFontSize 属性。这些属性分别提供本地化标题和所有剩余本地化文本的字体大小,例如,日语的字体大小将大于英语。

幸运的是,可以通过共享样式(在应用程序范围的资源字典中定义)以相似的模式绑定到这些样式,而无需值转换器

<Style TargetType="{x:Type TextBlock}">
    <Setter Property="FontSize" Value="{Binding Path=MinFontSize,
            Source={x:Static vm:CommonViewModel.Current}}" />
    <!-- Remaining setters ... -->
</Style>

这是一张并排图,显示了差异

Side-by-side comparison of languages with varying font size

还有一些语言是从右到左阅读的,例如阿拉伯语和希伯来语。为了正确地将 UI 本地化到这些语言,反转界面是有意义的,否则,如果逻辑顺序与阅读顺序不一致,尝试使用该应用程序将有些令人困惑。

Partial localisation of right-to-left languages

幸运的是,WPF 有一个方便的(附加)属性可以完成反转整个 UI 的繁重工作:FrameworkElement.FlowDirection

这样做非常强大,因为我只需要在包含在主窗口中的根级控件上进行一次绑定,因为这个值会被其下方的每个 FrameworkElement 在视觉层次结构中继承。此绑定只需要查看 CommonViewModelIsRightToLeft 属性,并通过(另一个值转换器)将其转换为 FlowDirection 枚举。创建了一个自定义标记扩展,遵循与之前类似的模式,以简化 XAML 到此

<Window x:Class="RePaver.UI.MainWindow" ... >
    <DockPanel FlowDirection="{ext:LocalisedFlowDirection}">
        <!-- Contents -->
    </DockPanel>
</Window>

鉴于其强大功能,以下是一些需要考虑的点/陷阱

  • 自定义面板会自动反向布局,因此您不需要创建 IsReversed 属性(或类似属性)并相应地调整 ArrangeOverride 计算。
  • 位图图像和形状(例如路径)会被反转。如果您想保留这些(例如用于公司徽标/品牌)的渲染,独立于流向,那么您需要通过将其设置为 LeftToRight 来覆盖 FlowDirection
  • 如果界面具有 RightToLeftFlowDirection 并且一个元素(例如 Image)具有 LeftToRightFlowDirection,那么该元素的 Margin 将以 RightToLeft 的方式起作用。由于 Padding 作用于元素的内部视觉层次结构,因此填充将以 LeftToRight 的方式起作用。
  • 包含语言无关数据的 TextBox 应将 FlowDirection 设置为 LeftToRight。理想情况下,这应该在样式中设置,以最大程度地减少重复并确保一致性。

所以这是必然的“之后”截图

More complete localisation of right-to-left languages

请注意,路径、旋转选择控件以及输入/输出文本框都以从左到右的方式呈现,而不管语言如何。这是因为这些元素特定于问题域,如果它们以从右到左的方式显示,那将没有意义(并且会造成一些混淆)。

结论

就这样——一个可以动态更改 UI 语言的本地化 WPF 应用程序。首次在法国的本地计算机上运行它,瞧,它以法语显示。这一切都来自同一个语言版本。

最后一点要注意但未详细介绍的是,整个 UI 以流式布局,以便自动调整布局以适应内容。不是显式设置控件的宽度和高度、网格行/列定义等,而是将它们保留为“自动”,同时可以定义最小和最大尺寸。这是更通用的最佳实践之一(而不是特定于本地化),但不遵循此实践在切换语言时会显现出来。

后记

软件开发界本地化是一个热门话题,这并不奇怪,所以自然我不是唯一一个写这个的人。事实证明,此后我发现还有其他人做过类似的事情

  • Sebastian Przybylski文章)也将 UI 文本存储在 XML 文件中作为嵌入资源,但 XAML 直接绑定到 XML 资源而不是视图模型。
  • David Sleeckx文章)使用自定义标记扩展来从本地缓存检索翻译文本,或者调用 Google 语言 API 进行实时翻译。
  • “SeriousM”已将WPF Localization Extension 上传到 CodePlex。此实现使用自定义标记扩展从资源文件/卫星资源程序集中提取本地化文本(和其他值)。

显然,现在有几种本地化 WPF 应用程序的选项——它们并非相互排斥。根据您的优先级,我实现的某些部分可能适用于您应用程序的特定区域,而替代实现的某些部分则适用于其他区域。可以根据需要进行定制。

历史

  • 2009 年 8 月 5 日:初始版本。
© . All rights reserved.