支持不同的语言和文化

应用包含可能专门针对特定文化而设计的资源。例如,应用可以包含特定于文化的字符串,这些字符串将转换为当前语言区域的语言。将特定于文化的资源与应用的其余内容分开是一种很好的做法。Android 会根据系统语言区域设置来解析特定于语言和文化的资源。您可以使用 Android 项目中的资源目录为不同的语言区域提供支持。

您可以指定根据使用应用的用户的文化量身定制的资源。您可以提供适合用户的语言和文化的任何资源类型。例如,以下屏幕截图显示了一个应用,它按设备的默认 (en_US) 语言区域和西班牙语 (es_ES) 语言区域显示字符串和可绘制资源。

应用根据当前的语言区域显示不同的文本和图标

图 1. 应用根据当前的语言区域使用不同的资源

如果您是使用 Android SDK 工具创建的项目(请阅读创建 Android 项目),则这些工具会在项目的顶层创建一个 res/ 目录。此 res/ 目录包含用于存放各类资源的子目录。此外,还有一些默认文件,如 res/values/strings.xml,该文件用于存放字符串值。

支持不同的语言不仅仅是使用特定于语言区域的资源。某些用户为其界面语言区域选择的语言(如阿拉伯语或希伯来语)使用从右到左 (RTL) 脚本。其他用户查看或生成内容时所用的语言则使用 RTL 脚本,即使他们已将某种使用 LTR 脚本的语言(如英语)设为其界面语言区域。要同时支持这两类用户,您的应用需要执行以下操作:

  • 对 RTL 语言区域采用 RTL 界面布局。
  • 检测并声明在设置了格式的消息中显示的文本数据的方向。通常,您只需调用一种方法来帮您确定文本数据的方向。

创建语言区域目录和资源文件

要添加对更多语言区域的支持,请在 res/ 内创建额外的目录。每个目录的名称都应遵循以下格式:

    <resource type>-b+<language code>[+<country code>]
    

例如,values-b+es/ 包含适用于语言代码为 es 的语言区域的字符串资源。同样,mipmap-b+es+ES/ 包含适用于语言代码为 es 且国家/地区代码为 ES 的语言区域的图标。Android 在运行时根据设备的语言区域设置加载适当的资源。有关详情,请参阅提供备用资源

决定要支持的语言区域后,请创建资源子目录和文件。例如:

    MyProject/
        res/
           values/
               strings.xml
           values-b+es/
               strings.xml
           mipmap/
               country_flag.png
           mipmap-b+es+ES/
               country_flag.png
    

例如,下面是一些适用于不同语言的不同资源文件:

英语字符串(默认语言区域),/values/strings.xml

    <resources>
        <string name="hello_world">Hello World!</string>
    </resources>
    

西班牙语字符串(语言区域为 es),/values-es/strings.xml

    <resources>
        <string name="hello_world">¡Hola Mundo!</string>
    </resources>
    

美国国旗图标(默认语言区域),/mipmap/country_flag.png

美国国旗的图标

图 2. 用于默认 (en_US) 语言区域的图标

西班牙国旗图标(语言区域为 es_ES),/mipmap-b+es+ES/country_flag.png

西班牙国旗的图标

图 3. 用于 es_ES 语言区域的图标

注意:您可以对任何资源类型使用语言区域限定符(或任何配置限定符),例如,如果您要提供可绘制位图资源的本地化版本,就可以这样做。有关详情,请参阅本地化

使用应用中的资源

您可以使用每个资源的 name 属性,在源代码和其他 XML 文件中引用相应的资源。

在源代码中,您可以使用 R.<resource type>.<resource name> 语法来引用资源。有多种方法以这种方式接受资源。

例如:

Kotlin

    // Get a string resource from your app's Resources
    val hello = resources.getString(R.string.hello_world)

    // Or supply a string resource to a method that requires a string
    TextView(this).apply {
        setText(R.string.hello_world)
    }
    

Java

    // Get a string resource from your app's Resources
    String hello = getResources().getString(R.string.hello_world);

    // Or supply a string resource to a method that requires a string
    TextView textView = new TextView(this);
    textView.setText(R.string.hello_world);
    

