用力抱一下APP国际化

4,869 阅读9分钟

APP国际化,说的直白应该也叫本土化或者本地化,如果你的应用上线到谷歌应用市场,那么应该做好本地化的支持,用来支持不同语言及地区的风俗习惯,当然也要结合公司拓展的海外市场需要,那么对于一款应用,至少应该做到多语言和多布局的支持。
最近忙于阿拉伯语适配工作,自己便去搜罗和整理了一些,也踩过很多的坑,如果你的APP在做国际化支持,那么推荐你阅读下,这也许是篇值得参考的文章,若对你有所帮助的话,那就反手点个大大的赞哇!

国际化资源

资源是指文本字符串、布局、声音、图形和你的Android 应用需要的任何其他静态数据。

  • res/drawable/(必需的目录,包含至少一个图形文件,用作 Google Play 上的应用图标)
  • res/layout/(必需的目录,包含定义默认布局的 XML 文件)
  • res/anim/(如果您有任何 res/anim- 文件夹,则为必需)
  • res/xml/(如果您有任何 res/xml- 文件夹,则为必需)
  • res/raw/(如果您有任何 res/raw- 文件夹,则为必需)

当你的应用支持国际化,那么必须要为这些资源至少设置一套默认的资源,当应用在您没有提供特定于该语言区域的文本的语言区域中运行时,Android就会去加载一些默认的资源,所以默认资源很重要。通常默认资源被认为是你的app内部使用最多的资源,需要注意的是,在资源的加载过程中,语言区域几乎总是处于优先地位,是被系统优先加载的
除了语言区域可以作为本地化资源的区分之外,Android系统也为我们提供了两种布局方向区分,即ldrtlldltr,ldrtl 是指“布局方向从右到左”。ldltr 是指“布局方向从左到右”(默认的隐式值)。举个栗子:若我们使用阿拉伯语,则layout-ar是被优先加载的,而layout-ldrtl优先级则没有语言区域ar的优先级高,如果我们使用的是其他的RTL语言,譬如说波斯语,那么就会去加载layout-ldrtl下的资源。

res/
    layout/
        main.xml (Default layout)
    layout-ar/
        main.xml (Specific layout for Arabic)
    layout-ldrtl/
        main.xml (Any "right-to-left" language, except
                  for Arabic, because the "ar" language qualifier
                  has a higher precedence.)

针对于其他资源,譬如drawable图片、anim动画、raw静态资源和xml的本地化同样可以通过语言区域作为划分,也可以通过布局方向作为区分,所以对于本地化来说我们可以结合多种方式灵活运用他们。AS创建Resource File或者Resource Directory系统已经提供选择语言和一些特殊的区域。如下图:

国际化字符串

  • 拒绝任何形式的硬编码字符串,所有字符串应该通过string.xml资源文件加载,便于本地化
 ../values-en/strings.xml 英语
 <string name="my_topic_btn">My Topic</string>
 ../values-ar/strings.xml 阿拉伯语
 <string name="my_topic_btn">موضوعي</string>
 ../values-fr/strings.xml 法语
 <string name="my_topic_btn">mon sujet</string>
 ../values-hi/strings.xml 印度语
 <string name = "my_topic_btn"> मेरा विषय</string>
  • 对于不应该被翻译的代码、占位符、特殊符号或名称,应该进行标记,不做翻译,可以使用 <xliff:g> 占位符标记,但是务必提供指定ID来说明用途
// 占位符最好不要被翻译,特别是阿拉伯语
 <string name="admission">
   تطبي <xliff:g id="xliff_admission">%1$s</xliff:g> للقبول
 </string>
// url连接地址不要做翻译
<string name="web_url">
  Visit us at <xliff:g id="main_web_url">http://my/app/home.html</xliff:g>
</string>
// 用户名不要做翻译
<string name="user_name">
  username: <xliff:g id="name">Herry</xliff:g>
</string>

LTR与RTL布局

我们一般的阅读习惯都是从左往右,即LTR(left to right),这是Android系统的默认支持的布局方式,除此之外,当targetSdkVersion 设为 17 或更高版本,则系统会激活和使用各种 RTL API,所谓的RTL即从右往左的布局,用来支持中东国家的阅读习惯,常见的语种有阿拉伯语、波斯语、希伯来语等等。
Android控件已经大部分支持RTL布局了,但是一些自定义的控件需要自己做适配。通常情况只需要在<application>标签增加 android:supportsRtl="true",就可启用RTL API来支持RTL布局,具体的适配方案后面会详细讲到。

RTL布局预览

