diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml
index 73b1ae679b..ca79447d3a 100644
--- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml
+++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml
@@ -261,6 +261,12 @@
android:parentActivityName=".KeymanSettingsActivity"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Base" />
+
> list = null;
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java
index ede2962217..d159e1a197 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java
@@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.data.Keyboard;
@@ -29,7 +30,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.widget.TextView;
-public class GetStartedActivity extends AppCompatActivity {
+public class GetStartedActivity extends BaseActivity {
private static ListView listView = null;
private static ArrayList> list = null;
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java
index 01bb79d4f5..7802f239ca 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java
@@ -4,6 +4,7 @@
package com.tavultesoft.kmapro;
+import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.BuildConfig;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.KMManager.FormFactor;
@@ -20,7 +21,7 @@ import android.widget.TextView;
import android.annotation.SuppressLint;
import androidx.appcompat.app.AppCompatActivity;
-public class InfoActivity extends AppCompatActivity {
+public class InfoActivity extends BaseActivity {
private final static String TAG = "InfoActivity";
private WebView webView;
private final String HELP_PRODUCTION_HOST = "help.keyman.com";
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java
index 6e3ef52681..f25af7371a 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java
@@ -19,6 +19,7 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
+import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.util.KMPLink;
@@ -27,7 +28,7 @@ import java.util.regex.Pattern;
import static android.app.Application.getProcessName;
-public class KMPBrowserActivity extends AppCompatActivity {
+public class KMPBrowserActivity extends BaseActivity {
private static final String TAG = "KMPBrowserActivity";
// URL for keyboard search web page presented to user when they add a keyboard in the app.
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java
index b681fff7e0..10bd0f9c2c 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java
@@ -2,10 +2,12 @@ package com.tavultesoft.kmapro;
import android.os.Bundle;
+import com.tavultesoft.kmea.BaseActivity;
+
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
-public class KeymanSettingsActivity extends AppCompatActivity {
+public class KeymanSettingsActivity extends BaseActivity {
protected static final String installedLanguagesKey = "InstalledLanguages";
protected static final String installKeyboardOrDictionaryKey = "InstallKeyboardOrDictionary";
protected static final String showBannerKey = "ShowBanner";
@@ -20,6 +22,7 @@ public class KeymanSettingsActivity extends AppCompatActivity {
setContentView(R.layout.keyman_settings_layout);
Toolbar toolbar = (Toolbar) findViewById(R.id.keyman_settings_toolbar);
toolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
+ toolbar.setTitle(R.string.keyman_settings);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java
index fa15d4c1b9..468367f879 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java
@@ -1,3 +1,7 @@
+/**
+ * Copyright (C) 2020-2021 SIL International. All rights reserved.
+ */
+
package com.tavultesoft.kmapro;
import android.content.Context;
@@ -12,6 +16,7 @@ import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
+import com.tavultesoft.kmea.DisplayLanguages;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.data.Dataset;
@@ -19,7 +24,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = "SettingsFragment";
private static Context context;
- private Preference languagesPreference, installKeyboardOrDictionary;
+ private Preference languagesPreference, installKeyboardOrDictionary, displayLanguagePreference;
private CheckBoxPreference setSystemKeyboardPreference;
private CheckBoxPreference setDefaultKeyboardPreference;
@@ -46,6 +51,13 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
Intent installIntent = new Intent(context, KeymanSettingsInstallActivity.class);
installKeyboardOrDictionary.setIntent(installIntent);
+ displayLanguagePreference = new Preference(context);
+ displayLanguagePreference.setKey(DisplayLanguages.displayLanguageKey);
+ displayLanguagePreference.setTitle(getString(R.string.change_display_language));
+ displayLanguagePreference.setWidgetLayoutResource(R.layout.preference_translate_icon_layout);
+ Intent displayLanguageIntent = new Intent(context, KeymanSettingsLocalizeActivity.class);
+ displayLanguagePreference.setIntent(displayLanguageIntent);
+
/*
Automatically does the following:
SharedPreferences.Editor editor = prefs.edit();
@@ -106,6 +118,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
screen.addPreference(languagesPreference);
screen.addPreference(installKeyboardOrDictionary);
+ screen.addPreference(displayLanguagePreference);
screen.addPreference(setSystemKeyboardPreference);
screen.addPreference(setDefaultKeyboardPreference);
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsLocalizeActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsLocalizeActivity.java
new file mode 100644
index 0000000000..fae8c6f463
--- /dev/null
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsLocalizeActivity.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (C) 2021 SIL International. All rights reserved.
+ */
+
+package com.tavultesoft.kmapro;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.preference.PreferenceManager;
+
+import com.tavultesoft.kmea.DisplayLanguages;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class KeymanSettingsLocalizeActivity extends AppCompatActivity {
+ private static final String TAG = "SettingsLocalizeActivity";
+ private static ArrayList> localizeOptionList = null;
+ private static Typeface titleFont = null;
+ private static final String titleKey = "title";
+ private static final String subtitleKey = "subtitle";
+ private static final String iconKey = "icon";
+ private static Context context;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
+ context = this;
+
+ setContentView(R.layout.activity_list_layout);
+ final Toolbar toolbar = findViewById(R.id.list_toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+
+ final ListView listView = findViewById(com.tavultesoft.kmea.R.id.listView);
+
+ Bundle bundle = getIntent().getExtras();
+
+ final TextView textView = findViewById(R.id.bar_title);
+ textView.setText(getString(R.string.change_display_language));
+ if (titleFont != null) {
+ textView.setTypeface(titleFont, Typeface.BOLD);
+ }
+
+ localizeOptionList = new ArrayList<>();
+
+ final String noIcon = "0";
+ for(DisplayLanguages.DisplayLanguageType l: DisplayLanguages.DisplayLanguages) {
+ HashMap hashMap = new HashMap<>();
+ hashMap.put(titleKey, l.getLanguageName());
+ hashMap.put(iconKey, noIcon);
+ localizeOptionList.add(hashMap);
+ }
+
+ String[] from = new String[]{titleKey, subtitleKey, iconKey};
+ int[] to = new int[]{com.tavultesoft.kmea.R.id.text1, com.tavultesoft.kmea.R.id.image1};
+
+ ListAdapter adapter = new SimpleAdapter(context, localizeOptionList, com.tavultesoft.kmea.R.layout.list_row_layout1, from, to) {
+ };
+ listView.setAdapter(adapter);
+ listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ // Store the BCP-47 language tag in shared preference
+ if (position >= 0 && position < DisplayLanguages.DisplayLanguages.length) {
+
+ DisplayLanguages.DisplayLanguageType l = DisplayLanguages.DisplayLanguages[position];
+ String languageTag = l.getLanguageTag();
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(DisplayLanguages.displayLanguageKey, languageTag);
+ editor.commit();
+
+ // Restart the app to use selected locale
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ super.onBackPressed();
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
index 2585dcd520..ddaf2ea3fa 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018-2020 SIL International. All rights reserved.
+ * Copyright (C) 2018-2021 SIL International. All rights reserved.
*/
package com.tavultesoft.kmapro;
@@ -17,6 +17,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMKeyboardDownloaderActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.KMManager.KeyboardType;
@@ -92,7 +93,7 @@ import android.widget.Toast;
import io.sentry.android.core.SentryAndroid;
-public class MainActivity extends AppCompatActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener,
+public class MainActivity extends BaseActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener,
ActivityCompat.OnRequestPermissionsResultCallback {
public static Context context;
diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java
index a2be8fd173..229b24fdb4 100644
--- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java
+++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebBrowserActivity.java
@@ -10,6 +10,7 @@ import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.util.KMPLink;
@@ -48,7 +49,7 @@ import androidx.appcompat.app.AppCompatActivity;
import static android.app.Application.getProcessName;
-public class WebBrowserActivity extends AppCompatActivity {
+public class WebBrowserActivity extends BaseActivity {
private static final String TAG = "WebBrowserActivity";
private WebView webView;
private EditText addressField;
diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_translate.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_translate.png
new file mode 100644
index 0000000000..20cf9715e0
Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/ic_translate.png differ
diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/ic_translate.png b/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/ic_translate.png
new file mode 100644
index 0000000000..20cf9715e0
Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-mdpi/ic_translate.png differ
diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/ic_translate.png b/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/ic_translate.png
new file mode 100644
index 0000000000..3f804126c2
Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xhdpi/ic_translate.png differ
diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/ic_translate.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/ic_translate.png
new file mode 100644
index 0000000000..71b2332aaa
Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xxhdpi/ic_translate.png differ
diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/ic_translate.png b/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/ic_translate.png
new file mode 100644
index 0000000000..0065c18426
Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-xxxhdpi/ic_translate.png differ
diff --git a/android/KMAPro/kMAPro/src/main/res/layout/preference_translate_icon_layout.xml b/android/KMAPro/kMAPro/src/main/res/layout/preference_translate_icon_layout.xml
new file mode 100644
index 0000000000..d66b445010
--- /dev/null
+++ b/android/KMAPro/kMAPro/src/main/res/layout/preference_translate_icon_layout.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/android/KMAPro/kMAPro/src/main/res/values/strings.xml b/android/KMAPro/kMAPro/src/main/res/values/strings.xml
index 8784919a06..1285c9f2e7 100644
--- a/android/KMAPro/kMAPro/src/main/res/values/strings.xml
+++ b/android/KMAPro/kMAPro/src/main/res/values/strings.xml
@@ -95,6 +95,9 @@
Install Keyboard or Dictionary
+
+ Change display language
+
Always show banner
diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle
index 20c23e8aa0..d5b7348f28 100644
--- a/android/KMEA/app/build.gradle
+++ b/android/KMEA/app/build.gradle
@@ -66,6 +66,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
implementation 'commons-io:commons-io:2.6'
implementation 'io.sentry:sentry-android:3.1.0'
+ implementation 'androidx.preference:preference:1.1.1'
// Robolectric
testImplementation 'androidx.test:core:1.2.0'
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java
new file mode 100644
index 0000000000..fdd8ed2de1
--- /dev/null
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) 2021 SIL International. All rights reserved.
+ */
+
+package com.tavultesoft.kmea;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.LocaleList;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceManager;
+
+import com.tavultesoft.kmea.util.ContextUtils;
+
+import java.util.Locale;
+
+public class BaseActivity extends AppCompatActivity {
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ // Override the app locale using the BCP 47 tag from shared preferences
+ // Using PreferenceManager because this is before onCreate()
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(newBase);
+ String languageTag = prefs.getString(DisplayLanguages.displayLanguageKey, DisplayLanguages.unspecifiedLocale);
+ Locale localeToSwitchTo;
+ if (languageTag == null || languageTag.equals(DisplayLanguages.unspecifiedLocale)) {
+ // If display language not specified, use the default locale
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ super.attachBaseContext(newBase);
+ return;
+ }
+ localeToSwitchTo = LocaleList.getDefault().get(0);
+ } else {
+ localeToSwitchTo = Locale.forLanguageTag(languageTag);
+ }
+ ContextWrapper localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitchTo);
+ super.attachBaseContext(localeUpdatedContext);
+ }
+
+}
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/DisplayLanguages.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/DisplayLanguages.java
new file mode 100644
index 0000000000..7ffc3e511d
--- /dev/null
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/DisplayLanguages.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 2021 SIL International. All rights reserved.
+ */
+
+package com.tavultesoft.kmea;
+
+public class DisplayLanguages {
+ private static final String TAG = "DisplayLanguages";
+ public static final String displayLanguageKey = "DisplayLanguage";
+ public static final String unspecifiedLocale = ""; // empty string for app to use device's default locale
+
+ public static class DisplayLanguageType {
+ String languageTag;
+ String languageName;
+
+ public DisplayLanguageType(String languageTag, String languageName) {
+ this.languageTag = languageTag;
+ this.languageName = languageName;
+ }
+
+ public String getLanguageTag() {
+ return this.languageTag;
+ }
+
+ public String getLanguageName() {
+ return this.languageName;
+ }
+ }
+
+ // Display Language types (as named in translate.keyman.com)
+ // Order doesn't matter since we're storing BCP-47 tags in the settings preference
+ public static final DisplayLanguageType[] DisplayLanguages =
+ {
+ new DisplayLanguageType(unspecifiedLocale, "Default Locale"),
+ new DisplayLanguageType("de-DE", "German"),
+ new DisplayLanguageType("km-KH", "Khmer"),
+ new DisplayLanguageType("ann", "Obolo")
+ };
+
+}
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java
index cd56cf6bc7..c55138ad35 100644
--- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java
@@ -22,7 +22,7 @@ import java.util.List;
import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DOWNLOAD_KEYBOARD;
import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DOWNLOAD_MODEL;
-public class KMKeyboardDownloaderActivity extends AppCompatActivity {
+public class KMKeyboardDownloaderActivity extends BaseActivity {
// Bundle Keys
// Cloud
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java
index 63f95182ab..4c2a316e96 100644
--- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java
@@ -36,7 +36,7 @@ import com.tavultesoft.kmea.util.MapCompat;
import com.tavultesoft.kmea.util.QRCodeUtil;
// Public access is necessary to avoid IllegalAccessException
-public final class KeyboardInfoActivity extends AppCompatActivity {
+public final class KeyboardInfoActivity extends BaseActivity {
private static final String TAG = "KeyboardInfoActivity";
private static Toolbar toolbar = null;
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java
index 5da07ec094..960e29a08f 100644
--- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java
@@ -45,7 +45,7 @@ import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
-public final class KeyboardPickerActivity extends AppCompatActivity {
+public final class KeyboardPickerActivity extends BaseActivity {
//TODO: view instances should not be static
private static Toolbar toolbar = null;
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java
index 7213a2cd68..80f45ed553 100644
--- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java
@@ -38,7 +38,7 @@ import com.tavultesoft.kmea.util.MapCompat;
import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DELETE_MODEL;
// Public access is necessary to avoid IllegalAccessException
-public final class ModelInfoActivity extends AppCompatActivity {
+public final class ModelInfoActivity extends BaseActivity {
private static ArrayList> infoList = null;
private final String titleKey = "title";
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java
index 741a1eb8d3..a4d03ef3ad 100644
--- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java
@@ -40,7 +40,7 @@ import java.util.Map;
* Gets the list of installable lexical models from Keyman cloud and allows user to download a model.
* Displays a list of available models for a language ID.
*/
-public final class ModelPickerActivity extends AppCompatActivity {
+public final class ModelPickerActivity extends BaseActivity {
private Context context;
private static Toolbar toolbar = null;
diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/ContextUtils.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/ContextUtils.java
new file mode 100644
index 0000000000..c432dfa63b
--- /dev/null
+++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/ContextUtils.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2021 SIL International. All rights reserved.
+ */
+package com.tavultesoft.kmea.util;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.LocaleList;
+
+import java.util.Locale;
+
+/**
+ * Class to programmatically change the application locale (#4236)
+ * References
+ * Tutorial from https://lokalise.com/blog/android-app-localization/#Change_Application_Locale_Programmatically
+ * https://github.com/nirm2009/lokalise/tree/master/android-i18n
+ * https://stackoverflow.com/questions/39705739/android-n-change-language-programmatically
+ */
+public class ContextUtils extends ContextWrapper {
+ public ContextUtils(Context base) {
+ super(base);
+ }
+
+ public static ContextWrapper updateLocale(Context context, Locale localeToSwitchTo) {
+ Resources resources = context.getResources();
+ Configuration configuration = resources.getConfiguration();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ LocaleList localeList = new LocaleList(localeToSwitchTo);
+ LocaleList.setDefault(localeList);
+ configuration.setLocales(localeList);
+ } else {
+ configuration.locale = localeToSwitchTo;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ context = context.createConfigurationContext(configuration);
+ } else {
+ resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+ }
+
+ return new ContextUtils(context);
+ }
+
+}
diff --git a/resources/build/l10n/README.md b/resources/build/l10n/README.md
index 0a89c334a5..318e6815e3 100644
--- a/resources/build/l10n/README.md
+++ b/resources/build/l10n/README.md
@@ -1,6 +1,6 @@
# Localization Maintenance
-Localization for Keyman is maintained at https://crowdin.com/project/keyman
+Localization for Keyman is maintained at https://translate.keyman.com
Downloading and updating files between Keyman and Crowdin happens automatically
on GitHub by way of the Crowdin git integration. The configuration file for all platforms
@@ -69,6 +69,9 @@ To download latest translations for the specific language:
crowdin download -b master -l {language_code}
```
+Note: the Crowdin API doesn't handle custom languages so those will need to be manually synced.
+See https://support.crowdin.com/api/language-codes/
+
To display a list of latest translations from Crowdin:
```bash
@@ -83,6 +86,12 @@ To upload source files to Crowdin:
crowdin upload sources
```
+### Updating DisplayLanguages.java in Keyman Engine for Android
+In Keyman for Android, the settings menu for changing display languages is maintained in
+android/KMEA/app/src/main/java/com/tavultesoft/kmea/DisplayLangugages.java
+
+For the BCP-47 language tags to use in that file, don't include script names since the Android locales only handle language ID and region.
+
## Tip
Microsoft offers a website that allows to check how terms used in Windows are translated