在其他 XML 文件中,只要 XML 属性接受兼容值,您就可以使用 @<resource type>/<resource name> 语法来引用资源。

例如:

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/country_flag" />
    

设置消息中文本的格式

应用中最常见的任务之一就是设置文本的格式。将文本和数字数据插入适当的位置,即可设置本地化消息的格式。遗憾的是,在处理 RTL 界面或 RTL 数据时,简单的格式设置可能会显示不正确甚至无法读取的文本输出。

阿拉伯语、希伯来语、波斯语和乌尔都语等语言总体上按 RTL 方向编写。不过,它们的某些元素(如数字和嵌入式 LTR 文本)却在 RTL 文本中按 LTR 方向编写。使用 LTR 脚本的语言(包括英语)也是双向的,因为它们可以包含需要按 RTL 方向显示的嵌入式 RTL 脚本。

大多数时候,此类嵌入式反向文本实例是由应用本身生成的。它们将任意语言的文本数据按任意文本方向插入本地化消息。这样混合方向时,通常不会明确指出反向文本的开始和结束位置。大多数问题都是由应用生成的文本所具有的这些特征导致的。

虽然系统对双向文本的默认处理方式通常会按预期呈现文本,但是当您的应用将文本插入本地化消息时,有可能文本无法正确呈现。下面举例说明了文本在什么情况下更有可能无法正确显示:

  • 在消息的最开头插入:

    PERSON_NAME 正在呼叫您

  • 以数字开头,如在地址或电话号码中:

    987 654-3210

  • 以标点符号开头,如在电话号码中:

    +19876543210

  • 以标点符号结尾:

    确定吗?

  • 已包含两个方向:

    בננה 一词是希伯来语,意思是香蕉。

示例

例如,假设某个应用有时需要显示消息“您是不是要找 %s?”,并在运行时插入地址来代替 %s。由于该应用支持不同的界面语言区域,因此该消息来自特定于语言区域的资源,并且在使用 RTL 语言区域时使用 RTL 方向。在希伯来语界面中,它应如下所示:

האם התכוונת ל %s?

不过,建议来自的数据库可能不包含采用相应语言区域的语言的文本。例如,如果相关地址是加利福尼亚州某个地方的地址,那么它会使用英语文本显示在数据库中。如果您将地址“15 Bay Street, Laurel, CA”插入 RTL 消息而不提供任何有关文本方向的提示,那么结果会不符合预期或不正确:

האם התכוונת ל 15 Bay Street, Laurel, CA?

请注意,门牌号显示在地址的右侧,而不是像预期那样显示在左侧,这使门牌号看起来更像一个奇怪的邮政编码。如果在使用 LTR 文本方向的消息中包含 RTL 文本,可能会出现同样的问题。

解释说明和解决方案

之所以出现上例中的问题,是因为文本格式设置工具没有指定“15”是地址的一部分,因此系统无法确定“15”是它前面的 RTL 文本的一部分,还是它后面的 LTR 文本的一部分。

要解决此问题,请对插入本地化消息的段文本使用 BidiFormatter 类中的 unicodeWrap() 方法。只有在个别情况下,才不应使用 unicodeWrap(),其中包括:

  • 正在将文本插入机器可读的字符串,如 URI 或 SQL 查询。
  • 您已经知道正确封装了这段文本。

unicodeWrap() 方法会检测字符串的方向,并以用来声明该方向的 Unicode 格式字符封装该字符串。由于“15”现在出现在声明为 LTR 的文本中,因此它显示在正确的位置:

האם התכוונת ל 15 Bay Street, Laurel, CA?

以下代码段演示了如何使用 unicodeWrap()

Kotlin

    val mySuggestion = "15 Bay Street, Laurel, CA"
    val myBidiFormatter: BidiFormatter = BidiFormatter.getInstance()

    // The "did_you_mean" localized string resource includes
    // a "%s" placeholder for the suggestion.
    String.format(getString(R.string.did_you_mean), myBidiFormatter.unicodeWrap(mySuggestion))
    