Android Studio默认已经为我们提供了对应语言区域的预览,打开预览的界面Locale for Preview,默认会显示Default(en-us),使用的布局为LTR,若我们使用到了阿拉伯语等RTL语言,预览可以选择对应的ar语或者RTL语言,如下图:

伪语言区域

Android系统平台默认提供了两种伪语言区域,英语 (XA)和AR (XB),分别表示从左到右 (LTR) 和从右到左 (RTL) 显示的语言。即使我们不使用 RTL 语言,伪语言区域也可以帮助我们创建应用的 RTL 版本。 部分定制手机可能不存在这两种伪语言区域。

英语 (XA):在基本英文界面文本中添加拉丁语重音符号,通过添加不带重音符号的文本扩展原始文本,并用方括号将每个消息单元括起来,以使扩展文本中的潜在问题暴露出来。潜在的问题可能是布局损坏和消息语法错误,表现为一个句子被分成多个部分,显示为多条由括号括住的消息。
AR (XB):将从左到右显示的原始消息的文本方向设为从右到左的方向,它会颠倒原始消息中字符的顺序。
要使用 Android 伪语言区域,必须运行 Android 4.3(API 级别 18)或更高版本,并在设备上启用开发者选项。在 Android Studio 中,可以通过以下配置添加到 build.gradle 文件来为特定应用启用伪语言区域.

 android {
      ...
      buildTypes {
        debug {
          pseudoLocalesEnabled true
        }
      }

RTL布局,阿拉伯语的适配

  • 一些简要的属性及API | name | desc | chinese | |------|------------|------------| | android:layoutDirection | the direction of layout drawing| 设置组件的布局排列方向| | android:textDirection | the direction of the text| 设置组件的文字排列方向| | android:textAlignment | the alignment of the text| 设置文字的对齐方式| | getLayoutDirectionFromLocale() |the layout direction for a given Locale| 获取指定地区的惯用布局方式|
  • 启用系统的RTL支持,这个只需要在清单文件增加相关配置即可
<application  
 android:supportsRtl="true">
</application>
  • 全局替换xxxLeft/xxxRight为xxxStart/xxxEnd,可通过AS中选中Refactor->Add RTL Support
  • TextView和EditText控件的全局适配,可以通过主题theme指定全局的样式
 <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- 阿拉伯语言适配-->
        <item name="android:textViewStyle">@style/TextViewStyle.TextDirection</item>
        <item name="editTextStyle">@style/EditTextStyle.Alignment</item>
    </style>

    <!--阿拉伯语适配文本-->
    <style name="TextViewStyle.RTL" parent="android:Widget.TextView">
        <item name="android:textDirection">locale</item>
    </style>

    <!--阿拉伯语适配编辑框-->
    <style name="EditTextStyle.RTL" parent="@android:style/Widget.EditText">
        <item name="android:textAlignment">viewStart</item>
        <item name="android:gravity">start|center_vertical</item>
        <item name="android:textDirection">locale</item>
    </style>
  • 适配图片,部分比较敏感的图片,比如箭头一些方向性的图标,需要创建翻转镜像。
    第一种方式:通过适配drawable资源目录,放置对应的翻转后的图片资源,比如:drawable-ldrtl-xhdpi,但是这样可能会增加额外的包体积大小。
..res
  ..drawable-ldrtl-xhdpi
    ..icon_logo.png
  ..drawable-ldrtl-xxhdpi
    ..icon_logo.png

第二种方式:如果是svg矢量图或者自定义的drawable,可以通过设置android:autoMirrored="true"属性,当系统检测到RTL布局时,会自动创建图片镜像。若是ImageView可以通过Drawable对象来创建镜像。

// svg创建镜像
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="20dp"
    android:height="20dp"
    android:autoMirrored="true"
    android:viewportWidth="20"
    android:viewportHeight="20">
  <path
      android:pathData="M5.5,10l3,4l7,-8"
      android:strokeLineCap="round"/>
</vector>

// ImageView创建镜像
 Drawable drawable = ContextCompat.getDrawable(context, R.drawable.logo);
 // 设置所有的返回按钮支持RTL布局
 drawable.setAutoMirrored(true);
 imageView.setImageDrawable(drawable);

  • 仅设置center_vertical属性,最好能带上start/end等属性。
  • 动态设置setPadding应使用setPaddingRelative代替
  • 动态设置setCompoundDrawables应使用setCompoundDrawablesRelative代替,同样getCompoundDrawables应使用getCompoundDrawablesRelative代替
  • 动态设置setCompoundDrawablesWithIntrinsicBounds应使用setCompoundDrawablesRelativeWithIntrinsicBounds代替
  • ..leftMargin /.rightMargin 使用 setMarginStart/setMarginEnd代替
  • 某些自定义控件包含TextView或者Editext时,如果全局主题适配失效,那么最好需要xml中重写style
  • 自定义控件关于获取getX横坐标距离计算相关逻辑,需要动态判断是否是RTL布局来重新计算坐标和距离
// 当前布局是否为RTL布局,true RTL/false LTR
boolean isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL
if (isRtl) {
   // TODO 如果是阿拉伯语,重新计算横坐标,得到x的点击区域
   return event.getX() > getPaddingEnd() && (event.getX() < getPaddingEnd() + getCompoundDrawablesRelative()[2].getIntrinsicWidth());
   } else {
  return event.getX() > (getWidth() - getPaddingEnd() - getCompoundDrawablesRelative()[2].getIntrinsicWidth()) && (event.getX() < ((getWidth() - getPaddingEnd())));
  }
  • 抽屉布局openDrawer和closeDrawer包含的Gravity属性 需要通过GravityCompat替换。比如Gravity.LEFT需要用GravityCompat.START替代
// 替换前
drawerLayout.closeDrawer(Gravity.LEFT)
// 替换后
drawerLayout.closeDrawer(GravityCompat.START)
  • 双光标的现象,部分手机的输入框可能在RTL语言下,一段文字的左上角和右下角出现半段主光标和副光标,这属于正常现象,是为了多语言文字混编更好的体验,如果感觉不爽,可以通过layout资源重写一套布局来解决。
  • 字符串格式化一些列问题,像日期和阿拉伯数字格式化,String.format可以不使用默认的Local
 // 指定Local兼容RTL
 SimpleDateFormat sdf = new SimpleDateFormat(format,Locale.US);
 // 指定Local兼容RTL
 String.format(Locale.US, "%d", minutes)
  • 网页webView适配问题,可以通过前端的同学自己进行适配,主要使用dir属性指定rtl布局
<!DOCTYPE html>
<html dir="rtl">
<head> 
<meta charset="utf-8"> 
<title>RTL布局测试</title> 
</head>
<body>
<bdo>文本方向从右到左!</bdo>
</body>
</html>
  • 垂直LinearLayout控件使用weight属性偶尔会导致适配失效,建议用FrameLayout的layout_gravity属性控制,或者使用RelativeLayout
  • 使用RelativeLayout时,尽量指定android:layout_alignParentStart,否则大部分界面,譬如在列表RecyclerView中作为item存在时,可能会出现布局错乱,因为它不知道起始控件的位置
  • 对于ConstraintLayout布局,关于屏障Barrier控件的barrierDirection属性支持不友好,AS无法自动将left转换为start,需要自己手动适配
    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="start"
        app:constraint_referenced_ids="haha"/>
  • 通常TextView宽度若是match_parent,要为他设置android:textAlignment="viewStart/viewEnd"
  • 在多层Fragment或者部分界面局部控件突然适配失效的情况,需要在加载页面前重新进行语言适配
  • 使用到WebView控件,当应用内切换语言后,第一次加载会导致整个页面出现适配无效的情况,解决方案如下:
1.在基类的BaseActivity的setContentView方法之前重新设置语言
    public static void changeLanguage(Context context, String newLanguage) {
        Resources resources = context.getResources();
        Configuration configuration = resources.getConfiguration();
        // app locale
        Locale locale = getLocaleByLanguage(newLanguage);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(locale);
        } else {
            configuration.locale = locale;
        }
        DisplayMetrics dm = resources.getDisplayMetrics();
        resources.updateConfiguration(configuration, dm);
        //保存当前语言
        ...
    }
2.在使用到WebView的界面,onCrate方法增加如下
 @Override
    public void onCreate(Bundle savedInstanceState) {
        // TODO 解决含有webView控件导致切换语言失效
        new WebView(this).destroy();
        super.onCreate(savedInstanceState);
    }

最后

关于本地化,个人的建议就是杜绝任何形式的硬编码字符串资源,灵活的使用语言区域限定符和布局方向限定符,某些不应该被翻译的部分应当合理的使用标记符进行标记,部分图片尽量通过系统提供的镜像API进行适配,防止apk资源包变得越来越庞大,还有一些程序使用过程中动态的方法需要通过全局搜索进行整体替换,一些第三方库这个不属于自己能完全控制的范畴,可视情况而定,所以,如果你本身有很不错的开源项目,也应该考虑下国际化。另外我很懒的,所以喜欢这篇文章的话随手点个赞吧!