├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── issue_zh_template_suggest.yml │ ├── issue_zh_template_question.yml │ └── issue_zh_template_bug.yml ├── workflows │ └── android.yml └── FUNDING.yml ├── app ├── AppSignature.jks ├── gradle.properties ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── xml │ │ │ └── network_security_config.xml │ │ ├── anim │ │ │ ├── activity_alpha_in.xml │ │ │ └── activity_alpha_out.xml │ │ ├── values-zh-rCN │ │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ │ └── strings.xml │ │ ├── values-ar │ │ │ └── strings.xml │ │ ├── values-en │ │ │ └── strings.xml │ │ ├── values-v23 │ │ │ └── styles.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── java │ │ └── com │ │ │ └── hjq │ │ │ └── language │ │ │ └── demo │ │ │ ├── BaseActivity.java │ │ │ ├── LanguagesWebView.java │ │ │ ├── AppApplication.java │ │ │ ├── MainActivity.java │ │ │ └── ActivityManager.java │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── picture ├── demo_code.png └── dynamic_figure.gif ├── library ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── hjq │ │ └── language │ │ ├── OnLanguageListener.java │ │ ├── ConfigurationObserver.java │ │ ├── LocaleChangeReceiver.java │ │ ├── LanguagesConfig.java │ │ ├── ActivityLanguages.java │ │ ├── LanguagesUtils.java │ │ ├── MultiLanguages.java │ │ └── LocaleContract.java ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── common.gradle ├── gradle.properties ├── settings.gradle ├── gradlew.bat ├── gradlew ├── HelpDoc.md ├── README.md ├── LICENSE ├── README-en.md └── HelpDoc-en.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /app/AppSignature.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/AppSignature.jks -------------------------------------------------------------------------------- /picture/demo_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/picture/demo_code.png -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /picture/dynamic_figure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/picture/dynamic_figure.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | StoreFile = AppSignature.jks 2 | StorePassword = AndroidProject 3 | KeyAlias = AndroidProject 4 | KeyPassword = AndroidProject -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /build 4 | */build 5 | /captures 6 | /.cxx 7 | */.cxx 8 | /.externalNativeBuild 9 | 10 | ._* 11 | *.iml 12 | .DS_Store 13 | local.properties -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #FF0033 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | zipStoreBase = GRADLE_USER_HOME 2 | zipStorePath = wrapper/dists 3 | distributionBase = GRADLE_USER_HOME 4 | distributionPath = wrapper/dists 5 | distributionUrl = https\://services.gradle.org/distributions/gradle-7.4-all.zip -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | -------------------------------------------------------------------------------- /app/src/main/res/anim/activity_alpha_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/activity_alpha_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 国际化 4 | 简体中文 5 | 6 | 当前 Activity 语种: 7 | 当前 Application 语种: 8 | 当前 System 语种: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 国际化 4 | 跟随系统 5 | 6 | 当前 Activity 语种: 7 | 当前 Application 语种: 8 | 当前 System 语种: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 國際化 4 | 繁體中文 5 | 6 | 當前 Activity 語種: 7 | 當前 Application 語種: 8 | 當前 System 語種: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | تدويل 4 | اتبع النظام 5 | 6 | النشاط الحالي اللغة: 7 | لغة التطبيق الحالية: 8 | لغة النظام الحالية: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MultiLanguages 4 | English 5 | 6 | Current Activity language: 7 | Current Application language: 8 | Current System language: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | 7 | import com.hjq.language.MultiLanguages; 8 | 9 | /** 10 | * author : Android 轮子哥 11 | * github : https://github.com/getActivity/MultiLanguages 12 | * time : 2019/08/10 13 | * desc : Activity 基类 14 | */ 15 | public abstract class BaseActivity extends AppCompatActivity { 16 | 17 | @Override 18 | protected void attachBaseContext(Context newBase) { 19 | // 绑定语种 20 | super.attachBaseContext(MultiLanguages.attach(newBase)); 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/res/values-v23/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png 13 | -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/OnLanguageListener.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * author : Android 轮子哥 7 | * github : https://github.com/getActivity/MultiLanguages 8 | * time : 2021/01/18 9 | * desc : 语种变化监听器 10 | */ 11 | public interface OnLanguageListener { 12 | 13 | /** 14 | * 当前应用语种发生变化时回调 15 | * 16 | * @param oldLocale 旧语种 17 | * @param newLocale 新语种 18 | */ 19 | void onAppLocaleChange(Locale oldLocale, Locale newLocale); 20 | 21 | /** 22 | * 手机系统语种发生变化时回调 23 | * 24 | * @param oldLocale 旧语种 25 | * @param newLocale 新语种 26 | */ 27 | void onSystemLocaleChange(Locale oldLocale, Locale newLocale); 28 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\SDK\Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\SDK\Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/LanguagesWebView.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.webkit.WebView; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | import com.hjq.language.MultiLanguages; 11 | 12 | /** 13 | * author : Android 轮子哥 14 | * github : https://github.com/getActivity/MultiLanguages 15 | * time : 2019/08/10 16 | * desc : 修复语种的 WebView 17 | */ 18 | public final class LanguagesWebView extends WebView { 19 | 20 | public LanguagesWebView(@NonNull Context context) { 21 | this(context, null); 22 | } 23 | 24 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 25 | this(context, attrs, android.R.attr.webViewStyle); 26 | } 27 | 28 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | // 修复 WebView 初始化时会修改 Activity 语种配置的问题 31 | MultiLanguages.updateAppLanguage(context); 32 | } 33 | } -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from : '../common.gradle' 3 | 4 | android { 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | } 9 | 10 | android.libraryVariants.configureEach { variant -> 11 | // aar 输出文件名配置 12 | variant.outputs.configureEach { output -> 13 | outputFileName = "${rootProject.name}-${android.defaultConfig.versionName}.aar" 14 | } 15 | } 16 | } 17 | 18 | tasks.withType(Javadoc).configureEach { 19 | options.addStringOption('Xdoclint:none', '-quiet') 20 | options.addStringOption('encoding', 'UTF-8') 21 | options.addStringOption('charSet', 'UTF-8') 22 | } 23 | 24 | tasks.register('sourcesJar', Jar) { 25 | from android.sourceSets.main.java.srcDirs 26 | classifier = 'sources' 27 | } 28 | 29 | tasks.register('javadoc', Javadoc) { 30 | source = android.sourceSets.main.java.srcDirs 31 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 32 | } 33 | 34 | tasks.register('javadocJar', Jar) { 35 | dependsOn javadoc 36 | classifier = 'javadoc' 37 | from javadoc.destinationDir 38 | } -------------------------------------------------------------------------------- /common.gradle: -------------------------------------------------------------------------------- 1 | // 通用配置 2 | android { 3 | 4 | // 编译源码版本 5 | compileSdk 34 6 | defaultConfig { 7 | // Android 版本适配指南:https://github.com/getActivity/AndroidVersionAdapter 8 | targetSdk 34 9 | versionCode 1020 10 | versionName "10.2" 11 | } 12 | 13 | // 支持 Java JDK 8 14 | compileOptions { 15 | targetCompatibility JavaVersion.VERSION_1_8 16 | sourceCompatibility JavaVersion.VERSION_1_8 17 | } 18 | 19 | // 读取 local.properties 文件配置 20 | def properties = new Properties() 21 | def localPropertiesFile = rootProject.file("local.properties") 22 | if (localPropertiesFile.exists()) { 23 | localPropertiesFile.withInputStream { inputStream -> 24 | properties.load(inputStream) 25 | } 26 | } 27 | 28 | String buildDirPath = properties.getProperty("build.dir") 29 | if (buildDirPath != null && buildDirPath != "") { 30 | // 将构建文件统一输出到指定的目录下 31 | setBuildDir(new File(buildDirPath, rootProject.name + "/build/${path.replaceAll(':', '/')}")) 32 | } else { 33 | // 将构建文件统一输出到项目根目录下的 build 文件夹 34 | setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}")) 35 | } 36 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # 表示使用 AndroidX 20 | android.useAndroidX = true 21 | # 表示将第三方库迁移到 AndroidX 22 | android.enableJetifier = true 23 | 24 | # Compilation failed while handling compileSdk 35 25 | # https://stackoverflow.com/questions/78678063/android-15-update-compilesdk-android-35-cause-an-error-res-table-type-type-e 26 | #android.aapt2Version=8.6.1-11315950 27 | #android.suppressUnsupportedCompileSdk=35 -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/ConfigurationObserver.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.content.ComponentCallbacks; 5 | import android.content.res.Configuration; 6 | 7 | /** 8 | * author : Android 轮子哥 9 | * github : https://github.com/getActivity/MultiLanguages 10 | * time : 2019/05/06 11 | * desc : 手机配置变化监听 12 | */ 13 | final class ConfigurationObserver implements ComponentCallbacks { 14 | 15 | /** 16 | * 注册系统语种变化监听 17 | */ 18 | static void register(Application application) { 19 | ConfigurationObserver configurationObserver = new ConfigurationObserver(application); 20 | application.registerComponentCallbacks(configurationObserver); 21 | } 22 | 23 | private final Application mApplication; 24 | 25 | private ConfigurationObserver(Application application) { 26 | mApplication = application; 27 | } 28 | 29 | /** 30 | * 手机的配置发生了变化 31 | */ 32 | @Override 33 | public void onConfigurationChanged(Configuration newConfig) { 34 | if (newConfig == null) { 35 | return; 36 | } 37 | // 如果当前是跟随系统语种,就则不往下执行 38 | if (MultiLanguages.isSystemLanguage(mApplication)) { 39 | return; 40 | } 41 | // 更新 Application 的配置,否则会出现横竖屏切换之后 Application 的 orientation 没有随之变化的问题 42 | LanguagesUtils.updateConfigurationChanged(mApplication, newConfig, MultiLanguages.getAppLanguage(mApplication)); 43 | } 44 | 45 | @Override 46 | public void onLowMemory() {} 47 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml: -------------------------------------------------------------------------------- 1 | name: 提交建议 2 | description: 请告诉我框架的不足之处,让我做得更好! 3 | title: "[建议]:" 4 | labels: ["help wanted"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: textarea 12 | id: input_id_1 13 | attributes: 14 | label: 你觉得框架有什么不足之处?【必答】 15 | description: 你可以描述框架有什么令你不满意的地方 16 | validations: 17 | required: true 18 | - type: dropdown 19 | id: input_id_2 20 | attributes: 21 | label: issue 是否有人曾提过类似的建议?【必答】 22 | description: 一旦出现重复提问我将不会再次解答 23 | multiple: false 24 | options: 25 | - 未选择 26 | - 是 27 | - 否 28 | validations: 29 | required: true 30 | - type: dropdown 31 | id: input_id_3 32 | attributes: 33 | label: 框架文档是否提及了该问题【必答】 34 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 35 | multiple: false 36 | options: 37 | - 未选择 38 | - 是 39 | - 否 40 | validations: 41 | required: true 42 | - type: dropdown 43 | id: input_id_4 44 | attributes: 45 | label: 是否已经查阅框架文档但还未能解决的【必答】 46 | description: 如果查阅了文档但还是没有解决的话,可以选择是 47 | multiple: false 48 | options: 49 | - 未选择 50 | - 是 51 | - 否 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: input_id_5 56 | attributes: 57 | label: 你觉得该怎么去完善会比较好?【非必答】 58 | description: 你可以提供一下自己的想法或者做法供作者参考 59 | validations: 60 | required: false -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_question.yml: -------------------------------------------------------------------------------- 1 | name: 提出疑问 2 | description: 提出你的困惑,我会给你解答 3 | title: "[疑惑]:" 4 | labels: ["question"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: textarea 12 | id: input_id_1 13 | attributes: 14 | label: 问题描述【必填】 15 | description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) 16 | validations: 17 | required: true 18 | - type: dropdown 19 | id: input_id_2 20 | attributes: 21 | label: 框架文档是否提及了该问题【必答】 22 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 23 | multiple: false 24 | options: 25 | - 未选择 26 | - 是 27 | - 否 28 | validations: 29 | required: true 30 | - type: dropdown 31 | id: input_id_3 32 | attributes: 33 | label: 是否已经查阅框架文档但还未能解决的【必答】 34 | description: 如果查阅了文档但还是没有解决的话,可以选择是 35 | multiple: false 36 | options: 37 | - 未选择 38 | - 是 39 | - 否 40 | validations: 41 | required: true 42 | - type: dropdown 43 | id: input_id_4 44 | attributes: 45 | label: issue 列表中是否有人曾提过类似的问题【必答】 46 | description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 47 | multiple: false 48 | options: 49 | - 未选择 50 | - 是 51 | - 否 52 | validations: 53 | required: true 54 | - type: dropdown 55 | id: input_id_5 56 | attributes: 57 | label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 58 | description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 59 | multiple: false 60 | options: 61 | - 未选择 62 | - 是 63 | - 否 64 | validations: 65 | required: true -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | // 阿里云云效仓库(Gradle 插件):https://maven.aliyun.com/mvn/guide 5 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } 6 | // 阿里云云效仓库:https://maven.aliyun.com/mvn/guide 7 | maven { url 'https://maven.aliyun.com/repository/public' } 8 | maven { url 'https://maven.aliyun.com/repository/google' } 9 | // 华为开源镜像:https://mirrors.huaweicloud.com 10 | maven { url 'https://repo.huaweicloud.com/repository/maven' } 11 | // JitPack 远程仓库:https://jitpack.io 12 | maven { url 'https://jitpack.io' } 13 | // MavenCentral 远程仓库:https://mvnrepository.com 14 | mavenCentral() 15 | // Google 仓库:https://maven.google.com/web/index.html 16 | google() 17 | // noinspection JcenterRepositoryObsolete 18 | jcenter() 19 | } 20 | } 21 | dependencyResolutionManagement { 22 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 23 | repositories { 24 | // 阿里云云效仓库:https://maven.aliyun.com/mvn/guide 25 | maven { url 'https://maven.aliyun.com/repository/public' } 26 | maven { url 'https://maven.aliyun.com/repository/google' } 27 | // 华为开源镜像:https://mirrors.huaweicloud.com 28 | maven { url 'https://repo.huaweicloud.com/repository/maven' } 29 | // JitPack 远程仓库:https://jitpack.io 30 | maven { url 'https://jitpack.io' } 31 | // MavenCentral 远程仓库:https://mvnrepository.com 32 | mavenCentral() 33 | // Google 仓库:https://maven.google.com/web/index.html 34 | google() 35 | // noinspection JcenterRepositoryObsolete 36 | jcenter() 37 | } 38 | } 39 | 40 | include ':app' 41 | include ':library' -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/AppApplication.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.util.Log; 7 | import com.hjq.language.MultiLanguages; 8 | import com.hjq.language.OnLanguageListener; 9 | import com.hjq.toast.Toaster; 10 | import java.util.List; 11 | import java.util.Locale; 12 | 13 | /** 14 | * author : Android 轮子哥 15 | * github : https://github.com/getActivity/MultiLanguages 16 | * time : 2019/08/10 17 | * desc : 应用入口 18 | */ 19 | public final class AppApplication extends Application { 20 | 21 | // static { 22 | // // 设置默认的语种(越早设置越好) 23 | // MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 24 | // } 25 | 26 | @Override 27 | public void onCreate() { 28 | super.onCreate(); 29 | 30 | ActivityManager.getInstance().init(this); 31 | 32 | // 初始化 Toast 框架 33 | Toaster.init(this); 34 | 35 | // 初始化多语种框架 36 | MultiLanguages.init(this); 37 | // 设置语种变化监听器 38 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 39 | 40 | @Override 41 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 42 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 43 | } 44 | 45 | @Override 46 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 47 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 48 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage(AppApplication.this)); 49 | 50 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 51 | return; 52 | } 53 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 54 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 55 | List activityList = ActivityManager.getInstance().getActivityList(); 56 | for (Activity activity : activityList) { 57 | activity.recreate(); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | @Override 64 | protected void attachBaseContext(Context newBase) { 65 | // 绑定语种 66 | super.attachBaseContext(MultiLanguages.attach(newBase)); 67 | } 68 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LocaleChangeReceiver.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import java.util.Locale; 9 | 10 | /** 11 | * author : Android 轮子哥 12 | * github : https://github.com/getActivity/MultiLanguages 13 | * time : 2023/11/24 14 | * desc : 语种变化广播 15 | */ 16 | final class LocaleChangeReceiver extends BroadcastReceiver { 17 | 18 | /** 系统语种 */ 19 | private static volatile Locale sSystemLanguage; 20 | 21 | static void register(Application application) { 22 | sSystemLanguage = LanguagesUtils.getSystemLocale(application); 23 | IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 24 | application.registerReceiver(new LocaleChangeReceiver(application), filter); 25 | } 26 | 27 | private final Application mApplication; 28 | 29 | public LocaleChangeReceiver(Application application) { 30 | mApplication = application; 31 | } 32 | 33 | @Override 34 | public void onReceive(Context context, Intent intent) { 35 | if (intent == null) { 36 | return; 37 | } 38 | 39 | String action = intent.getAction(); 40 | 41 | if (action == null) { 42 | return; 43 | } 44 | 45 | if (!Intent.ACTION_LOCALE_CHANGED.equals(action)) { 46 | return; 47 | } 48 | 49 | if (sSystemLanguage == null) { 50 | return; 51 | } 52 | 53 | Locale latestSystemLocale = MultiLanguages.getSystemLanguage(mApplication); 54 | if (MultiLanguages.equalsLanguageAndCountry(latestSystemLocale, sSystemLanguage)) { 55 | return; 56 | } 57 | 58 | notifySystemLocaleChange(sSystemLanguage, latestSystemLocale); 59 | } 60 | 61 | /** 62 | * 通知系统语种发生变化 63 | */ 64 | public void notifySystemLocaleChange(Locale oldLocale, Locale newLocale) { 65 | sSystemLanguage = newLocale; 66 | 67 | // 如果当前的语种是跟随系统变化的,那么就需要重置一下当前 App 的语种 68 | if (LanguagesConfig.isSystemLanguage(mApplication)) { 69 | LanguagesConfig.clearLanguageSetting(mApplication); 70 | } 71 | 72 | OnLanguageListener listener = MultiLanguages.getOnLanguagesListener(); 73 | if (listener == null) { 74 | return; 75 | } 76 | listener.onSystemLocaleChange(oldLocale, newLocale); 77 | } 78 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | // apply plugin: 'org.jetbrains.kotlin.android' 3 | apply from : '../common.gradle' 4 | 5 | android { 6 | 7 | defaultConfig { 8 | applicationId "com.hjq.language.demo" 9 | minSdkVersion 16 10 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 11 | } 12 | 13 | // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 14 | signingConfigs { 15 | config { 16 | storeFile file(StoreFile) 17 | storePassword StorePassword 18 | keyAlias KeyAlias 19 | keyPassword KeyPassword 20 | } 21 | } 22 | 23 | buildTypes { 24 | debug { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | signingConfig signingConfigs.config 28 | } 29 | 30 | release { 31 | minifyEnabled true 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | signingConfig signingConfigs.config 34 | } 35 | } 36 | 37 | applicationVariants.configureEach { variant -> 38 | // apk 输出文件名配置 39 | variant.outputs.configureEach { output -> 40 | outputFileName = rootProject.getName() + '.apk' 41 | } 42 | } 43 | 44 | bundle { 45 | language { 46 | enableSplit = false 47 | } 48 | } 49 | 50 | //androidResources { 51 | // 启用各应用自动设定语言支持: 52 | // https://developer.android.google.cn/guide/topics/resources/app-languages?hl=zh-cn#auto-localeconfig 53 | //generateLocaleConfig = true 54 | //} 55 | } 56 | 57 | dependencies { 58 | // 依赖 libs 目录下所有的 jar 和 aar 包 59 | implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') 60 | 61 | implementation project(':library') 62 | 63 | // AndroidX 库:https://github.com/androidx/androidx 64 | implementation 'androidx.appcompat:appcompat:1.4.0' 65 | // Material 库:https://github.com/material-components/material-components-android 66 | implementation 'com.google.android.material:material:1.4.0' 67 | 68 | // 标题栏框架:https://github.com/getActivity/TitleBar 69 | implementation 'com.github.getActivity:TitleBar:10.6' 70 | 71 | // 吐司框架:https://github.com/getActivity/Toaster 72 | implementation 'com.github.getActivity:Toaster:12.8' 73 | 74 | // 内存泄漏检测:https://github.com/square/leakcanary 75 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' 76 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LanguagesConfig.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.text.TextUtils; 6 | import java.util.Locale; 7 | 8 | /** 9 | * author : Android 轮子哥 10 | * github : https://github.com/getActivity/MultiLanguages 11 | * time : 2019/05/03 12 | * desc : 语种配置保存类 13 | */ 14 | final class LanguagesConfig { 15 | 16 | private static final String KEY_LANGUAGE = "key_language"; 17 | private static final String KEY_COUNTRY = "key_country"; 18 | 19 | private static String sSharedPreferencesName = "language_setting"; 20 | 21 | /** 当前语种 */ 22 | private static volatile Locale sCurrentLocale; 23 | 24 | /** 默认语种 */ 25 | private static volatile Locale sDefaultLocale; 26 | 27 | static void setSharedPreferencesName(String name) { 28 | sSharedPreferencesName = name; 29 | } 30 | 31 | private static SharedPreferences getSharedPreferences(Context context) { 32 | return context.getSharedPreferences(sSharedPreferencesName, Context.MODE_PRIVATE); 33 | } 34 | 35 | /** 36 | * 读取 App 语种 37 | */ 38 | static Locale readAppLanguageSetting(Context context) { 39 | if (sCurrentLocale != null) { 40 | return sCurrentLocale; 41 | } 42 | 43 | String language = getSharedPreferences(context).getString(KEY_LANGUAGE, ""); 44 | String country = getSharedPreferences(context).getString(KEY_COUNTRY, ""); 45 | 46 | if (!TextUtils.isEmpty(language)) { 47 | sCurrentLocale = new Locale(language, country); 48 | return sCurrentLocale; 49 | } 50 | 51 | if (sDefaultLocale != null) { 52 | sCurrentLocale = sDefaultLocale; 53 | return sCurrentLocale; 54 | } 55 | 56 | sCurrentLocale = LanguagesUtils.getLocale(context); 57 | 58 | return sCurrentLocale; 59 | } 60 | 61 | /** 62 | * 保存 App 语种设置 63 | */ 64 | static void saveAppLanguageSetting(Context context, Locale locale) { 65 | sCurrentLocale = locale; 66 | getSharedPreferences(context).edit() 67 | .putString(KEY_LANGUAGE, locale.getLanguage()) 68 | .putString(KEY_COUNTRY, locale.getCountry()) 69 | .apply(); 70 | } 71 | 72 | /** 73 | * 清除语种设置 74 | */ 75 | static void clearLanguageSetting(Context context) { 76 | sCurrentLocale = MultiLanguages.getSystemLanguage(context); 77 | getSharedPreferences(context).edit() 78 | .remove(KEY_LANGUAGE) 79 | .remove(KEY_COUNTRY) 80 | .apply(); 81 | } 82 | 83 | /** 84 | * 是否跟随系统 85 | */ 86 | public static boolean isSystemLanguage(Context context) { 87 | if (sDefaultLocale != null) { 88 | return false; 89 | } 90 | 91 | String language = getSharedPreferences(context).getString(KEY_LANGUAGE, ""); 92 | return TextUtils.isEmpty(language); 93 | } 94 | 95 | /** 96 | * 设置默认的语种 97 | */ 98 | public static void setDefaultLanguage(Locale locale) { 99 | if (sCurrentLocale != null) { 100 | // 这个 API 需要越早调用越好,建议放在 Application 静态代码块中初始化 101 | // 当然也可以在 Application 调用 super.attachBaseContext 方法之前 102 | throw new IllegalStateException("Please set this before application initialization"); 103 | } 104 | sDefaultLocale = locale; 105 | } 106 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml: -------------------------------------------------------------------------------- 1 | name: 提交 Bug 2 | description: 请告诉我框架存在的问题,我会协助你解决此问题! 3 | title: "[Bug]:" 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: input 12 | id: input_id_1 13 | attributes: 14 | label: 框架版本【必填】 15 | description: 请输入你使用的框架版本 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: input_id_2 20 | attributes: 21 | label: 问题描述【必填】 22 | description: 请输入你对这个问题的描述 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: input_id_3 27 | attributes: 28 | label: 复现步骤【必填】 29 | description: 请输入问题的复现步骤 30 | validations: 31 | required: true 32 | - type: dropdown 33 | id: input_id_4 34 | attributes: 35 | label: 是否必现【必填】 36 | multiple: false 37 | options: 38 | - 未选择 39 | - 是 40 | - 否 41 | validations: 42 | required: true 43 | - type: input 44 | id: input_id_5 45 | attributes: 46 | label: 项目 targetSdkVersion【必填】 47 | validations: 48 | required: true 49 | - type: input 50 | id: input_id_6 51 | attributes: 52 | label: 出现问题的手机信息【必填】 53 | description: 请填写出现问题的品牌和机型 54 | validations: 55 | required: true 56 | - type: input 57 | id: input_id_7 58 | attributes: 59 | label: 出现问题的安卓版本【必填】 60 | description: 请填写出现问题的 Android 版本 61 | validations: 62 | required: true 63 | - type: dropdown 64 | id: input_id_8 65 | attributes: 66 | label: 问题信息的来源渠道【必填】 67 | multiple: true 68 | options: 69 | - 自己遇到的 70 | - Bugly 看到的 71 | - 用户反馈 72 | - 其他渠道 73 | - type: input 74 | id: input_id_9 75 | attributes: 76 | label: 是部分机型还是所有机型都会出现【必答】 77 | description: 部分/全部(例如:某为,某 Android 版本会出现) 78 | validations: 79 | required: true 80 | - type: dropdown 81 | id: input_id_10 82 | attributes: 83 | label: 框架最新的版本是否存在这个问题【必答】 84 | description: 如果用的是旧版本的话,建议升级看问题是否还存在 85 | multiple: false 86 | options: 87 | - 未选择 88 | - 是 89 | - 否 90 | validations: 91 | required: true 92 | - type: dropdown 93 | id: input_id_11 94 | attributes: 95 | label: 框架文档是否提及了该问题【必答】 96 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 97 | multiple: false 98 | options: 99 | - 未选择 100 | - 是 101 | - 否 102 | validations: 103 | required: true 104 | - type: dropdown 105 | id: input_id_12 106 | attributes: 107 | label: 是否已经查阅框架文档但还未能解决的【必答】 108 | description: 如果查阅了文档但还是没有解决的话,可以选择是 109 | multiple: false 110 | options: 111 | - 未选择 112 | - 是 113 | - 否 114 | validations: 115 | required: true 116 | - type: dropdown 117 | id: input_id_13 118 | attributes: 119 | label: issue 列表中是否有人曾提过类似的问题【必答】 120 | description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 121 | multiple: false 122 | options: 123 | - 未选择 124 | - 是 125 | - 否 126 | validations: 127 | required: true 128 | - type: dropdown 129 | id: input_id_14 130 | attributes: 131 | label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 132 | description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 133 | multiple: false 134 | options: 135 | - 未选择 136 | - 是 137 | - 否 138 | validations: 139 | required: true 140 | - type: dropdown 141 | id: input_id_15 142 | attributes: 143 | label: 是否可以通过 Demo 来复现该问题【必答】 144 | description: 排查一下是不是自己的项目代码写得有问题导致的 145 | multiple: false 146 | options: 147 | - 未选择 148 | - 是 149 | - 否 150 | validations: 151 | required: true 152 | - type: textarea 153 | id: input_id_16 154 | attributes: 155 | label: 提供报错堆栈 156 | description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 157 | render: text 158 | validations: 159 | required: false 160 | - type: textarea 161 | id: input_id_17 162 | attributes: 163 | label: 提供截图或视频 164 | description: 根据需要提供,此项不强制 165 | validations: 166 | required: false 167 | - type: textarea 168 | id: input_id_18 169 | attributes: 170 | label: 提供解决方案 171 | description: 如果已经解决了的话,此项不强制 172 | validations: 173 | required: false -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/ActivityLanguages.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | 7 | /** 8 | * author : Android 轮子哥 9 | * github : https://github.com/getActivity/MultiLanguages 10 | * time : 2021/01/21 11 | * desc : Activity 语种注入 12 | */ 13 | final class ActivityLanguages implements Application.ActivityLifecycleCallbacks { 14 | 15 | /* 16 | 这里解释一下,为什么要在 Activity 所有生命周期中刷新语种 17 | 这是因为发现有的手机厂商系统(例如 miui 系统)会偷摸修改 Activity 或 Application 绑定的语种 18 | Github 地址:https://github.com/getActivity/MultiLanguages/issues/52 19 | */ 20 | 21 | static void inject(Application application) { 22 | application.registerActivityLifecycleCallbacks(new ActivityLanguages()); 23 | } 24 | 25 | @Override 26 | public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) { 27 | refreshActivityAndApplicationLanguage(activity); 28 | } 29 | 30 | @Override 31 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 32 | refreshActivityAndApplicationLanguage(activity); 33 | } 34 | 35 | @Override 36 | public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { 37 | refreshActivityAndApplicationLanguage(activity); 38 | } 39 | 40 | @Override 41 | public void onActivityPreStarted(Activity activity) { 42 | refreshActivityAndApplicationLanguage(activity); 43 | } 44 | 45 | @Override 46 | public void onActivityStarted(Activity activity) { 47 | refreshActivityAndApplicationLanguage(activity); 48 | } 49 | 50 | @Override 51 | public void onActivityPostStarted(Activity activity) { 52 | refreshActivityAndApplicationLanguage(activity); 53 | } 54 | 55 | @Override 56 | public void onActivityPreResumed(Activity activity) { 57 | refreshActivityAndApplicationLanguage(activity); 58 | } 59 | 60 | @Override 61 | public void onActivityResumed(Activity activity) { 62 | refreshActivityAndApplicationLanguage(activity); 63 | } 64 | 65 | @Override 66 | public void onActivityPostResumed(Activity activity) { 67 | refreshActivityAndApplicationLanguage(activity); 68 | } 69 | 70 | @Override 71 | public void onActivityPrePaused(Activity activity) { 72 | refreshActivityAndApplicationLanguage(activity); 73 | } 74 | 75 | @Override 76 | public void onActivityPaused(Activity activity) { 77 | refreshActivityAndApplicationLanguage(activity); 78 | } 79 | 80 | @Override 81 | public void onActivityPostPaused(Activity activity) { 82 | refreshActivityAndApplicationLanguage(activity); 83 | } 84 | 85 | @Override 86 | public void onActivityPreStopped(Activity activity) { 87 | refreshActivityAndApplicationLanguage(activity); 88 | } 89 | 90 | @Override 91 | public void onActivityStopped(Activity activity) { 92 | refreshActivityAndApplicationLanguage(activity); 93 | } 94 | 95 | @Override 96 | public void onActivityPostStopped(Activity activity) { 97 | refreshActivityAndApplicationLanguage(activity); 98 | } 99 | 100 | @Override 101 | public void onActivityPreSaveInstanceState(Activity activity, Bundle outState) { 102 | refreshActivityAndApplicationLanguage(activity); 103 | } 104 | 105 | @Override 106 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 107 | refreshActivityAndApplicationLanguage(activity); 108 | } 109 | 110 | @Override 111 | public void onActivityPostSaveInstanceState(Activity activity, Bundle outState) { 112 | refreshActivityAndApplicationLanguage(activity); 113 | } 114 | 115 | @Override 116 | public void onActivityPreDestroyed(Activity activity) { 117 | refreshActivityAndApplicationLanguage(activity); 118 | } 119 | 120 | @Override 121 | public void onActivityDestroyed(Activity activity) { 122 | refreshApplicationLanguage(activity.getApplication()); 123 | } 124 | 125 | @Override 126 | public void onActivityPostDestroyed(Activity activity) { 127 | refreshApplicationLanguage(activity.getApplication()); 128 | } 129 | 130 | /** 131 | * 刷新 Activity 和 Application 的语种 132 | */ 133 | private void refreshActivityAndApplicationLanguage(Activity activity) { 134 | if (activity == null) { 135 | return; 136 | } 137 | MultiLanguages.updateAppLanguage(activity); 138 | refreshApplicationLanguage(activity.getApplication()); 139 | } 140 | 141 | /** 142 | * 刷新 Application 的语种 143 | */ 144 | private void refreshApplicationLanguage(Application application) { 145 | if (application == null) { 146 | return; 147 | } 148 | MultiLanguages.updateAppLanguage(application); 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 25 | 26 | 30 | 31 | 38 | 39 | 45 | 46 | 53 | 54 | 61 | 62 | 69 | 70 | 77 | 78 | 79 | 80 | 85 | 86 | 90 | 91 | 98 | 99 | 100 | 105 | 106 | 110 | 111 | 117 | 118 | 119 | 125 | 126 | 130 | 131 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LanguagesUtils.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.LocaleManager; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.Resources; 7 | import android.os.Build; 8 | import android.os.LocaleList; 9 | import android.text.TextUtils; 10 | import java.util.Locale; 11 | 12 | /** 13 | * author : Android 轮子哥 14 | * github : https://github.com/getActivity/MultiLanguages 15 | * time : 2019/05/03 16 | * desc : 国际化工具类 17 | */ 18 | @SuppressWarnings("deprecation") 19 | final class LanguagesUtils { 20 | 21 | /** 22 | * 获取语种对象 23 | */ 24 | static Locale getLocale(Context context) { 25 | return getLocale(context.getResources().getConfiguration()); 26 | } 27 | 28 | static Locale getLocale(Configuration config) { 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 30 | return config.getLocales().get(0); 31 | } else { 32 | return config.locale; 33 | } 34 | } 35 | 36 | /** 37 | * 设置语种对象 38 | */ 39 | static void setLocale(Configuration config, Locale locale) { 40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 41 | LocaleList localeList = new LocaleList(locale); 42 | config.setLocales(localeList); 43 | // 修复在 Android 13 的联想平板上调用 setLocales 或者 setLocale 修改语种无效的问题 44 | // Github issue 地址:https://github.com/getActivity/MultiLanguages/pull/62 45 | if (!equalsLanguageAndCountry(locale, config.getLocales().get(0))) { 46 | config.locale = locale; 47 | } 48 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 49 | config.setLocale(locale); 50 | } else { 51 | config.locale = locale; 52 | } 53 | 54 | // 某些语种(例如:阿拉伯语)默认需支持 RTL 特性,以便符合阿拉伯人的阅读习惯 55 | // Github issue 地址:https://github.com/getActivity/MultiLanguages/pull/62 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 57 | config.setLayoutDirection(locale); 58 | } 59 | } 60 | 61 | /** 62 | * 获取系统的语种对象 63 | */ 64 | static Locale getSystemLocale(Context context) { 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 66 | // 在 Android 13 上,不能用 Resources.getSystem() 来获取系统语种了 67 | // Android 13 上面新增了一个 LocaleManager 的语种管理类 68 | // 因为如果调用 LocaleManager.setApplicationLocales 会影响获取到的结果不准确 69 | // 所以应该得用 LocaleManager.getSystemLocales 来获取会比较精准 70 | LocaleManager localeManager = context.getSystemService(LocaleManager.class); 71 | if (localeManager != null) { 72 | return localeManager.getSystemLocales().get(0); 73 | } 74 | } 75 | 76 | return LanguagesUtils.getLocale(Resources.getSystem().getConfiguration()); 77 | } 78 | 79 | /** 80 | * 获取默认的语种环境 81 | */ 82 | static Locale getDefaultLocale() { 83 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 84 | return LocaleList.getDefault().get(0); 85 | } else { 86 | return Locale.getDefault(); 87 | } 88 | } 89 | 90 | /** 91 | * 设置默认的语种环境(日期格式化会用到) 92 | */ 93 | static void setDefaultLocale(Context context) { 94 | Configuration configuration = context.getResources().getConfiguration(); 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 96 | LocaleList.setDefault(configuration.getLocales()); 97 | } else { 98 | Locale.setDefault(configuration.locale); 99 | } 100 | } 101 | 102 | /** 103 | * 绑定当前 App 的语种 104 | */ 105 | static Context attachLanguages(Context context, Locale locale) { 106 | Resources resources = context.getResources(); 107 | Configuration config = new Configuration(resources.getConfiguration()); 108 | setLocale(config, locale); 109 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 110 | context = context.createConfigurationContext(config); 111 | } 112 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 113 | return context; 114 | } 115 | 116 | /** 117 | * 更新 Resources 语种 118 | */ 119 | static void updateLanguages(Resources resources, Locale locale) { 120 | Configuration config = resources.getConfiguration(); 121 | setLocale(config, locale); 122 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 123 | } 124 | 125 | /** 126 | * 更新手机配置信息变化 127 | */ 128 | static void updateConfigurationChanged(Context context, Configuration newConfig, Locale appLanguage) { 129 | Configuration config = new Configuration(newConfig); 130 | // 绑定当前语种到这个新的配置对象中 131 | setLocale(config, appLanguage); 132 | Resources resources = context.getResources(); 133 | // 更新上下文的配置信息 134 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 135 | } 136 | 137 | /** 138 | * 生成某个语种下的 Resources 对象 139 | */ 140 | static Resources generateLanguageResources(Context context, Locale locale) { 141 | Configuration config = new Configuration(); 142 | setLocale(config, locale); 143 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 144 | return context.createConfigurationContext(config).getResources(); 145 | } 146 | return new Resources(context.getAssets(), context.getResources().getDisplayMetrics(), config); 147 | } 148 | 149 | /** 150 | * 对比两个语言是否同一个语种(比如:中文有简体和繁体,但是它们都属于同一个语种) 151 | */ 152 | public static boolean equalsLanguage(Locale locale1, Locale locale2) { 153 | return TextUtils.equals(locale1.getLanguage(), locale2.getLanguage()); 154 | } 155 | 156 | /** 157 | * 对比两个语言是否同一个语种并且同一个地方的(比如:中国大陆用的中文简体,中国台湾用的中文繁体) 158 | */ 159 | public static boolean equalsLanguageAndCountry(Locale locale1, Locale locale2) { 160 | // Github issue 地址:https://github.com/getActivity/MultiLanguages/issues/63 161 | return TextUtils.equals(locale1.getLanguage(), locale2.getLanguage()) && 162 | TextUtils.equals(locale1.getCountry(), locale2.getCountry()); 163 | } 164 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.text.TextUtils; 10 | import android.view.View; 11 | import android.webkit.WebChromeClient; 12 | import android.webkit.WebResourceRequest; 13 | import android.webkit.WebView; 14 | import android.webkit.WebViewClient; 15 | import android.widget.RadioGroup; 16 | import android.widget.TextView; 17 | import androidx.annotation.NonNull; 18 | import com.hjq.bar.OnTitleBarListener; 19 | import com.hjq.bar.TitleBar; 20 | import com.hjq.language.LocaleContract; 21 | import com.hjq.language.MultiLanguages; 22 | import java.util.HashMap; 23 | import java.util.Locale; 24 | import java.util.Map; 25 | 26 | /** 27 | * author : Android 轮子哥 28 | * github : https://github.com/getActivity/MultiLanguages 29 | * time : 2019/08/10 30 | * desc : 多语种切换演示 31 | */ 32 | public final class MainActivity extends BaseActivity 33 | implements RadioGroup.OnCheckedChangeListener, OnTitleBarListener { 34 | 35 | private WebView mWebView; 36 | private TextView mSystemLanguageView; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_main); 42 | 43 | mWebView = findViewById(R.id.wv_main_web); 44 | 45 | TitleBar titleBar = findViewById(R.id.tb_main_bar); 46 | RadioGroup radioGroup = findViewById(R.id.rg_main_languages); 47 | 48 | titleBar.setOnTitleBarListener(this); 49 | 50 | mWebView.setWebViewClient(new LanguagesViewClient()); 51 | mWebView.setWebChromeClient(new WebChromeClient()); 52 | mWebView.loadUrl("https://developer.android.google.cn/kotlin", generateLanguageRequestHeader(this)); 53 | 54 | //((TextView) findViewById(R.id.tv_language_activity)).setText(this.getResources().getString(R.string.current_language)); 55 | ((TextView) findViewById(R.id.tv_main_language_application)).setText( 56 | getApplication().getResources().getString(R.string.current_language)); 57 | mSystemLanguageView = findViewById(R.id.tv_main_language_system); 58 | mSystemLanguageView.setText(MultiLanguages.getLanguageString(this, 59 | MultiLanguages.getSystemLanguage(this), R.string.current_language)); 60 | 61 | if (MultiLanguages.isSystemLanguage(this)) { 62 | radioGroup.check(R.id.rb_main_language_auto); 63 | } else { 64 | Locale locale = MultiLanguages.getAppLanguage(this); 65 | if (MultiLanguages.equalsLanguageAndCountry(locale, LocaleContract.getSimplifiedChineseLocale())) { 66 | radioGroup.check(R.id.rb_main_language_cn); 67 | } else if (MultiLanguages.equalsLanguageAndCountry(locale, LocaleContract.getTraditionalChineseLocale())) { 68 | radioGroup.check(R.id.rb_main_language_tw); 69 | } else if (MultiLanguages.equalsLanguageAndCountry(locale, LocaleContract.getEnglishLocale())) { 70 | radioGroup.check(R.id.rb_main_language_en); 71 | } else if (MultiLanguages.equalsLanguageAndCountry(locale, LocaleContract.getArabicLocale())) { 72 | radioGroup.check(R.id.rb_main_language_ar); 73 | } else { 74 | radioGroup.check(R.id.rb_main_language_auto); 75 | } 76 | } 77 | 78 | radioGroup.setOnCheckedChangeListener(this); 79 | } 80 | 81 | /** 82 | * {@link RadioGroup.OnCheckedChangeListener} 83 | */ 84 | @Override 85 | public void onCheckedChanged(RadioGroup group, int checkedId) { 86 | // 是否需要重启 87 | boolean restart = false; 88 | 89 | if (checkedId == R.id.rb_main_language_auto) { 90 | // 跟随系统 91 | restart = MultiLanguages.clearAppLanguage(this); 92 | } else if (checkedId == R.id.rb_main_language_cn) { 93 | // 简体中文 94 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getSimplifiedChineseLocale()); 95 | } else if (checkedId == R.id.rb_main_language_tw) { 96 | // 繁体中文 97 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getTraditionalChineseLocale()); 98 | } else if (checkedId == R.id.rb_main_language_en) { 99 | // 英语 100 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getEnglishLocale()); 101 | } else if (checkedId == R.id.rb_main_language_ar) { 102 | // 阿拉伯语 103 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getArabicLocale()); 104 | } 105 | 106 | if (restart) { 107 | // 1.使用 recreate 来重启 Activity,效果差,有闪屏的缺陷 108 | // recreate(); 109 | 110 | // 2.使用常规 startActivity 来重启 Activity,有从左向右的切换动画 111 | // 稍微比 recreate 的效果好一点,但是这种并不是最佳的效果 112 | // startActivity(new Intent(this, LanguageActivity.class)); 113 | // finish(); 114 | 115 | // 3.我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果,相比前面的两种带来的体验是最佳的 116 | // startActivity(new Intent(this, MainActivity.class)); 117 | // overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 118 | // finish(); 119 | 120 | // 4. 我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果 121 | Intent intent = new Intent(this, MainActivity.class); 122 | // Github 地址:https://github.com/getActivity/MultiLanguages/issues/55 123 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 124 | startActivity(intent); 125 | overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 126 | finish(); 127 | } 128 | } 129 | 130 | @Override 131 | public void onResume() { 132 | super.onResume(); 133 | mWebView.onResume(); 134 | mWebView.resumeTimers(); 135 | 136 | if (mSystemLanguageView == null) { 137 | return; 138 | } 139 | mSystemLanguageView.setText(MultiLanguages.getLanguageString(this, 140 | MultiLanguages.getSystemLanguage(this), R.string.current_language)); 141 | } 142 | 143 | @Override 144 | public void onPause() { 145 | super.onPause(); 146 | mWebView.onPause(); 147 | mWebView.pauseTimers(); 148 | } 149 | 150 | @Override 151 | protected void onDestroy() { 152 | super.onDestroy(); 153 | //清除历史记录 154 | mWebView.clearHistory(); 155 | //停止加载 156 | mWebView.stopLoading(); 157 | //加载一个空白页 158 | mWebView.loadUrl("about:blank"); 159 | mWebView.setWebChromeClient(null); 160 | mWebView.setWebViewClient(new WebViewClient()); 161 | //移除WebView所有的View对象 162 | mWebView.removeAllViews(); 163 | //销毁此的WebView的内部状态 164 | mWebView.destroy(); 165 | } 166 | 167 | @Override 168 | public void onTitleClick(TitleBar titleBar) { 169 | Intent intent = new Intent(Intent.ACTION_VIEW); 170 | intent.setData(Uri.parse(titleBar.getTitle().toString())); 171 | startActivity(intent); 172 | } 173 | 174 | public static class LanguagesViewClient extends WebViewClient { 175 | 176 | @TargetApi(Build.VERSION_CODES.N) 177 | @Override 178 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 179 | return shouldOverrideUrlLoading(view, request.getUrl().toString()); 180 | } 181 | 182 | @Override 183 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 184 | String scheme = Uri.parse(url).getScheme(); 185 | if (scheme == null) { 186 | return false; 187 | } 188 | switch (scheme) { 189 | // 如果这是跳链接操作 190 | case "http": 191 | case "https": 192 | view.loadUrl(url, generateLanguageRequestHeader(view.getContext())); 193 | break; 194 | default: 195 | break; 196 | } 197 | return true; 198 | } 199 | 200 | @Override 201 | public void onPageCommitVisible(WebView view, String url) { 202 | super.onPageCommitVisible(view, url); 203 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 204 | return; 205 | } 206 | Locale locale = MultiLanguages.getAppLanguage(view.getContext()); 207 | if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_LTR) { 208 | return; 209 | } 210 | // 将水平水平滚动条滚动到最右边,以便更好适配 RTL 特性 211 | // Github issue 地址:https://github.com/getActivity/MultiLanguages/pull/62 212 | view.scrollTo(view.getWidth(), 0); 213 | } 214 | } 215 | 216 | /** 217 | * 给 WebView 请求头添加语种环境 218 | */ 219 | @NonNull 220 | public static Map generateLanguageRequestHeader(Context context) { 221 | Map map = new HashMap<>(1); 222 | // Android 13 上面语种失效的问题解决方案 223 | // https://developer.android.google.cn/about/versions/13/features/app-languages?hl=zh-cn#consider-header 224 | map.put("Accept-Language", String.valueOf(MultiLanguages.getAppLanguage(context))); 225 | return map; 226 | } 227 | } -------------------------------------------------------------------------------- /HelpDoc.md: -------------------------------------------------------------------------------- 1 | #### 目录 2 | 3 | * [如何设置 App 默认的语种](#如何设置-app-默认的语种) 4 | 5 | * [切换语种后没有任何效果该怎么办](#切换语种后没有任何效果该怎么办) 6 | 7 | * [怎么在切换语种后应用到所有 Activity 上](#怎么在切换语种后应用到所有-activity-上) 8 | 9 | * [怎么在用户切换系统语种的时候刷新所有界面](#怎么在用户切换系统语种的时候刷新所有界面) 10 | 11 | * [在系统切换语种返回应用后没有生效怎么办](#在系统切换语种返回应用后没有生效怎么办) 12 | 13 | * [WebView 导致语种失效的解决方案](#webview-导致语种失效的解决方案) 14 | 15 | * [Android 13 WebView 语种失效怎么办](#android-13-webview-语种失效怎么办) 16 | 17 | * [有没有一种不用通过重启的方式来切换语种](#有没有一种不用通过重启的方式来切换语种) 18 | 19 | #### 如何设置 App 默认的语种 20 | 21 | * 在从来没有调用过 `MultiLanguages.setAppLanguage` 的情况下,又不想让应用跟随系统的语种,而是想指定某个语种该怎么做?具体写法示例如下: 22 | 23 | ```java 24 | public final class XxxApplication extends Application { 25 | 26 | static { 27 | // 设置默认的语种(越早设置越好) 28 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 29 | } 30 | } 31 | ``` 32 | 33 | * 当然你如果想判断当前的系统语种类型,然后再设置默认的语种,可以这样写: 34 | 35 | ```java 36 | public final class XxxApplication extends Application { 37 | 38 | @Override 39 | protected void attachBaseContext(Context newBase) { 40 | if (newBase != null) { 41 | Locale systemLanguage = MultiLanguages.getSystemLanguage(newBase); 42 | // 如果当前语种既不是中文(包含简体和繁体)和英语(包含美式英式等),就默认设置成英文的,避免跟随系统语种 43 | if (!MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getChineseLocale()) && 44 | !MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getEnglishLocale())) { 45 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 46 | } 47 | } 48 | // 绑定语种 49 | super.attachBaseContext(MultiLanguages.attach(newBase)); 50 | } 51 | } 52 | ``` 53 | 54 | #### 切换语种后没有任何效果该怎么办 55 | 56 | * 情况一:可以检查一下是否在 `build.gradle` 文件中配置了仅保留某个国家的语种资源,例如 `resConfigs 'zh'` 就代表只保留和中文相关的语种资源,而其他国家的语种资源就不会被打包进 apk 包中,这样就会导致在切换语种的时候始终都是中文的尴尬局面。 57 | 58 | * 情况二:如果是 Fragment 里面的语种切换之后没有变化,请检查 Fragment 在 Activity 重启之后是否被复用了,一般情况下是调用了 `fragment.setRetainInstance(true)` 导致的。 59 | 60 | * 情况三:如果是上架 GooglePlay 或者华为的 aab 包后,上架成功后再下载发现切换不了 App 语种,那是因为在分包的时候导致多语言资源缺失了,这时候需要对主模块的 `build.gradle` 文件进行配置,具体用法可以参考[官方文档](https://developer.android.google.cn/guide/app-bundle/configure-base?hl=zh-cn) 61 | 62 | ```groovy 63 | android { 64 | 65 | ...... 66 | 67 | bundle { 68 | 69 | ...... 70 | 71 | language { 72 | enableSplit = false 73 | } 74 | 75 | ...... 76 | } 77 | } 78 | ``` 79 | 80 | * 其他情况:如果不是以上原因造成的,请提一个 [issue](https://github.com/getActivity/MultiLanguages/issues) 给到我处理。 81 | 82 | #### 怎么在切换语种后应用到所有 Activity 上 83 | 84 | * 在 Application 的 onCreate 方法中加入以下代码 85 | 86 | ```java 87 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 88 | 89 | @Override 90 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 91 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 92 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 93 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 94 | List activityList = getAllActivity(); 95 | for (Activity activity : activityList) { 96 | activity.recreate(); 97 | } 98 | } 99 | 100 | @Override 101 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 102 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 103 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 104 | } 105 | }); 106 | ``` 107 | 108 | #### 怎么在用户切换系统语种的时候刷新所有界面 109 | 110 | * 在 Application 的 onCreate 方法中加入以下代码 111 | 112 | ```java 113 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 114 | 115 | @Override 116 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 117 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 118 | } 119 | 120 | @Override 121 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 122 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 123 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 124 | 125 | // 当前语种是否是跟随系统,如果不是则不往下执行 126 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 127 | return; 128 | } 129 | 130 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 131 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 132 | List activityList = getAllActivity(); 133 | for (Activity activity : activityList) { 134 | activity.recreate(); 135 | } 136 | } 137 | }); 138 | ``` 139 | 140 | #### 在系统切换语种返回应用后没有生效怎么办 141 | 142 | * 请检查在 Activity 配置了 `android:configChanges` 属性,有的话请去掉再进行尝试,这个属性的文档请查看 [``](https://developer.android.google.cn/guide/topics/manifest/activity-element?hl=zh) 143 | 144 | ```xml 145 | 148 | ``` 149 | 150 | * 如果你不想去除 `android:configChanges` 属性,又想切换系统语种后生效,在 Application 的 onCreate 方法中加入以下代码 151 | 152 | ```java 153 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 154 | 155 | @Override 156 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 157 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 158 | } 159 | 160 | @Override 161 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 162 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 163 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 164 | 165 | // 当前语种是否是跟随系统,如果不是则不往下执行 166 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 167 | return; 168 | } 169 | 170 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 171 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 172 | List activityList = getAllActivity(); 173 | for (Activity activity : activityList) { 174 | activity.recreate(); 175 | } 176 | } 177 | }); 178 | ``` 179 | 180 | #### WebView 导致语种失效的解决方案 181 | 182 | * 由于 WebView 初始化会修改 Activity 语种配置,间接导致 Activity 语种会被还原回去,所以需要你手动重写 WebView 对这个问题进行修复 183 | 184 | ```java 185 | public final class LanguagesWebView extends WebView { 186 | 187 | public LanguagesWebView(@NonNull Context context) { 188 | this(context, null); 189 | } 190 | 191 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 192 | this(context, attrs, android.R.attr.webViewStyle); 193 | } 194 | 195 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 196 | super(context, attrs, defStyleAttr); 197 | 198 | // 修复 WebView 初始化时会修改 Activity 语种配置的问题 199 | MultiLanguages.updateAppLanguage(context); 200 | } 201 | } 202 | ``` 203 | 204 | #### Android 13 WebView 语种失效怎么办 205 | 206 | * 将 `webView.loadUrl(@NonNull String url)` 替换成 `webView.loadUrl(@NonNull String url, @NonNull Map additionalHttpHeaders)`,并添加 `Accept-Language` 请求头即可,具体的示例代码如下: 207 | 208 | ```java 209 | public final class MainActivity extends Activity { 210 | 211 | private WebView mWebView; 212 | 213 | @Override 214 | protected void onCreate(Bundle savedInstanceState) { 215 | super.onCreate(savedInstanceState); 216 | setContentView(R.layout.activity_main); 217 | 218 | mWebView = findViewById(R.id.wv_main_web); 219 | 220 | mWebView.setWebViewClient(new LanguagesViewClient()); 221 | mWebView.setWebChromeClient(new WebChromeClient()); 222 | mWebView.loadUrl("https://developer.android.google.cn/kotlin", generateLanguageRequestHeader(this)); 223 | } 224 | 225 | public static class LanguagesViewClient extends WebViewClient { 226 | 227 | @TargetApi(Build.VERSION_CODES.N) 228 | @Override 229 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 230 | return shouldOverrideUrlLoading(view, request.getUrl().toString()); 231 | } 232 | 233 | @Override 234 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 235 | String scheme = Uri.parse(url).getScheme(); 236 | if (scheme == null) { 237 | return false; 238 | } 239 | switch (scheme) { 240 | // 如果这是跳链接操作 241 | case "http": 242 | case "https": 243 | view.loadUrl(url, generateLanguageRequestHeader(view.getContext())); 244 | break; 245 | default: 246 | break; 247 | } 248 | return true; 249 | } 250 | } 251 | 252 | /** 253 | * 给 WebView 请求头添加语种环境 254 | */ 255 | @NonNull 256 | public static Map generateLanguageRequestHeader(Context context) { 257 | Map map = new HashMap<>(1); 258 | // Android 13 上面语种失效的问题解决方案 259 | // https://developer.android.google.cn/about/versions/13/features/app-languages?hl=zh-cn#consider-header 260 | map.put("Accept-Language", String.valueOf(MultiLanguages.getAppLanguage(context))); 261 | return map; 262 | } 263 | } 264 | ``` 265 | 266 | #### 有没有一种不用通过重启的方式来切换语种 267 | 268 | * 我先问大家一个问题,生米煮成熟饭了,怎么从熟饭变成生米?这显然是不现实的,退一万步讲,假设框架能做到,文字和图片都能自动跟随语种的变化而变化,那么通过网络请求的数据又怎么切换语种?是不是得重新请求?如果是列表数据是不是得从第 1 页开始请求?再问大家一个问题,还有语种切换是一个常用动作吗?我相信大家此时心里已经有了答案。 269 | 270 | * 所以并不是做不到不用重启的效果,而是没有那个必要(切语种不是常用动作),并且存在一定的硬伤(虽然 UI 层不用动,但是数据层还是要重新请求)。 -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/MultiLanguages.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.Resources; 7 | import android.os.Build; 8 | import android.os.Looper; 9 | import android.os.MessageQueue; 10 | import android.text.TextUtils; 11 | import java.util.Locale; 12 | 13 | /** 14 | * author : Android 轮子哥 15 | * github : https://github.com/getActivity/MultiLanguages 16 | * time : 2019/05/03 17 | * desc : 语种切换框架 18 | */ 19 | @SuppressWarnings("unused") 20 | public final class MultiLanguages { 21 | 22 | /** 应用上下文对象 */ 23 | private static Application sApplication; 24 | 25 | /** 语种变化监听对象 */ 26 | private static OnLanguageListener sLanguageListener; 27 | 28 | /** 29 | * 初始化多语种框架 30 | */ 31 | public static void init(Application application) { 32 | init(application, true); 33 | } 34 | 35 | public static void init(final Application application, boolean inject) { 36 | if (sApplication != null) { 37 | // 如果框架已经初始化过了,则不往下执行 38 | return; 39 | } 40 | sApplication = application; 41 | LanguagesUtils.setDefaultLocale(application); 42 | if (inject) { 43 | ActivityLanguages.inject(application); 44 | } 45 | /* 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 47 | LocaleManager localeManager = application.getSystemService(LocaleManager.class); 48 | if (localeManager != null) { 49 | if (isSystemLanguage(application)) { 50 | // 在没有设置过 setApplicationLocales 方法,里面的默认值会为 LocaleList.getEmptyLocaleList() 51 | // 当设置过一次的 setApplicationLocales 为其他值,在 Android 13 的手机设置中修改语种(系统或者应用)的时候 52 | // 就会导致 context.registerComponentCallbacks 中的 onConfigurationChanged 监听方法没有触发到 53 | // 如果当前应用指定了某种语种,监听方法没有回调是正常的,但是如果当前语种是跟随系统的模式,那么不回调就是有问题的了 54 | // 这是因为系统对 setApplicationLocales 的结果进行了持久化操作,所以这里要重新设置一下,并传入 LocaleList.getEmptyLocaleList() 55 | // Github issue:https://github.com/getActivity/MultiLanguages/issues/37 56 | localeManager.setApplicationLocales(LocaleList.getEmptyLocaleList()); 57 | } else { 58 | localeManager.setApplicationLocales(new LocaleList(getAppLanguage(application))); 59 | } 60 | } 61 | } 62 | */ 63 | // 等所有的任务都执行完了,再设置对系统语种的监听,用户不可能在这点间隙的时间完成切换语言的 64 | // 经过实践证明 IdleHandler 会在第一个 Activity attachBaseContext 之后调用的,所以没有什么问题 65 | Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { 66 | @Override 67 | public boolean queueIdle() { 68 | ConfigurationObserver.register(application); 69 | LocaleChangeReceiver.register(application); 70 | return false; 71 | } 72 | }); 73 | } 74 | 75 | /** 76 | * 在上下文的子类中重写 attachBaseContext 方法(用于更新 Context 的语种) 77 | */ 78 | public static Context attach(Context context) { 79 | Locale locale = getAppLanguage(context); 80 | if (equalsLanguageAndCountry(locale, LanguagesUtils.getLocale(context))) { 81 | return context; 82 | } 83 | return LanguagesUtils.attachLanguages(context, locale); 84 | } 85 | 86 | /** 87 | * 更新 Context 的语种 88 | */ 89 | public static void updateAppLanguage(Context context) { 90 | updateAppLanguage(context, context.getResources()); 91 | } 92 | 93 | /** 94 | * 更新 Resources 的语种 95 | */ 96 | public static void updateAppLanguage(Context context, Resources resources) { 97 | if (resources == null) { 98 | return; 99 | } 100 | Locale locale = getAppLanguage(context); 101 | Configuration config = resources.getConfiguration(); 102 | if (!LanguagesUtils.equalsLanguageAndCountry(locale, LanguagesUtils.getLocale(config)) 103 | || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 104 | && TextUtils.getLayoutDirectionFromLocale(locale) != config.getLayoutDirection())) { 105 | LanguagesUtils.updateLanguages(resources, locale); 106 | } 107 | if (!LanguagesUtils.equalsLanguageAndCountry(locale, LanguagesUtils.getDefaultLocale())) { 108 | // Github issue 地址:https://github.com/getActivity/MultiLanguages/issues/59 109 | LanguagesUtils.setDefaultLocale(context); 110 | } 111 | } 112 | 113 | /** 114 | * 获取 App 的语种 115 | */ 116 | public static Locale getAppLanguage(Context context) { 117 | if (isSystemLanguage(context)) { 118 | return getSystemLanguage(context); 119 | } else { 120 | return LanguagesConfig.readAppLanguageSetting(context); 121 | } 122 | } 123 | 124 | /** 125 | * 设置 App 的语种 126 | * 127 | * @return 语种是否发生改变了 128 | */ 129 | public static boolean setAppLanguage(Context context, Locale newLocale) { 130 | /* 131 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 132 | context.getSystemService(LocaleManager.class).setApplicationLocales(new LocaleList(newLocale)); 133 | } 134 | */ 135 | // 这里解释一下,在 Android 13 上为什么不用 LocaleManager.setApplicationLocales 来设置语种,原因如下: 136 | // 1. 调用此 API 会自动重启 Activity,而框架是将重启操作放到了外层给开发者去重启 137 | // 2. 上面说了,调用此 API 会重启 Activity,重启也就算了,还顺带闪了一下,这个不能忍 138 | LanguagesConfig.saveAppLanguageSetting(context, newLocale); 139 | if (LanguagesUtils.equalsLanguageAndCountry(newLocale, LanguagesUtils.getLocale(context))) { 140 | return false; 141 | } 142 | 143 | Locale oldLocale = LanguagesUtils.getLocale(context); 144 | // 更新 Context 的语种 145 | LanguagesUtils.updateLanguages(context.getResources(), newLocale); 146 | if (context != sApplication) { 147 | // 更新 Application 的语种 148 | LanguagesUtils.updateLanguages(sApplication.getResources(), newLocale); 149 | } 150 | // 重新设置默认的语种环境 151 | LanguagesUtils.setDefaultLocale(context); 152 | 153 | if (sLanguageListener != null) { 154 | sLanguageListener.onAppLocaleChange(oldLocale, newLocale); 155 | } 156 | return true; 157 | } 158 | 159 | /** 160 | * 获取系统的语种 161 | */ 162 | public static Locale getSystemLanguage(Context context) { 163 | return LanguagesUtils.getSystemLocale(context); 164 | } 165 | 166 | /** 167 | * 是否跟随系统的语种 168 | */ 169 | public static boolean isSystemLanguage(Context context) { 170 | return LanguagesConfig.isSystemLanguage(context); 171 | } 172 | 173 | /** 174 | * 跟随系统语种 175 | * 176 | * @return 语种是否发生改变了 177 | */ 178 | public static boolean clearAppLanguage(Context context) { 179 | LanguagesConfig.clearLanguageSetting(context); 180 | if (LanguagesUtils.equalsLanguageAndCountry(getSystemLanguage(sApplication), LanguagesUtils.getLocale(context))) { 181 | return false; 182 | } 183 | 184 | LanguagesUtils.updateLanguages(context.getResources(), getSystemLanguage(sApplication)); 185 | LanguagesUtils.setDefaultLocale(context); 186 | if (context != sApplication) { 187 | // 更新 Application 的语种 188 | LanguagesUtils.updateLanguages(sApplication.getResources(), getSystemLanguage(sApplication)); 189 | } 190 | return true; 191 | } 192 | 193 | /** 194 | * 设置默认的语种(越早设置越好) 195 | */ 196 | public static void setDefaultLanguage(Locale locale) { 197 | LanguagesConfig.setDefaultLanguage(locale); 198 | } 199 | 200 | /** 201 | * 对比两个语言是否同一个语种(比如:中文有简体和繁体,但是它们都属于同一个语种) 202 | */ 203 | public static boolean equalsLanguage(Locale locale1, Locale locale2) { 204 | return LanguagesUtils.equalsLanguage(locale1, locale2); 205 | } 206 | 207 | /** 208 | * @deprecated 该 API 已经过时,随时会被删除,请尽早迁移到 {@link #equalsLanguageAndCountry(Locale, Locale)} 209 | */ 210 | public static boolean equalsCountry(Locale locale1, Locale locale2) { 211 | return equalsLanguageAndCountry(locale1, locale2); 212 | } 213 | 214 | /** 215 | * 对比两个语言是否同一个语种并且同一个地方的(比如:中国大陆用的中文简体,中国台湾用的中文繁体) 216 | */ 217 | public static boolean equalsLanguageAndCountry(Locale locale1, Locale locale2) { 218 | return LanguagesUtils.equalsLanguageAndCountry(locale1, locale2); 219 | } 220 | 221 | /** 222 | * 获取某个语种下的 String 223 | */ 224 | public static String getLanguageString(Context context, Locale locale, int id) { 225 | return generateLanguageResources(context, locale).getString(id); 226 | } 227 | 228 | /** 229 | * 生成某个语种下的 Resources 对象 230 | */ 231 | public static Resources generateLanguageResources(Context context, Locale locale) { 232 | return LanguagesUtils.generateLanguageResources(context, locale); 233 | } 234 | 235 | /** 236 | * 设置语种变化监听器 237 | */ 238 | public static void setOnLanguageListener(OnLanguageListener listener) { 239 | sLanguageListener = listener; 240 | } 241 | 242 | /** 243 | * 设置保存的 SharedPreferences 文件名(请在 Application 初始化之前设置,可以放在 Application 中的代码块或者静态代码块) 244 | */ 245 | public static void setSharedPreferencesName(String name) { 246 | LanguagesConfig.setSharedPreferencesName(name); 247 | } 248 | 249 | /** 250 | * 获取语种变化监听对象 251 | */ 252 | static OnLanguageListener getOnLanguagesListener() { 253 | return sLanguageListener; 254 | } 255 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.text.TextUtils; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | 14 | import java.io.FileInputStream; 15 | import java.io.IOException; 16 | import java.lang.reflect.InvocationTargetException; 17 | import java.lang.reflect.Method; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.ArrayList; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | 23 | /** 24 | * author : Android 轮子哥 25 | * github : https://github.com/getActivity/AndroidProject 26 | * time : 2018/11/18 27 | * desc : Activity 管理器 28 | */ 29 | public final class ActivityManager implements Application.ActivityLifecycleCallbacks { 30 | 31 | private static volatile ActivityManager sInstance; 32 | 33 | /** Activity 存放集合 */ 34 | private final List mActivityList = new ArrayList<>(); 35 | 36 | /** 应用生命周期回调 */ 37 | private final ArrayList mLifecycleCallbacks = new ArrayList<>(); 38 | 39 | /** 当前应用上下文对象 */ 40 | private Application mApplication; 41 | /** 栈顶的 Activity 对象 */ 42 | private Activity mTopActivity; 43 | /** 前台并且可见的 Activity 对象 */ 44 | private Activity mResumedActivity; 45 | 46 | private ActivityManager() {} 47 | 48 | public static ActivityManager getInstance() { 49 | if(sInstance == null) { 50 | synchronized (ActivityManager.class) { 51 | if(sInstance == null) { 52 | sInstance = new ActivityManager(); 53 | } 54 | } 55 | } 56 | return sInstance; 57 | } 58 | 59 | public void init(Application application) { 60 | mApplication = application; 61 | mApplication.registerActivityLifecycleCallbacks(this); 62 | } 63 | 64 | /** 65 | * 获取 Application 对象 66 | */ 67 | @NonNull 68 | public Application getApplication() { 69 | return mApplication; 70 | } 71 | 72 | /** 73 | * 获取栈顶的 Activity 74 | */ 75 | @Nullable 76 | public Activity getTopActivity() { 77 | return mTopActivity; 78 | } 79 | 80 | /** 81 | * 获取前台并且可见的 Activity 82 | */ 83 | @Nullable 84 | public Activity getResumedActivity() { 85 | return mResumedActivity; 86 | } 87 | 88 | /** 89 | * 获取 Activity 集合 90 | */ 91 | @NonNull 92 | public List getActivityList() { 93 | return mActivityList; 94 | } 95 | 96 | /** 97 | * 判断当前应用是否处于前台状态 98 | */ 99 | public boolean isForeground() { 100 | return getResumedActivity() != null; 101 | } 102 | 103 | /** 104 | * 注册应用生命周期回调 105 | */ 106 | public void registerApplicationLifecycleCallback(ApplicationLifecycleCallback callback) { 107 | mLifecycleCallbacks.add(callback); 108 | } 109 | 110 | /** 111 | * 取消注册应用生命周期回调 112 | */ 113 | public void unregisterApplicationLifecycleCallback(ApplicationLifecycleCallback callback) { 114 | mLifecycleCallbacks.remove(callback); 115 | } 116 | 117 | /** 118 | * 销毁指定的 Activity 119 | */ 120 | public void finishActivity(Class clazz) { 121 | if (clazz == null) { 122 | return; 123 | } 124 | 125 | Iterator iterator = mActivityList.iterator(); 126 | while (iterator.hasNext()) { 127 | Activity activity = iterator.next(); 128 | if (!activity.getClass().equals(clazz)) { 129 | continue; 130 | } 131 | if (!activity.isFinishing()) { 132 | activity.finish(); 133 | } 134 | iterator.remove(); 135 | } 136 | } 137 | 138 | /** 139 | * 销毁所有的 Activity 140 | */ 141 | public void finishAllActivities() { 142 | finishAllActivities((Class) null); 143 | } 144 | 145 | /** 146 | * 销毁所有的 Activity 147 | * 148 | * @param classArray 白名单 Activity 149 | */ 150 | @SafeVarargs 151 | public final void finishAllActivities(Class... classArray) { 152 | Iterator iterator = mActivityList.iterator(); 153 | while (iterator.hasNext()) { 154 | Activity activity = iterator.next(); 155 | boolean whiteClazz = false; 156 | if (classArray != null) { 157 | for (Class clazz : classArray) { 158 | if (activity.getClass().equals(clazz)) { 159 | whiteClazz = true; 160 | } 161 | } 162 | } 163 | if (whiteClazz) { 164 | continue; 165 | } 166 | // 如果不是白名单上面的 Activity 就销毁掉 167 | if (!activity.isFinishing()) { 168 | activity.finish(); 169 | } 170 | iterator.remove(); 171 | } 172 | } 173 | 174 | @Override 175 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { 176 | if (mActivityList.isEmpty()) { 177 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 178 | callback.onApplicationCreate(activity); 179 | } 180 | } 181 | mActivityList.add(activity); 182 | mTopActivity = activity; 183 | } 184 | 185 | @Override 186 | public void onActivityStarted(@NonNull Activity activity) { 187 | } 188 | 189 | @Override 190 | public void onActivityResumed(@NonNull Activity activity) { 191 | if (mTopActivity == activity && mResumedActivity == null) { 192 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 193 | callback.onApplicationForeground(activity); 194 | } 195 | } 196 | mTopActivity = activity; 197 | mResumedActivity = activity; 198 | } 199 | 200 | @Override 201 | public void onActivityPaused(@NonNull Activity activity) { 202 | } 203 | 204 | @Override 205 | public void onActivityStopped(@NonNull Activity activity) { 206 | if (mResumedActivity == activity) { 207 | mResumedActivity = null; 208 | } 209 | if (mResumedActivity == null) { 210 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 211 | callback.onApplicationBackground(activity); 212 | } 213 | } 214 | } 215 | 216 | @Override 217 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { 218 | } 219 | 220 | @Override 221 | public void onActivityDestroyed(@NonNull Activity activity) { 222 | mActivityList.remove(activity); 223 | if (mTopActivity == activity) { 224 | mTopActivity = null; 225 | } 226 | if (mActivityList.isEmpty()) { 227 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 228 | callback.onApplicationDestroy(activity); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * 判断是否在主进程中 235 | */ 236 | public static boolean isMainProcess(Context context) { 237 | String processName = getProcessName(); 238 | if (TextUtils.isEmpty(processName)) { 239 | // 如果获取不到进程名称,那么则将它当做主进程 240 | return true; 241 | } 242 | return TextUtils.equals(processName, context.getPackageName()); 243 | } 244 | 245 | /** 246 | * 获取当前进程名称 247 | */ 248 | @SuppressLint("PrivateApi, DiscouragedPrivateApi") 249 | @Nullable 250 | public static String getProcessName() { 251 | String processName = null; 252 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 253 | processName = Application.getProcessName(); 254 | } else { 255 | try { 256 | Class activityThread = Class.forName("android.app.ActivityThread"); 257 | Method currentProcessNameMethod = activityThread.getDeclaredMethod("currentProcessName"); 258 | processName = (String) currentProcessNameMethod.invoke(null); 259 | } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 260 | e.printStackTrace(); 261 | } 262 | } 263 | 264 | if (!TextUtils.isEmpty(processName)) { 265 | return processName; 266 | } 267 | 268 | // 利用 Linux 系统获取进程名 269 | FileInputStream inputStream = null; 270 | try { 271 | inputStream = new FileInputStream("/proc/self/cmdline"); 272 | byte[] buffer = new byte[256]; 273 | int len = 0; 274 | int b; 275 | while ((b = inputStream.read()) > 0 && len < buffer.length) { 276 | buffer[len++] = (byte) b; 277 | } 278 | if (len > 0) { 279 | return new String(buffer, 0, len, StandardCharsets.UTF_8); 280 | } 281 | } catch (IOException e) { 282 | e.printStackTrace(); 283 | } finally { 284 | if (inputStream != null) { 285 | try { 286 | inputStream.close(); 287 | } catch (IOException e) { 288 | e.printStackTrace(); 289 | } 290 | } 291 | } 292 | return null; 293 | } 294 | 295 | /** 296 | * 应用生命周期回调 297 | */ 298 | public interface ApplicationLifecycleCallback { 299 | 300 | /** 301 | * 第一个 Activity 创建了 302 | */ 303 | void onApplicationCreate(Activity activity); 304 | 305 | /** 306 | * 最后一个 Activity 销毁了 307 | */ 308 | void onApplicationDestroy(Activity activity); 309 | 310 | /** 311 | * 应用从前台进入到后台 312 | */ 313 | void onApplicationBackground(Activity activity); 314 | 315 | /** 316 | * 应用从后台进入到前台 317 | */ 318 | void onApplicationForeground(Activity activity); 319 | } 320 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [English Doc](README-en.md) 2 | 3 | # 语种切换框架 4 | 5 | * 项目地址:[Github](https://github.com/getActivity/MultiLanguages) 6 | 7 | * 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/MultiLanguages/releases/download/10.2/MultiLanguages.apk) 8 | 9 | ![](picture/dynamic_figure.gif) 10 | 11 | #### 集成步骤 12 | 13 | * 如果你的项目 Gradle 配置是在 `7.0 以下`,需要在 `build.gradle` 文件中加入 14 | 15 | ```groovy 16 | allprojects { 17 | repositories { 18 | // JitPack 远程仓库:https://jitpack.io 19 | maven { url 'https://jitpack.io' } 20 | } 21 | } 22 | ``` 23 | 24 | * 如果你的 Gradle 配置是 `7.0 及以上`,则需要在 `settings.gradle` 文件中加入 25 | 26 | ```groovy 27 | dependencyResolutionManagement { 28 | repositories { 29 | // JitPack 远程仓库:https://jitpack.io 30 | maven { url 'https://jitpack.io' } 31 | } 32 | } 33 | ``` 34 | 35 | * 配置完远程仓库后,在项目 app 模块下的 `build.gradle` 文件中加入远程依赖 36 | 37 | ```groovy 38 | dependencies { 39 | // 语种切换框架:https://github.com/getActivity/MultiLanguages 40 | implementation 'com.github.getActivity:MultiLanguages:10.2' 41 | } 42 | ``` 43 | 44 | #### 初始化框架 45 | 46 | * 在 Application 中初始化框架 47 | 48 | ```java 49 | public final class XxxApplication extends Application { 50 | 51 | @Override 52 | public void onCreate() { 53 | super.onCreate(); 54 | 55 | // 初始化语种切换框架 56 | MultiLanguages.init(this); 57 | } 58 | } 59 | ``` 60 | 61 | * 重写 Application 的 attachBaseContext 方法 62 | 63 | ```java 64 | @Override 65 | protected void attachBaseContext(Context base) { 66 | // 绑定语种 67 | super.attachBaseContext(MultiLanguages.attach(base)); 68 | } 69 | ``` 70 | 71 | * 重写**基类** BaseActivity 的 attachBaseContext 方法 72 | 73 | ```java 74 | @Override 75 | protected void attachBaseContext(Context newBase) { 76 | // 绑定语种 77 | super.attachBaseContext(MultiLanguages.attach(newBase)); 78 | } 79 | ``` 80 | 81 | * 只要是 Context 的子类都需要重写,Service 也雷同,这里不再赘述 82 | 83 | * 温馨提示:Fragment 不需要重写此方法,因为它不是 Context 的子类 84 | 85 | #### 语种设置 86 | 87 | ```java 88 | // 设置当前的语种(返回 true 表示需要重启 App) 89 | MultiLanguages.setAppLanguage(Context context, Locale locale); 90 | 91 | // 获取当前的语种 92 | MultiLanguages.getAppLanguage(Context context); 93 | 94 | // 跟随系统语种(返回 true 表示需要重启 App) 95 | MultiLanguages.clearAppLanguage(Context context); 96 | ``` 97 | 98 | #### 其他 API 99 | 100 | ```java 101 | // 获取系统的语种 102 | MultiLanguages.getSystemLanguage(Context context); 103 | // 是否跟随系统的语种 104 | MultiLanguages.isSystemLanguage(Context context); 105 | 106 | // 对比两个语言是否同一个语种(比如:中文有简体和繁体,英语有美式和英式) 107 | MultiLanguages.equalsLanguage(Locale locale1, Locale locale2); 108 | // 对比两个语言是否同一个语种并且同一个地方的(比如:中国大陆用的中文简体,中国台湾用的中文繁体) 109 | MultiLanguages.equalsLanguageAndCountry(Locale locale1, Locale locale2); 110 | // 注意事项:如果是为了判断两个 Locale 对象是否相同,请使用 equalsLanguageAndCountry 方法来判断,而不是 equalsLanguage 方法, 111 | // 另外请不要用 Locale 类中自带的 equals 方法来判断,因为它会比较更多的信息,会导致判断错误。 112 | // Github 相关问题 issue:https://github.com/getActivity/MultiLanguages/issues/63 113 | 114 | // 获取某个语种下的 String 115 | MultiLanguages.getLanguageString(Context context, Locale locale, int stringId); 116 | // 生成某个语种下的 Resources 对象 117 | MultiLanguages.generateLanguageResources(Context context, Locale locale); 118 | 119 | // 更新 Context 的语种 120 | MultiLanguages.updateAppLanguage(Context context); 121 | // 更新 Resources 的语种 122 | MultiLanguages.updateAppLanguage(Resources resources); 123 | 124 | // 设置默认的语种(越早设置越好) 125 | MultiLanguages.setDefaultLanguage(Locale locale); 126 | ``` 127 | 128 | #### 语种变化监听器 129 | 130 | ```java 131 | // 设置语种变化监听器 132 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 133 | 134 | @Override 135 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 136 | Log.d("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 137 | } 138 | 139 | @Override 140 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 141 | Log.d("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 142 | } 143 | }); 144 | ``` 145 | 146 | #### 使用案例 147 | 148 | ```java 149 | @Override 150 | public void onClick(View v) { 151 | // 是否需要重启 152 | boolean restart; 153 | switch (v.getId()) { 154 | // 跟随系统 155 | case R.id.btn_language_auto: 156 | restart = MultiLanguages.clearAppLanguage(this); 157 | break; 158 | // 简体中文 159 | case R.id.btn_language_cn: 160 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getSimplifiedChineseLocale()); 161 | break; 162 | // 繁体中文 163 | case R.id.btn_language_tw: 164 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getTraditionalChineseLocale()); 165 | break; 166 | // 英语 167 | case R.id.btn_language_en: 168 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getEnglishLocale()); 169 | break; 170 | default: 171 | restart = false; 172 | break; 173 | } 174 | 175 | if (restart) { 176 | // 我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果 177 | Intent intent = new Intent(this, MainActivity.class); 178 | // Github 地址:https://github.com/getActivity/MultiLanguages/issues/55 179 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 180 | startActivity(intent); 181 | overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 182 | finish(); 183 | } 184 | } 185 | ``` 186 | 187 | ## [常见疑问请点击此处查看](HelpDoc.md) 188 | 189 | #### 其他资源:[语言代码列表大全](https://github.com/championswimmer/android-locales) 190 | 191 | #### 作者的其他开源项目 192 | 193 | * 安卓技术中台:[AndroidProject](https://github.com/getActivity/AndroidProject) ![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) 194 | 195 | * 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) 196 | 197 | * 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) 198 | 199 | * 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) 200 | 201 | * 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) 202 | 203 | * 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) 204 | 205 | * 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) 206 | 207 | * 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) 208 | 209 | * Shape 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) 210 | 211 | * Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) 212 | 213 | * 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) 214 | 215 | * Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) 216 | 217 | * Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) 218 | 219 | * Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) 220 | 221 | * Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) 222 | 223 | * Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) 224 | 225 | * 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) 226 | 227 | * AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) 228 | 229 | * 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) 230 | 231 | * Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) 232 | 233 | #### 微信公众号:Android轮子哥 234 | 235 | ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) 236 | 237 | #### Android 技术 Q 群:10047167 238 | 239 | #### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) 240 | 241 | ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) 242 | 243 | ## License 244 | 245 | ```text 246 | Copyright 2019 Huang JinQun 247 | 248 | Licensed under the Apache License, Version 2.0 (the "License"); 249 | you may not use this file except in compliance with the License. 250 | You may obtain a copy of the License at 251 | 252 | http://www.apache.org/licenses/LICENSE-2.0 253 | 254 | Unless required by applicable law or agreed to in writing, software 255 | distributed under the License is distributed on an "AS IS" BASIS, 256 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 257 | See the License for the specific language governing permissions and 258 | limitations under the License. 259 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, July 2019 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Huang JinQun 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # [中文文档](README.md) 2 | 3 | # Language Switching Framework 4 | 5 | * Project address: [Github](https://github.com/getActivity/MultiLanguages) 6 | 7 | * [Click here to download demo apk directly](https://github.com/getActivity/MultiLanguages/releases/download/10.2/MultiLanguages.apk) 8 | 9 | ![](picture/dynamic_figure.gif) 10 | 11 | #### Integration Steps 12 | 13 | * If your project's Gradle configuration is `below 7.0`, add the following to your `build.gradle` file: 14 | 15 | ```groovy 16 | allprojects { 17 | repositories { 18 | // JitPack remote repository: https://jitpack.io 19 | maven { url 'https://jitpack.io' } 20 | } 21 | } 22 | ``` 23 | 24 | * If your Gradle configuration is `7.0 or above`, add the following to your `settings.gradle` file: 25 | 26 | ```groovy 27 | dependencyResolutionManagement { 28 | repositories { 29 | // JitPack remote repository: https://jitpack.io 30 | maven { url 'https://jitpack.io' } 31 | } 32 | } 33 | ``` 34 | 35 | * After configuring the remote repository, add the remote dependency in the `build.gradle` file under your app module: 36 | 37 | ```groovy 38 | dependencies { 39 | // Language switching framework: https://github.com/getActivity/MultiLanguages 40 | implementation 'com.github.getActivity:MultiLanguages:10.2' 41 | } 42 | ``` 43 | 44 | #### Initialize the Framework 45 | 46 | * Initialize the framework in your Application class: 47 | 48 | ```java 49 | public final class XxxApplication extends Application { 50 | 51 | @Override 52 | public void onCreate() { 53 | super.onCreate(); 54 | 55 | // Initialize the language switching framework 56 | MultiLanguages.init(this); 57 | } 58 | } 59 | ``` 60 | 61 | * Override the attachBaseContext method in your Application: 62 | 63 | ```java 64 | @Override 65 | protected void attachBaseContext(Context base) { 66 | // Bind language 67 | super.attachBaseContext(MultiLanguages.attach(base)); 68 | } 69 | ``` 70 | 71 | * Override the attachBaseContext method in your **base class** BaseActivity: 72 | 73 | ```java 74 | @Override 75 | protected void attachBaseContext(Context newBase) { 76 | // Bind language 77 | super.attachBaseContext(MultiLanguages.attach(newBase)); 78 | } 79 | ``` 80 | 81 | * Any subclass of Context needs to override this method, including Service. This will not be repeated here. 82 | 83 | * Note: Fragment does not need to override this method because it is not a subclass of Context. 84 | 85 | #### Language Settings 86 | 87 | ```java 88 | // Set the current language (returns true if the app needs to be restarted) 89 | MultiLanguages.setAppLanguage(Context context, Locale locale); 90 | 91 | // Get the current language 92 | MultiLanguages.getAppLanguage(Context context); 93 | 94 | // Follow system language (returns true if the app needs to be restarted) 95 | MultiLanguages.clearAppLanguage(Context context); 96 | ``` 97 | 98 | #### Other APIs 99 | 100 | ```java 101 | // Get the system language 102 | MultiLanguages.getSystemLanguage(Context context); 103 | // Whether to follow the system language 104 | MultiLanguages.isSystemLanguage(Context context); 105 | 106 | // Compare if two languages are the same (e.g., Simplified and Traditional Chinese, American and British English) 107 | MultiLanguages.equalsLanguage(Locale locale1, Locale locale2); 108 | // Compare whether the two languages are of the same language and from the same place (for example: simplified Chinese used in Chinese mainland and traditional Chinese used in Taiwan, China). 109 | MultiLanguages.equalsLanguageAndCountry(Locale locale1, Locale locale2); 110 | // Notes: If it is to determine whether two Locale objects are the same, please use the equalsLanguageAndCountry method for judgment instead of the equalsLanguage method 111 | // Also, please do not use the equals method that comes with the Locale class to make a judgment, as it will compare more information and may lead to a judgment error. 112 | // Github issue : https://github.com/getActivity/MultiLanguages/issues/63 113 | 114 | // Get a String in a specific language 115 | MultiLanguages.getLanguageString(Context context, Locale locale, int stringId); 116 | // Generate a Resources object for a specific language 117 | MultiLanguages.generateLanguageResources(Context context, Locale locale); 118 | 119 | // Update the language of Context 120 | MultiLanguages.updateAppLanguage(Context context); 121 | // Update the language of Resources 122 | MultiLanguages.updateAppLanguage(Resources resources); 123 | 124 | // Set the default language (the earlier, the better) 125 | MultiLanguages.setDefaultLanguage(Locale locale); 126 | ``` 127 | 128 | #### Language Change Listener 129 | 130 | ```java 131 | // Set language change listener 132 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 133 | 134 | @Override 135 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 136 | Log.d("MultiLanguages", "Detected app language change, old language: " + oldLocale + ", new language: " + newLocale); 137 | } 138 | 139 | @Override 140 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 141 | Log.d("MultiLanguages", "Detected system language change, old language: " + oldLocale + ", new language: " + newLocale + ", following system: " + MultiLanguages.isSystemLanguage()); 142 | } 143 | }); 144 | ``` 145 | 146 | #### Usage Example 147 | 148 | ```java 149 | @Override 150 | public void onClick(View v) { 151 | // Whether restart is needed 152 | boolean restart; 153 | switch (v.getId()) { 154 | // Follow system 155 | case R.id.btn_language_auto: 156 | restart = MultiLanguages.clearAppLanguage(this); 157 | break; 158 | // Simplified Chinese 159 | case R.id.btn_language_cn: 160 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getSimplifiedChineseLocale()); 161 | break; 162 | // Traditional Chinese 163 | case R.id.btn_language_tw: 164 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getTraditionalChineseLocale()); 165 | break; 166 | // English 167 | case R.id.btn_language_en: 168 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getEnglishLocale()); 169 | break; 170 | default: 171 | restart = false; 172 | break; 173 | } 174 | 175 | if (restart) { 176 | // You can make full use of Activity transition animations, and set a fade effect when switching 177 | Intent intent = new Intent(this, MainActivity.class); 178 | // Github address: https://github.com/getActivity/MultiLanguages/issues/55 179 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 180 | startActivity(intent); 181 | overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 182 | finish(); 183 | } 184 | } 185 | ``` 186 | 187 | ## [Click here for Frequently Asked Questions](HelpDoc-en.md) 188 | 189 | #### Other resources: [Complete List of Language Codes](https://github.com/championswimmer/android-locales) 190 | 191 | #### Other Open Source Projects by the Author 192 | 193 | * Android middle office: [AndroidProject](https://github.com/getActivity/AndroidProject)![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) 194 | 195 | * Android middle office kt version: [AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) 196 | 197 | * Permissions framework: [XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) 198 | 199 | * Toast framework: [Toaster](https://github.com/getActivity/Toaster)![](https://img.shields.io/github/stars/getActivity/Toaster.svg)![](https://img.shields.io/github/forks/getActivity/Toaster.svg) 200 | 201 | * Network framework: [EasyHttp](https://github.com/getActivity/EasyHttp)![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg)![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) 202 | 203 | * Title bar framework: [TitleBar](https://github.com/getActivity/TitleBar)![](https://img.shields.io/github/stars/getActivity/TitleBar.svg)![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) 204 | 205 | * Floating window framework: [EasyWindow](https://github.com/getActivity/EasyWindow)![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg)![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) 206 | 207 | * Device compatibility framework:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) 208 | 209 | * Shape view framework: [ShapeView](https://github.com/getActivity/ShapeView)![](https://img.shields.io/github/stars/getActivity/ShapeView.svg)![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) 210 | 211 | * Shape drawable framework: [ShapeDrawable](https://github.com/getActivity/ShapeDrawable)![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg)![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) 212 | 213 | * Gson parsing fault tolerance: [GsonFactory](https://github.com/getActivity/GsonFactory)![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg)![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) 214 | 215 | * Logcat viewing framework: [Logcat](https://github.com/getActivity/Logcat)![](https://img.shields.io/github/stars/getActivity/Logcat.svg)![](https://img.shields.io/github/forks/getActivity/Logcat.svg) 216 | 217 | * Nested scrolling layout framework:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) 218 | 219 | * Android version guide: [AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg)![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) 220 | 221 | * Android code standard: [AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg)![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) 222 | 223 | * Android resource summary:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) 224 | 225 | * Android open source leaderboard: [AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg)![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) 226 | 227 | * Studio boutique plugins: [StudioPlugins](https://github.com/getActivity/StudioPlugins)![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg)![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) 228 | 229 | * Emoji collection: [EmojiPackage](https://github.com/getActivity/EmojiPackage)![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg)![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) 230 | 231 | * China provinces json: [ProvinceJson](https://github.com/getActivity/ProvinceJson)![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg)![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) 232 | 233 | * Markdown documentation:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) 234 | 235 | ## License 236 | 237 | ```text 238 | Copyright 2019 Huang JinQun 239 | 240 | Licensed under the Apache License, Version 2.0 (the "License"); 241 | you may not use this file except in compliance with the License. 242 | You may obtain a copy of the License at 243 | 244 | http://www.apache.org/licenses/LICENSE-2.0 245 | 246 | Unless required by applicable law or agreed to in writing, software 247 | distributed under the License is distributed on an "AS IS" BASIS, 248 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 249 | See the License for the specific language governing permissions and 250 | limitations under the License. 251 | ``` -------------------------------------------------------------------------------- /HelpDoc-en.md: -------------------------------------------------------------------------------- 1 | #### Table of Contents 2 | 3 | * [How to Set the Default Language of the App](#how-to-set-the-default-language-of-the-app) 4 | 5 | * [What to Do If Switching Language Has No Effect](#what-to-do-if-switching-language-has-no-effect) 6 | 7 | * [How to Apply Language Change to All Activities](#how-to-apply-language-change-to-all-activities) 8 | 9 | * [How to Refresh All Screens When the User Switches System Language](#how-to-refresh-all-screens-when-the-user-switches-system-language) 10 | 11 | * [What to Do If Returning to the App After System Language Switch Has No Effect](#what-to-do-if-returning-to-the-app-after-system-language-switch-has-no-effect) 12 | 13 | * [Solution for WebView Causing Language Failure](#solution-for-webview-causing-language-failure) 14 | 15 | * [What to Do If WebView Language Fails on Android 13](#what-to-do-if-webview-language-fails-on-android-13) 16 | 17 | * [Is There a Way to Switch Language Without Restarting](#is-there-a-way-to-switch-language-without-restarting) 18 | 19 | #### How to Set the Default Language of the App 20 | 21 | * If you have never called `MultiLanguages.setAppLanguage` and do not want the app to follow the system language, but want to specify a certain language, you can do as follows: 22 | 23 | ```java 24 | public final class XxxApplication extends Application { 25 | 26 | static { 27 | // Set the default language (the earlier, the better) 28 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 29 | } 30 | } 31 | ``` 32 | 33 | * If you want to determine the current system language type and then set the default language, you can write: 34 | 35 | ```java 36 | public final class XxxApplication extends Application { 37 | 38 | @Override 39 | protected void attachBaseContext(Context newBase) { 40 | if (newBase != null) { 41 | Locale systemLanguage = MultiLanguages.getSystemLanguage(newBase); 42 | // If the current language is neither Chinese (including Simplified and Traditional) nor English (including American and British), set it to English by default to avoid following the system language 43 | if (!MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getChineseLocale()) && 44 | !MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getEnglishLocale())) { 45 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 46 | } 47 | } 48 | // Bind language 49 | super.attachBaseContext(MultiLanguages.attach(newBase)); 50 | } 51 | } 52 | ``` 53 | 54 | #### What to Do If Switching Language Has No Effect 55 | 56 | * Case 1: Check if you have configured to keep only certain country language resources in the `build.gradle` file, for example, `resConfigs 'zh'` means only Chinese-related language resources are kept, and other countries' language resources will not be packaged into the apk, which will cause the language to always be Chinese when switching. 57 | 58 | * Case 2: If the language switch in Fragment has no effect, check if the Fragment is reused after the Activity restarts, usually caused by calling `fragment.setRetainInstance(true)`. 59 | 60 | * Case 3: If you cannot switch the app language after uploading the aab package to Google Play or Huawei and downloading it, it is because multi-language resources are missing during splitting. You need to configure the main module's `build.gradle` file. For details, refer to the [official documentation](https://developer.android.google.cn/guide/app-bundle/configure-base?hl=en). 61 | 62 | ```groovy 63 | android { 64 | 65 | ...... 66 | 67 | bundle { 68 | 69 | ...... 70 | 71 | language { 72 | enableSplit = false 73 | } 74 | 75 | ...... 76 | } 77 | } 78 | ``` 79 | 80 | * Other cases: If the above reasons do not apply, please submit an [issue](https://github.com/getActivity/MultiLanguages/issues) for assistance. 81 | 82 | #### How to Apply Language Change to All Activities 83 | 84 | * Add the following code in the onCreate method of Application: 85 | 86 | ```java 87 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 88 | 89 | @Override 90 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 91 | Log.i("MultiLanguages", "Detected app language change, old language: " + oldLocale + ", new language: " + newLocale); 92 | // If you want the app to change with the system language, get all Activities here and call their recreate method 93 | // getAllActivity is just demonstration code, replace it with your own implementation. If not available, please encapsulate it yourself 94 | List activityList = getAllActivity(); 95 | for (Activity activity : activityList) { 96 | activity.recreate(); 97 | } 98 | } 99 | 100 | @Override 101 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 102 | Log.i("MultiLanguages", "Detected system language change, old language: " + oldLocale + ", new language: " + newLocale + ", following system: " + MultiLanguages.isSystemLanguage()); 103 | } 104 | }); 105 | ``` 106 | 107 | #### How to Refresh All Screens When the User Switches System Language 108 | 109 | * Add the following code in the onCreate method of Application: 110 | 111 | ```java 112 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 113 | 114 | @Override 115 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 116 | Log.i("MultiLanguages", "Detected app language change, old language: " + oldLocale + ", new language: " + newLocale); 117 | } 118 | 119 | @Override 120 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 121 | Log.i("MultiLanguages", "Detected system language change, old language: " + oldLocale + ", new language: " + newLocale + ", following system: " + MultiLanguages.isSystemLanguage()); 122 | 123 | // If the current language is not following the system, do not proceed 124 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 125 | return; 126 | } 127 | 128 | // If you want the app to change with the system language, get all Activities here and call their recreate method 129 | // getAllActivity is just demonstration code, replace it with your own implementation. If not available, please encapsulate it yourself 130 | List activityList = getAllActivity(); 131 | for (Activity activity : activityList) { 132 | activity.recreate(); 133 | } 134 | } 135 | }); 136 | ``` 137 | 138 | #### What to Do If Returning to the App After System Language Switch Has No Effect 139 | 140 | * Please check if the Activity is configured with the `android:configChanges` attribute. If so, remove it and try again. For documentation, see [``](https://developer.android.com/guide/topics/manifest/activity-element?hl=en) 141 | 142 | ```xml 143 | 146 | ``` 147 | 148 | * If you do not want to remove the `android:configChanges` attribute but want the language switch to take effect after switching the system language, add the following code in the onCreate method of Application: 149 | 150 | ```java 151 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 152 | 153 | @Override 154 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 155 | Log.i("MultiLanguages", "Detected app language change, old language: " + oldLocale + ", new language: " + newLocale); 156 | } 157 | 158 | @Override 159 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 160 | Log.i("MultiLanguages", "Detected system language change, old language: " + oldLocale + ", new language: " + newLocale + ", following system: " + MultiLanguages.isSystemLanguage()); 161 | 162 | // If the current language is not following the system, do not proceed 163 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 164 | return; 165 | } 166 | 167 | // If you want the app to change with the system language, get all Activities here and call their recreate method 168 | // getAllActivity is just demonstration code, replace it with your own implementation. If not available, please encapsulate it yourself 169 | List activityList = getAllActivity(); 170 | for (Activity activity : activityList) { 171 | activity.recreate(); 172 | } 173 | } 174 | }); 175 | ``` 176 | 177 | #### Solution for WebView Causing Language Failure 178 | 179 | * Since WebView initialization will modify the Activity language configuration, which indirectly causes the Activity language to be restored, you need to manually override WebView to fix this issue 180 | 181 | ```java 182 | public final class LanguagesWebView extends WebView { 183 | 184 | public LanguagesWebView(@NonNull Context context) { 185 | this(context, null); 186 | } 187 | 188 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 189 | this(context, attrs, android.R.attr.webViewStyle); 190 | } 191 | 192 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 193 | super(context, attrs, defStyleAttr); 194 | 195 | // Fix the issue where WebView initialization modifies the Activity language configuration 196 | MultiLanguages.updateAppLanguage(context); 197 | } 198 | } 199 | ``` 200 | 201 | #### What to Do If WebView Language Fails on Android 13 202 | 203 | * Replace `webView.loadUrl(@NonNull String url)` with `webView.loadUrl(@NonNull String url, @NonNull Map additionalHttpHeaders)` and add the `Accept-Language` request header. Example code: 204 | 205 | ```java 206 | public final class MainActivity extends Activity { 207 | 208 | private WebView mWebView; 209 | 210 | @Override 211 | protected void onCreate(Bundle savedInstanceState) { 212 | super.onCreate(savedInstanceState); 213 | setContentView(R.layout.activity_main); 214 | 215 | mWebView = findViewById(R.id.wv_main_web); 216 | 217 | mWebView.setWebViewClient(new LanguagesViewClient()); 218 | mWebView.setWebChromeClient(new WebChromeClient()); 219 | mWebView.loadUrl("https://developer.android.google.cn/kotlin", generateLanguageRequestHeader(this)); 220 | } 221 | 222 | public static class LanguagesViewClient extends WebViewClient { 223 | 224 | @TargetApi(Build.VERSION_CODES.N) 225 | @Override 226 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 227 | return shouldOverrideUrlLoading(view, request.getUrl().toString()); 228 | } 229 | 230 | @Override 231 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 232 | String scheme = Uri.parse(url).getScheme(); 233 | if (scheme == null) { 234 | return false; 235 | } 236 | switch (scheme) { 237 | // If this is a link jump 238 | case "http": 239 | case "https": 240 | view.loadUrl(url, generateLanguageRequestHeader(view.getContext())); 241 | break; 242 | default: 243 | break; 244 | } 245 | return true; 246 | } 247 | } 248 | 249 | /** 250 | * Add language environment to WebView request header 251 | */ 252 | @NonNull 253 | public static Map generateLanguageRequestHeader(Context context) { 254 | Map map = new HashMap<>(1); 255 | // Solution for language failure on Android 13 256 | // https://developer.android.com/about/versions/13/features/app-languages#consider-header 257 | map.put("Accept-Language", String.valueOf(MultiLanguages.getAppLanguage(context))); 258 | return map; 259 | } 260 | } 261 | ``` 262 | 263 | #### Is There a Way to Switch Language Without Restarting 264 | 265 | * Let me ask everyone a question: once rice is cooked, how do you turn it back into raw rice? Obviously, this is unrealistic. Even if the framework could do it, and text and images could automatically change with the language, how would you switch the language for data requested via the network? Would you need to request again? If it's a list, would you need to request from page 1? Another question: is language switching a common action? I believe you already have the answer. 266 | 267 | * So it's not that switching language without restarting is impossible, but it's unnecessary (language switching is not a common action), and there are some inherent issues (even if the UI doesn't need to change, the data layer still needs to be re-requested). -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LocaleContract.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * author : Android 轮子哥 7 | * github : https://github.com/getActivity/MultiLanguages 8 | * time : 2022/04/08 9 | * desc : 语种契约类 10 | * doc : https://blog.csdn.net/liuhhaiffeng/article/details/54706027 11 | */ 12 | public final class LocaleContract { 13 | 14 | /** 中文 */ 15 | private static volatile Locale sChineseLocale; 16 | 17 | public static Locale getChineseLocale() { 18 | if (sChineseLocale == null) { 19 | sChineseLocale = Locale.CHINESE; 20 | } 21 | return sChineseLocale; 22 | } 23 | 24 | /** 简体中文 */ 25 | private static volatile Locale sSimplifiedChineseLocale; 26 | 27 | public static Locale getSimplifiedChineseLocale() { 28 | if (sSimplifiedChineseLocale == null) { 29 | sSimplifiedChineseLocale = Locale.SIMPLIFIED_CHINESE; 30 | } 31 | return sSimplifiedChineseLocale; 32 | } 33 | 34 | /** 繁体中文 */ 35 | private static volatile Locale sTraditionalChineseLocale; 36 | 37 | public static Locale getTraditionalChineseLocale() { 38 | if (sTraditionalChineseLocale == null) { 39 | sTraditionalChineseLocale = Locale.TRADITIONAL_CHINESE; 40 | } 41 | return sTraditionalChineseLocale; 42 | } 43 | 44 | public static Locale getTaiWanLocale() { 45 | return getTraditionalChineseLocale(); 46 | } 47 | 48 | /** 新加坡 */ 49 | private static volatile Locale sSingaporeLocale ; 50 | 51 | public static Locale getSingaporeLocale() { 52 | if (sSimplifiedChineseLocale == null) { 53 | sSingaporeLocale = new Locale("zh", "SG"); 54 | } 55 | return sSingaporeLocale; 56 | } 57 | 58 | /** 英语 */ 59 | private static volatile Locale sEnglishLocale; 60 | 61 | public static Locale getEnglishLocale() { 62 | if (sEnglishLocale == null) { 63 | sEnglishLocale = Locale.ENGLISH; 64 | } 65 | return sEnglishLocale; 66 | } 67 | 68 | /** 法语 */ 69 | private static volatile Locale sFrenchLocale; 70 | 71 | public static Locale getFrenchLocale() { 72 | if (sFrenchLocale == null) { 73 | sFrenchLocale = Locale.FRENCH; 74 | } 75 | return sFrenchLocale; 76 | } 77 | 78 | /** 德语 */ 79 | private static volatile Locale sGermanLocale; 80 | 81 | public static Locale getGermanLocale() { 82 | if (sGermanLocale == null) { 83 | sGermanLocale = Locale.GERMAN; 84 | } 85 | return sGermanLocale; 86 | } 87 | 88 | /** 意大利语 */ 89 | private static volatile Locale sItalianLocale; 90 | 91 | public static Locale getItalianLocale() { 92 | if (sItalianLocale == null) { 93 | sItalianLocale = Locale.ITALIAN; 94 | } 95 | return sItalianLocale; 96 | } 97 | 98 | /** 日语 */ 99 | private static volatile Locale sJapaneseLocale; 100 | 101 | public static Locale getJapaneseLocale() { 102 | if (sJapaneseLocale == null) { 103 | sJapaneseLocale = Locale.JAPANESE; 104 | } 105 | return sJapaneseLocale; 106 | } 107 | 108 | /** 韩语 */ 109 | private static volatile Locale sKoreanLocale; 110 | 111 | public static Locale getKoreanLocale() { 112 | if (sKoreanLocale == null) { 113 | sKoreanLocale = Locale.KOREAN; 114 | } 115 | return sKoreanLocale; 116 | } 117 | 118 | /** 越南语 */ 119 | private static volatile Locale sVietnameseLocale; 120 | 121 | public static Locale getVietnameseLocale() { 122 | if (sVietnameseLocale == null) { 123 | sVietnameseLocale = new Locale("vi"); 124 | } 125 | return sVietnameseLocale; 126 | } 127 | 128 | /** 荷兰语 */ 129 | private static volatile Locale sDutchLocale; 130 | 131 | public static Locale getDutchLocale() { 132 | if (sDutchLocale == null) { 133 | sDutchLocale = new Locale("af"); 134 | } 135 | return sDutchLocale; 136 | } 137 | 138 | /** 阿尔巴尼亚语 */ 139 | private static volatile Locale sAlbanianLocale; 140 | 141 | public static Locale getAlbanianLocale() { 142 | if (sAlbanianLocale == null) { 143 | sAlbanianLocale = new Locale("sq"); 144 | } 145 | return sAlbanianLocale; 146 | } 147 | 148 | /** 阿拉伯语 */ 149 | private static volatile Locale sArabicLocale; 150 | 151 | public static Locale getArabicLocale() { 152 | if (sArabicLocale == null) { 153 | sArabicLocale = new Locale("ar"); 154 | } 155 | return sArabicLocale; 156 | } 157 | 158 | /** 亚美尼亚语 */ 159 | private static volatile Locale sArmenianLocale; 160 | 161 | public static Locale getArmenianLocale() { 162 | if (sArmenianLocale == null) { 163 | sArmenianLocale = new Locale("hy"); 164 | } 165 | return sArmenianLocale; 166 | } 167 | 168 | /** 阿塞拜疆语 */ 169 | private static volatile Locale sAzerbaijaniLocale; 170 | 171 | public static Locale getAzerbaijaniLocale() { 172 | if (sAzerbaijaniLocale == null) { 173 | sAzerbaijaniLocale = new Locale("az"); 174 | } 175 | return sAzerbaijaniLocale; 176 | } 177 | 178 | /** 巴斯克语 */ 179 | private static volatile Locale sBasqueLocale; 180 | 181 | public static Locale getBasqueLocale() { 182 | if (sBasqueLocale == null) { 183 | sBasqueLocale = new Locale("eu"); 184 | } 185 | return sBasqueLocale; 186 | } 187 | 188 | /** 白俄罗斯 */ 189 | private static volatile Locale sBelarusianLocale; 190 | 191 | public static Locale getBelarusianLocale() { 192 | if (sBelarusianLocale == null) { 193 | sBelarusianLocale = new Locale("be"); 194 | } 195 | return sBelarusianLocale; 196 | } 197 | 198 | /** 保加利亚 */ 199 | private static volatile Locale sBulgariaLocale; 200 | 201 | public static Locale getBulgariaLocale() { 202 | if (sBulgariaLocale == null) { 203 | sBulgariaLocale = new Locale("bg"); 204 | } 205 | return sBulgariaLocale; 206 | } 207 | 208 | /** 加泰罗尼亚 */ 209 | private static volatile Locale sCatalonianLocale; 210 | 211 | public static Locale getCatalonianLocale() { 212 | if (sCatalonianLocale == null) { 213 | sCatalonianLocale = new Locale("ca"); 214 | } 215 | return sCatalonianLocale; 216 | } 217 | 218 | /** 克罗埃西亚 */ 219 | private static volatile Locale sCroatiaLocale; 220 | 221 | public static Locale getCroatiaLocale() { 222 | if (sCroatiaLocale == null) { 223 | sCroatiaLocale = new Locale("hr"); 224 | } 225 | return sCroatiaLocale; 226 | } 227 | 228 | /** 捷克 */ 229 | private static volatile Locale sCzechRepublicLocale; 230 | 231 | public static Locale getCzechRepublicLocale() { 232 | if (sCzechRepublicLocale == null) { 233 | sCzechRepublicLocale = new Locale("cs"); 234 | } 235 | return sCzechRepublicLocale; 236 | } 237 | 238 | /** 丹麦文 */ 239 | private static volatile Locale sDanishLocale; 240 | 241 | public static Locale getDanishLocale() { 242 | if (sDanishLocale == null) { 243 | sDanishLocale = new Locale("da"); 244 | } 245 | return sDanishLocale; 246 | } 247 | 248 | /** 迪维希语 */ 249 | private static volatile Locale sDhivehiLocale; 250 | 251 | public static Locale getDhivehiLocale() { 252 | if (sDhivehiLocale == null) { 253 | sDhivehiLocale = new Locale("div"); 254 | } 255 | return sDhivehiLocale; 256 | } 257 | 258 | /** 荷兰 */ 259 | private static volatile Locale sNetherlandsLocale; 260 | 261 | public static Locale getNetherlandsLocale() { 262 | if (sNetherlandsLocale == null) { 263 | sNetherlandsLocale = new Locale("nl"); 264 | } 265 | return sNetherlandsLocale; 266 | } 267 | 268 | /** 法罗语 */ 269 | private static volatile Locale sFaroeseLocale; 270 | 271 | public static Locale getFaroeseLocale() { 272 | if (sFaroeseLocale == null) { 273 | sFaroeseLocale = new Locale("fo"); 274 | } 275 | return sFaroeseLocale; 276 | } 277 | 278 | /** 爱沙尼亚 */ 279 | private static volatile Locale sEstoniaLocale; 280 | 281 | public static Locale getEstoniaLocale() { 282 | if (sEstoniaLocale == null) { 283 | sEstoniaLocale = new Locale("et"); 284 | } 285 | return sEstoniaLocale; 286 | } 287 | 288 | /** 波斯语 */ 289 | private static volatile Locale sFarsiLocale; 290 | 291 | public static Locale getFarsiLocale() { 292 | if (sFarsiLocale == null) { 293 | sFarsiLocale = new Locale("fa"); 294 | } 295 | return sFarsiLocale; 296 | } 297 | 298 | /** 芬兰语 */ 299 | private static volatile Locale sFinnishLocale; 300 | 301 | public static Locale getFinnishLocale() { 302 | if (sFinnishLocale == null) { 303 | sFinnishLocale = new Locale("fi"); 304 | } 305 | return sFinnishLocale; 306 | } 307 | 308 | /** 加利西亚 */ 309 | private static volatile Locale sGaliciaLocale; 310 | 311 | public static Locale getGaliciaLocale() { 312 | if (sGaliciaLocale == null) { 313 | sGaliciaLocale = new Locale("gl"); 314 | } 315 | return sGaliciaLocale; 316 | } 317 | 318 | /** 格鲁吉亚州 */ 319 | private static volatile Locale sGeorgiaLocale; 320 | 321 | public static Locale getGeorgiaLocale() { 322 | if (sGeorgiaLocale == null) { 323 | sGeorgiaLocale = new Locale("ka"); 324 | } 325 | return sGeorgiaLocale; 326 | } 327 | 328 | /** 希腊 */ 329 | private static volatile Locale sGreeceLocale; 330 | 331 | public static Locale getGreeceLocale() { 332 | if (sGreeceLocale == null) { 333 | sGreeceLocale = new Locale("el"); 334 | } 335 | return sGreeceLocale; 336 | } 337 | 338 | /** 古吉拉特语 */ 339 | private static volatile Locale sGujaratiLocale; 340 | 341 | public static Locale getGujaratiLocale() { 342 | if (sGujaratiLocale == null) { 343 | sGujaratiLocale = new Locale("gu"); 344 | } 345 | return sGujaratiLocale; 346 | } 347 | 348 | /** 希伯来 */ 349 | private static volatile Locale sHebrewLocale; 350 | 351 | public static Locale getHebrewLocale() { 352 | if (sHebrewLocale == null) { 353 | sHebrewLocale = new Locale("he"); 354 | } 355 | return sHebrewLocale; 356 | } 357 | 358 | /** 北印度语 */ 359 | private static volatile Locale sHindiLocale; 360 | 361 | public static Locale getHindiLocale() { 362 | if (sHindiLocale == null) { 363 | sHindiLocale = new Locale("hi"); 364 | } 365 | return sHindiLocale; 366 | } 367 | 368 | /** 匈牙利 */ 369 | private static volatile Locale sHungaryLocale; 370 | 371 | public static Locale getHungaryLocale() { 372 | if (sHungaryLocale == null) { 373 | sHungaryLocale = new Locale("hu"); 374 | } 375 | return sHungaryLocale; 376 | } 377 | 378 | /** 冰岛语 */ 379 | private static volatile Locale sIcelandicLocale; 380 | 381 | public static Locale getIcelandicLocale() { 382 | if (sIcelandicLocale == null) { 383 | sIcelandicLocale = new Locale("is"); 384 | } 385 | return sIcelandicLocale; 386 | } 387 | 388 | /** 印尼 */ 389 | private static volatile Locale sIndonesiaLocale; 390 | 391 | public static Locale getIndonesiaLocale() { 392 | if (sIndonesiaLocale == null) { 393 | sIndonesiaLocale = new Locale("id"); 394 | } 395 | return sIndonesiaLocale; 396 | } 397 | 398 | /** 卡纳达语 */ 399 | private static volatile Locale sKannadaLocale; 400 | 401 | public static Locale getKannadaLocale() { 402 | if (sKannadaLocale == null) { 403 | sKannadaLocale = new Locale("kn"); 404 | } 405 | return sKannadaLocale; 406 | } 407 | 408 | /** 哈萨克语 */ 409 | private static volatile Locale sKazakhLocale; 410 | 411 | public static Locale getKazakhLocale() { 412 | if (sKazakhLocale == null) { 413 | sKazakhLocale = new Locale("kk"); 414 | } 415 | return sKazakhLocale; 416 | } 417 | 418 | /** 贡根文 */ 419 | private static volatile Locale sKonkaniLocale; 420 | 421 | public static Locale getKonkaniLocale() { 422 | if (sKonkaniLocale == null) { 423 | sKonkaniLocale = new Locale("kok"); 424 | } 425 | return sKonkaniLocale; 426 | } 427 | 428 | /** 吉尔吉斯斯坦 */ 429 | private static volatile Locale sKyrgyzLocale; 430 | 431 | public static Locale getKyrgyzLocale() { 432 | if (sKyrgyzLocale == null) { 433 | sKyrgyzLocale = new Locale("ky"); 434 | } 435 | return sKyrgyzLocale; 436 | } 437 | 438 | /** 拉脱维亚 */ 439 | private static volatile Locale sLatviaLocale; 440 | 441 | public static Locale getLatviaLocale() { 442 | if (sLatviaLocale == null) { 443 | sLatviaLocale = new Locale("lv"); 444 | } 445 | return sLatviaLocale; 446 | } 447 | 448 | /** 立陶宛 */ 449 | private static volatile Locale sLithuaniaLocale; 450 | 451 | public static Locale getLithuaniaLocale() { 452 | if (sLithuaniaLocale == null) { 453 | sLithuaniaLocale = new Locale("lt"); 454 | } 455 | return sLithuaniaLocale; 456 | } 457 | 458 | /** 马其顿 */ 459 | private static volatile Locale sMacedoniaLocale; 460 | 461 | public static Locale getMacedoniaLocale() { 462 | if (sMacedoniaLocale == null) { 463 | sMacedoniaLocale = new Locale("mk"); 464 | } 465 | return sMacedoniaLocale; 466 | } 467 | 468 | /** 马来 */ 469 | private static volatile Locale sMalayLocale; 470 | 471 | public static Locale getMalayLocale() { 472 | if (sMalayLocale == null) { 473 | sMalayLocale = new Locale("ms"); 474 | } 475 | return sMalayLocale; 476 | } 477 | 478 | /** 马拉地语 */ 479 | private static volatile Locale sMarathiLocale; 480 | 481 | public static Locale getMarathiLocale() { 482 | if (sMarathiLocale == null) { 483 | sMarathiLocale = new Locale("mr"); 484 | } 485 | return sMarathiLocale; 486 | } 487 | 488 | /** 蒙古 */ 489 | private static volatile Locale sMongoliaLocale; 490 | 491 | public static Locale getMongoliaLocale() { 492 | if (sMongoliaLocale == null) { 493 | sMongoliaLocale = new Locale("mn"); 494 | } 495 | return sMongoliaLocale; 496 | } 497 | 498 | /** 挪威 */ 499 | private static volatile Locale sNorwayLocale; 500 | 501 | public static Locale getNorwayLocale() { 502 | if (sNorwayLocale == null) { 503 | sNorwayLocale = new Locale("no"); 504 | } 505 | return sNorwayLocale; 506 | } 507 | 508 | /** 波兰 */ 509 | private static volatile Locale sPolandLocale; 510 | 511 | public static Locale getPolandLocale() { 512 | if (sPolandLocale == null) { 513 | sPolandLocale = new Locale("pl"); 514 | } 515 | return sPolandLocale; 516 | } 517 | 518 | /** 葡萄牙语 */ 519 | private static volatile Locale sPortugueseLocale; 520 | 521 | public static Locale getPortugalLocale() { 522 | if (sPortugueseLocale == null) { 523 | sPortugueseLocale = new Locale("pt"); 524 | } 525 | return sPortugueseLocale; 526 | } 527 | 528 | /** 旁遮普语(印度和巴基斯坦语) */ 529 | private static volatile Locale sPunjabLocale; 530 | 531 | public static Locale getPunjabLocale() { 532 | if (sPunjabLocale == null) { 533 | sPunjabLocale = new Locale("pa"); 534 | } 535 | return sPunjabLocale; 536 | } 537 | 538 | /** 罗马尼亚语 */ 539 | private static volatile Locale sRomanianLocale; 540 | 541 | public static Locale getRomanianLocale() { 542 | if (sRomanianLocale == null) { 543 | sRomanianLocale = new Locale("ro"); 544 | } 545 | return sRomanianLocale; 546 | } 547 | 548 | /** 俄国 */ 549 | private static volatile Locale sRussiaLocale; 550 | 551 | public static Locale getRussiaLocale() { 552 | if (sRussiaLocale == null) { 553 | sRussiaLocale = new Locale("ru"); 554 | } 555 | return sRussiaLocale; 556 | } 557 | 558 | /** 梵文 */ 559 | private static volatile Locale sSanskritLocale; 560 | 561 | public static Locale getSanskritLocale() { 562 | if (sSanskritLocale == null) { 563 | sSanskritLocale = new Locale("sa"); 564 | } 565 | return sSanskritLocale; 566 | } 567 | 568 | /** 斯洛伐克 */ 569 | private static volatile Locale sSlovakiaLocale; 570 | 571 | public static Locale getSlovakiaLocale() { 572 | if (sSlovakiaLocale == null) { 573 | sSlovakiaLocale = new Locale("sk"); 574 | } 575 | return sSlovakiaLocale; 576 | } 577 | 578 | /** 斯洛文尼亚 */ 579 | private static volatile Locale sSloveniaLocale; 580 | 581 | public static Locale getSloveniaLocale() { 582 | if (sSloveniaLocale == null) { 583 | sSloveniaLocale = new Locale("sl"); 584 | } 585 | return sSloveniaLocale; 586 | } 587 | 588 | /** 西班牙语 */ 589 | private static volatile Locale sSpanishLocale; 590 | 591 | public static Locale getSpainLocale() { 592 | if (sSpanishLocale == null) { 593 | sSpanishLocale = new Locale("es"); 594 | } 595 | return sSpanishLocale; 596 | } 597 | 598 | /** 斯瓦希里语 */ 599 | private static volatile Locale sSwahiliLocale; 600 | 601 | public static Locale getSwahiliLocale() { 602 | if (sSwahiliLocale == null) { 603 | sSwahiliLocale = new Locale("sw"); 604 | } 605 | return sSwahiliLocale; 606 | } 607 | 608 | /** 瑞典 */ 609 | private static volatile Locale sSwedenLocale; 610 | 611 | public static Locale getSwedenLocale() { 612 | if (sSwedenLocale == null) { 613 | sSwedenLocale = new Locale("sv"); 614 | } 615 | return sSwedenLocale; 616 | } 617 | 618 | /** 叙利亚语 */ 619 | private static volatile Locale sSyriacLocale; 620 | 621 | public static Locale getSyriacLocale() { 622 | if (sSyriacLocale == null) { 623 | sSyriacLocale = new Locale("syr"); 624 | } 625 | return sSyriacLocale; 626 | } 627 | 628 | /** 坦米尔 */ 629 | private static volatile Locale sTamilLocale; 630 | 631 | public static Locale getTamilLocale() { 632 | if (sTamilLocale == null) { 633 | sTamilLocale = new Locale("ta"); 634 | } 635 | return sTamilLocale; 636 | } 637 | 638 | /** 泰语 */ 639 | private static volatile Locale sThailandLocale; 640 | 641 | public static Locale getThailandLocale() { 642 | if (sThailandLocale == null) { 643 | sThailandLocale = new Locale("th"); 644 | } 645 | return sThailandLocale; 646 | } 647 | 648 | /** 鞑靼语 */ 649 | private static volatile Locale sTatarLocale; 650 | 651 | public static Locale getTatarLocale() { 652 | if (sTatarLocale == null) { 653 | sTatarLocale = new Locale("tt"); 654 | } 655 | return sTatarLocale; 656 | } 657 | 658 | /** 泰卢固语 */ 659 | private static volatile Locale sTeluguLocale; 660 | 661 | public static Locale getTeluguLocale() { 662 | if (sTeluguLocale == null) { 663 | sTeluguLocale = new Locale("te"); 664 | } 665 | return sTeluguLocale; 666 | } 667 | 668 | /** 土耳其语 */ 669 | private static volatile Locale sTurkishLocale; 670 | 671 | public static Locale getTurkishLocale() { 672 | if (sTurkishLocale == null) { 673 | sTurkishLocale = new Locale("tr"); 674 | } 675 | return sTurkishLocale; 676 | } 677 | 678 | /** 乌克兰 */ 679 | private static volatile Locale sUkraineLocale; 680 | 681 | public static Locale getUkraineLocale() { 682 | if (sUkraineLocale == null) { 683 | sUkraineLocale = new Locale("uk"); 684 | } 685 | return sUkraineLocale; 686 | } 687 | 688 | /** 乌尔都语 */ 689 | private static volatile Locale sUrduLocale; 690 | 691 | public static Locale getUrduLocale() { 692 | if (sUrduLocale == null) { 693 | sUrduLocale = new Locale("ur"); 694 | } 695 | return sUrduLocale; 696 | } 697 | 698 | /** 乌兹别克语 */ 699 | private static volatile Locale sUzbekLocale; 700 | 701 | public static Locale getUzbekLocale() { 702 | if (sUzbekLocale == null) { 703 | sUzbekLocale = new Locale("uz"); 704 | } 705 | return sUzbekLocale; 706 | } 707 | } --------------------------------------------------------------------------------