Java

    String mySuggestion = "15 Bay Street, Laurel, CA";
    BidiFormatter myBidiFormatter = BidiFormatter.getInstance();

    // The "did_you_mean" localized string resource includes
    // a "%s" placeholder for the suggestion.
    String.format(getString(R.string.did_you_mean),
            myBidiFormatter.unicodeWrap(mySuggestion));
    

注意:如果您的应用以 Android 4.3(API 级别 18)或更高版本为目标平台,请使用 Android 框架中的 BidiFormatter 版本。否则,请使用支持库中的 BidiFormatter 版本。

设置数字的格式

您可以使用格式字符串(而非方法调用)在应用的逻辑中将数字转换为字符串:

Kotlin

    var myIntAsString = "$myInt"
    

Java

    String myIntAsString = String.format("%d", myInt);
    

这样将针对您的语言区域适当地设置数字格式,可能包括使用不同的数字集。

当您使用 String.format() 在某个设备上创建 SQL 查询并且该设备的语言区域(如波斯语和大多数阿拉伯语语言区域)使用其自己的数字集时,如果查询的任何参数是数字,就会出现问题。这是因为作为参数的数字设置成了该语言区域的数字格式,而该语言区域的这些数字在 SQL 中无效。

要保留 ASCII 格式的数字并使 SQL 查询保持有效,您应改用包含语言区域作为第一个参数的 String.format() 过载版本。该语言区域参数应该为 Locale.US

支持布局镜像

使用 RTL 脚本的用户更喜欢 RTL 界面,这种界面包含右对齐菜单、右对齐文本和指向左侧的向前箭头。

图 4 显示了“设置”应用中某个屏幕的 LTR 版本与对应的 RTL 版本之间的对比:

通知区域靠近右上角右对齐,应用栏中的菜单按钮靠近左上角,屏幕主要部分中的内容左对齐且按 LTR 显示,后退按钮靠近左下角且指向左侧。 通知区域靠近左上角左对齐,应用栏中的菜单按钮靠近右上角,屏幕主要部分中的内容右对齐且按 RTL 显示,后退按钮靠近右下角且指向右侧。
图 4. 屏幕的 LTR 和 RTL 变体

向您的应用添加 RTL 支持时,记住以下几点特别重要:

  • 只有在运行 Android 4.2(API 级别 17)或更高版本的设备上使用时,才会在应用中支持 RTL 文本镜像。要了解如何在旧款设备上支持文本镜像,请参阅对旧版应用提供支持
  • 要测试您的应用是否支持 RTL 文本方向,请使用开发者选项进行测试,并邀请使用 RTL 脚本的用户使用您的应用。

注意:要查看与布局镜像相关的其他设计指南,包括您应该和不应该镜像的元素的列表,请参阅双向性 Material Design 指南。

要镜像应用中的界面布局以使其在 RTL 语言区域中按 RTL 显示,请完成下面几部分中的步骤。

修改编译和清单文件

修改应用模块的 build.gradle 文件和应用清单文件,如下所示:

build.gradle(Module:app)

    android {
        ...
        defaultConfig {
            targetSdkVersion 17 // Or higher
            ...
        }
    }
    

AndroidManifest.xml

    <manifest ... >
        ...
        <application ...
            android:supportsRtl="true">
        </application>
    </manifest>
    

注意:如果您的应用以 Android 4.1.1(API 级别 16)或更低版本为目标平台,那么系统会忽略 android:supportsRtl 属性,以及显示在应用布局文件中的任何 startend 属性值。在这种情况下,不会在您的应用中自动发生 RTL 布局镜像。

更新现有资源

在每个现有布局资源文件中,将 leftright 分别转换为 startend。这样做以后,就允许框架根据用户的语言设置来对齐应用的界面元素。

注意:在更新资源之前,请了解如何对旧版应用提供支持,或对以 Android 4.1.1(API 级别 16)及更低版本为目标平台的应用提供支持。

要使用框架的 RTL 对齐功能,请更改表 1 中显示的布局文件中的属性。

表 1. 应用支持多种文本方向时使用的属性

仅支持 LTR 的属性 支持 LTR 和 RTL 的属性
android:gravity="left" android:gravity="start"
android:gravity="right" android:gravity="end"
android:layout_gravity="left" android:layout_gravity="start"
android:layout_gravity="right" android:layout_gravity="end"
android:paddingLeft android:paddingStart
android:paddingRight android:paddingEnd
android:drawableLeft android:drawableStart
android:drawableRight android:drawableEnd
android:layout_alignLeft android:layout_alignStart
android:layout_alignRight android:layout_alignEnd
android:layout_marginLeft android:layout_marginStart
android:layout_marginRight android:layout_marginEnd
android:layout_alignParentLeft android:layout_alignParentStart
android:layout_alignParentRight android:layout_alignParentEnd
android:layout_toLeftOf android:layout_toStartOf
android:layout_toRightOf android:layout_toEndOf

表 2 显示了系统如何根据目标 SDK 版本、是否定义了 leftright 属性以及是否定义了 startend 属性来处理界面对齐属性。

表 2. 基于目标 SDK 版本和已定义属性的界面元素对齐行为

是否以 Android 4.2
(API 级别 17)或更高版本为目标平台?
是否定义了 left 和 right 属性? 是否定义了 start 和 end 属性? 结果
startend 得到解析,并替换 leftright
仅使用 leftright
仅使用 startend
使用 leftright(忽略 startend
仅使用 leftright
startend 解析为 leftright

添加特定于方向和语言的资源

此步骤涉及添加特定版本的布局、可绘制资源和值资源文件,这些文件包含针对不同语言和文本方向的自定义值。

对于 Android 4.2(API 级别 17)及更高版本,您可以使用 -ldrtl(布局方向从右到左)和 -ldltr(布局方向从左到右)资源限定符。为了保持与加载现有资源向后兼容,旧版 Android 系统使用资源的语言限定符来推断正确的文本方向。

假设您要添加特定的布局文件来支持 RTL 脚本,如希伯来语、阿拉伯语和波斯语的布局文件。为此,您应在 res/ 目录中添加 layout-ldrtl/ 目录,如以下示例中所示:

    res/
        layout/
            main.xml This layout file is loaded by default.
        layout-ldrtl/
            main.xml This layout file is loaded for languages using an
                     RTL text direction, including Arabic, Persian, and Hebrew.
    

如果要添加专为阿拉伯语文本设计的特定版本的布局,则目录结构将变为:

    res/
        layout/
            main.xml This layout file is loaded by default.
        layout-ar/
            main.xml This layout file is loaded for Arabic text.
        layout-ldrtl/
            main.xml This layout file is loaded only for non-Arabic
                     languages that use an RTL text direction.
    

注意:特定于语言的资源优先于特定于布局方向的资源,而特定于布局方向的资源优先于默认资源。

使用支持的微件

从 Android 4.2(API 级别 17)开始,大多数框架界面元素都自动支持 RTL 文本方向。不过,也有一些框架元素(如 ViewPager)不支持 RTL 文本方向。

主屏幕微件支持 RTL 文本方向,前提是它们对应的清单文件包含属性分配 android:supportsRtl="true"

对旧版应用提供支持

如果您的应用以 Android 4.1.1(API 级别 16)或更低版本为目标平台,那么除了 startend 之外,还应添加 leftright 属性。

要检查布局是否应使用 RTL 文本方向,请使用以下逻辑:

Kotlin

    private fun shouldUseLayoutRtl(): Boolean {
        return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
            View.LAYOUT_DIRECTION_RTL == layoutDirection
        } else {
            false
        }
    }
    

Java

    private boolean shouldUseLayoutRtl() {
        if (android.os.Build.VERSION.SDK_INT >=
                android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return View.LAYOUT_DIRECTION_RTL == getLayoutDirection();
        } else {
            return false;
        }
    }
    

注意:为避免兼容性问题,请使用 Android SDK Build Tools 23.0.1 或更高版本。

使用开发者选项进行测试

在搭载 Android 4.4(API 级别 19)或更高版本的设备上,您可以在设备上的开发者选项中启用强制使用从右到左的布局方向。此设置允许您在 RTL 模式下查看使用 LTR 脚本的文本(如英语文本)。

更新应用逻辑

本部分介绍在调整您的应用以便处理多种文本方向时应更新应用逻辑中的哪些特定位置。

属性更改

要处理与 RTL 相关的任何属性(如布局方向、布局参数、内边距、文本方向、文本对齐或可绘制资源放置)的更改,您可以使用 onRtlPropertiesChanged() 回调。通过此回调,您可以获取当前的布局方向,并相应地更新 Activity 的 View 对象。

视图

如果您要创建的界面微件不直接属于 Activity 的视图层次结构(如类似于对话框或消息框的界面元素),请根据上下文设置正确的布局方向。以下代码段演示了如何完成此过程:

Kotlin

    val config: Configuration = context.resources.configuration
    view.layoutDirection = config.layoutDirection
    

Java

    final Configuration config =
        getContext().getResources().getConfiguration();
    view.setLayoutDirection(config.getLayoutDirection());
    

View 类的几种方法需要额外考虑:

onMeasure()
视图测量数据可能会因文本方向不同而异。
onLayout()
如果您创建自己的布局实现,则需要在您的 onLayout() 版本中调用 super(),并调整自定义逻辑以支持 RTL 脚本。
onDraw()
如果您要实现自定义视图或向绘图添加高级功能,则需要更新代码以支持 RTL 脚本。您可以使用以下代码来确定微件是否处于 RTL 模式:

Kotlin

    // On devices running Android 4.1.1 (API level 16) and lower,
    // you can call the isLayoutRtl() system method directly.
    fun isLayoutRtl(): Boolean = layoutDirection == LAYOUT_DIRECTION_RTL
    

Java

    // On devices running Android 4.1.1 (API level 16) and lower,
    // you can call the isLayoutRtl() system method directly.
    public boolean isLayoutRtl() {
        return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
    }
    

可绘制资源

如果您有需要针对 RTL 布局镜像的可绘制资源,请根据设备上运行的 Android 版本完成以下某个步骤:

  • 对于运行 Android 4.3(API 级别 18)及更低版本的设备,您需要添加并定义 -ldrtl 资源文件。
  • 对于 Android 4.4(API 级别 19)及更高版本,在定义可绘制资源时,您可以使用 android:autoMirrored="true",该属性可让系统为您处理 RTL 布局镜像。

    注意android:autoMirrored 属性仅适用于简单的可绘制资源,其双向镜像只是整个可绘制资源的图形镜像。如果可绘制资源包含多个元素,或者如果反射可绘制资源会改变它的解释,那么您应自行执行镜像。请尽可能与双向镜像方面的专家核实,以确定镜像的可绘制资源对用户是否有意义。

重心

如果您的应用代码使用的是 Gravity.LEFTGravity.RIGHT,那么您需要将这些值分别更改为 Gravity.STARTGravity.END

例如,如果您使用的是以下代码:

Kotlin

    when (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
        Gravity.LEFT -> {
            // Handle objects that are left-aligned.
        }
        Gravity.RIGHT -> {
            // Handle objects that are right-aligned.
        }
    }
    

Java

    switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
        case Gravity.LEFT:
            // Handle objects that are left-aligned.
            break;
        case Gravity.RIGHT:
            // Handle objects that are right-aligned.
            break;
    }
    

那么您需要将其更改为以下代码:

Kotlin

    val absoluteGravity: Int = Gravity.getAbsoluteGravity(gravity, layoutDirection)
    when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
        Gravity.LEFT -> {
            // Handle objects that are left-aligned.
        }
        Gravity.RIGHT -> {
            // Handle objects that are right-aligned.
        }
    }
    

Java

    final int layoutDirection = getLayoutDirection();
    final int absoluteGravity =
            Gravity.getAbsoluteGravity(gravity, layoutDirection);
    switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
        case Gravity.LEFT:
            // Handle objects that are left-aligned.
            break;
        case Gravity.RIGHT:
            // Handle objects that are right-aligned.
            break;
    }
    

这意味着,您可以保留用来处理左对齐和右对齐值的现有代码,即使您正在对重心值使用 startend 也是如此。

注意:应用重心设置时,请使用包含 layoutDirection 参数的 Gravity.apply() 过载版本。

外边距和内边距

要在应用中支持 RTL 脚本,请遵循下面这些与外边距和内边距值相关的最佳做法:

另请参阅