diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 839eab3ef..2a5d6b97b 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -284,3 +284,34 @@ android {
warning 'ExtraTranslation'
}
}
+
+task verifyTranslations {
+ doLast {
+ def file = "briar-android/src/main/res/values/arrays.xml"
+ def arrays = new XmlParser().parse(file)
+ def lc = arrays.children().find { it.@name == "pref_language_values" }
+ def translations = []
+ lc.children().each { value -> translations.add(value.text()) }
+
+ def folders = ["default", "en-US"]
+ new File("briar-android/src/main/res").eachDir { dir ->
+ if (dir.name.startsWith("values-")) {
+ folders.add(dir.name.substring(7).replace("-r", "-"))
+ }
+ }
+ folders.each { n ->
+ if (!translations.remove(n)) {
+ throw new GradleException("Translation " + n + " is missing in $file")
+ }
+ }
+ if (translations.size() != 0)
+ throw new GradleException("Translations\n" + translations.join("\n")
+ + "\nhave no matching value folder")
+ }
+}
+
+project.afterEvaluate {
+ preBuild.dependsOn.add(verifyTranslations)
+}
+
+
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java
index 97e19062a..7b7e35846 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java
@@ -2,6 +2,7 @@ package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
+import android.content.res.Configuration;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.StrictMode.VmPolicy;
@@ -75,7 +76,10 @@ public class BriarApplicationImpl extends Application
@Override
protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
+ // Loading the language needs to be done here.
+ Localizer.initialize(base);
+ super.attachBaseContext(
+ Localizer.getInstance().setLocale(base));
ACRA.init(this);
}
@@ -108,6 +112,12 @@ public class BriarApplicationImpl extends Application
AndroidEagerSingletons.initEagerSingletons(applicationComponent);
}
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Localizer.getInstance().setLocale(this);
+ }
+
private void enableStrictMode() {
ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder();
threadPolicy.detectAll();
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
index f3cc72c5b..b6aaad4b9 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java
@@ -168,6 +168,11 @@ public class BriarService extends Service {
registerReceiver(receiver, filter);
}
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(Localizer.getInstance().setLocale(base));
+ }
+
private void showStartupFailureNotification(StartResult result) {
androidExecutor.runOnUiThread(() -> {
NotificationCompat.Builder b = new NotificationCompat.Builder(
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java
new file mode 100644
index 000000000..c734f13f0
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java
@@ -0,0 +1,85 @@
+package org.briarproject.briar.android;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.preference.PreferenceManager;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.util.Locale;
+
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
+
+@NotNullByDefault
+public class Localizer {
+
+ private static Localizer INSTANCE;
+ @Nullable
+ private final Locale locale;
+ private final SharedPreferences sharedPreferences;
+
+ private Localizer(Context context) {
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ locale = getLocaleFromTag(
+ sharedPreferences.getString(LANGUAGE, "default"));
+ }
+
+ public static synchronized void initialize(Context context) {
+ if (INSTANCE == null)
+ INSTANCE = new Localizer(context);
+ }
+
+ public static synchronized Localizer getInstance() {
+ if (INSTANCE == null)
+ throw new IllegalStateException("Localizer not initialized");
+ return INSTANCE;
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ return sharedPreferences;
+ }
+
+ // Get Locale from BCP-47 tag
+ @Nullable
+ public static Locale getLocaleFromTag(String tag) {
+ if (tag.equals("default"))
+ return null;
+ if (SDK_INT >= 21) {
+ return Locale.forLanguageTag(tag);
+ }
+ if (tag.contains("-")) {
+ String[] langArray = tag.split("-");
+ return new Locale(langArray[0], langArray[1]);
+ } else
+ return new Locale(tag);
+ }
+
+ public Context setLocale(Context context) {
+ if (locale == null)
+ return context;
+ Resources res = context.getResources();
+ Configuration conf = res.getConfiguration();
+ Locale currentLocale;
+ if (SDK_INT >= 24) {
+ currentLocale = conf.getLocales().get(0);
+ } else
+ currentLocale = conf.locale;
+ if (locale.equals(currentLocale))
+ return context;
+ Locale.setDefault(locale);
+ if (SDK_INT >= 17) {
+ conf.setLocale(locale);
+ context.createConfigurationContext(conf);
+ } else
+ conf.locale = locale;
+ //noinspection deprecation
+ res.updateConfiguration(conf, res.getDisplayMetrics());
+ return context;
+ }
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
index 7c8f5061a..c693ccbfc 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java
@@ -1,5 +1,6 @@
package org.briarproject.briar.android.activity;
+import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes;
@@ -18,6 +19,7 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.AndroidComponent;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.DestroyableContext;
+import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.fragment.BaseFragment;
@@ -84,6 +86,12 @@ public abstract class BaseActivity extends AppCompatActivity
}
}
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(
+ Localizer.getInstance().setLocale(base));
+ }
+
public ActivityComponent getActivityComponent() {
return activityComponent;
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java
index af09db700..e7800f1db 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java
@@ -68,6 +68,7 @@ public class NavDrawerActivity extends BriarActivity implements
public static final String INTENT_GROUPS = "intent_groups";
public static final String INTENT_FORUMS = "intent_forums";
public static final String INTENT_BLOGS = "intent_blogs";
+ public static final String INTENT_SIGN_OUT = "intent_sign_out";
private static final Logger LOG =
Logger.getLogger(NavDrawerActivity.class.getName());
@@ -99,6 +100,8 @@ public class NavDrawerActivity extends BriarActivity implements
R.id.nav_btn_contacts);
} else if (intent.getBooleanExtra(INTENT_BLOGS, false)) {
startFragment(FeedFragment.newInstance(), R.id.nav_btn_blogs);
+ } else if (intent.getBooleanExtra(INTENT_SIGN_OUT, false)) {
+ signOut(false);
}
setIntent(null);
}
@@ -225,12 +228,12 @@ public class NavDrawerActivity extends BriarActivity implements
finish();
} else if (fm.getBackStackEntryCount() == 0
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
- /*
- * This makes sure that the first fragment (ContactListFragment) the
- * user sees is the same as the last fragment the user sees before
- * exiting. This models the typical Google navigation behaviour such
- * as in Gmail/Inbox.
- */
+ /*
+ * This makes sure that the first fragment (ContactListFragment) the
+ * user sees is the same as the last fragment the user sees before
+ * exiting. This models the typical Google navigation behaviour such
+ * as in Gmail/Inbox.
+ */
startFragment(ContactListFragment.newInstance(),
R.id.nav_btn_contacts);
} else {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java
index 37870a983..48b4b7fa9 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java
@@ -36,5 +36,4 @@ public class SettingsActivity extends BriarActivity {
}
return false;
}
-
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
index c69dd65de..f75ba97e3 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java
@@ -1,8 +1,10 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
+import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -30,8 +32,11 @@ import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.Localizer;
+import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.util.UserFeedback;
+import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -58,6 +63,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
+import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.INTENT_SIGN_OUT;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
@@ -80,11 +86,13 @@ public class SettingsFragment extends PreferenceFragmentCompat
public static final String SETTINGS_NAMESPACE = "android-ui";
public static final String BT_NAMESPACE = BluetoothConstants.ID.getString();
public static final String TOR_NAMESPACE = TorConstants.ID.getString();
+ public static final String LANGUAGE = "pref_key_language";
private static final Logger LOG =
Logger.getLogger(SettingsFragment.class.getName());
private SettingsActivity listener;
+ private ListPreference language;
private ListPreference enableBluetooth;
private ListPreference torNetwork;
private CheckBoxPreference notifyPrivateMessages;
@@ -119,6 +127,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings);
+ language = (ListPreference) findPreference(LANGUAGE);
+ setLanguageEntries();
enableBluetooth = (ListPreference) findPreference("pref_key_bluetooth");
torNetwork = (ListPreference) findPreference("pref_key_tor_network");
notifyPrivateMessages = (CheckBoxPreference) findPreference(
@@ -137,6 +147,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
setSettingsEnabled(false);
+ language.setOnPreferenceChangeListener(this);
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
if (SDK_INT >= 21) {
@@ -180,6 +191,33 @@ public class SettingsFragment extends PreferenceFragmentCompat
eventBus.removeListener(this);
}
+ private void setLanguageEntries() {
+ CharSequence[] tags = language.getEntryValues();
+ CharSequence[] nativeNames = new CharSequence[tags.length];
+ for (int i = 0; i < tags.length; i++) {
+ String tag = tags[i].toString();
+ if (tag.equals("default")) {
+ nativeNames[i] = getString(R.string.pref_language_default);
+ continue;
+ }
+ Locale locale = Localizer.getLocaleFromTag(tag);
+ if (locale == null)
+ throw new IllegalStateException();
+ String nativeName = locale.getDisplayLanguage(locale);
+ // Fallback to English if the name is unknown in both native and
+ // current locale.
+ if (nativeName.equals(tag)) {
+ String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
+ if (!tmp.isEmpty() && !tmp.equals(nativeName))
+ nativeName = tmp;
+ }
+ nativeNames[i] =
+ nativeName.substring(0, 1).toUpperCase() +
+ nativeName.substring(1);
+ }
+ language.setEntries(nativeNames);
+ }
+
private void loadSettings() {
listener.runOnDbThread(() -> {
try {
@@ -312,41 +350,69 @@ public class SettingsFragment extends PreferenceFragmentCompat
}
@Override
- public boolean onPreferenceChange(Preference preference, Object o) {
- if (preference == enableBluetooth) {
- boolean btSetting = Boolean.valueOf((String) o);
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference == language) {
+ if (!language.getValue().equals(newValue))
+ languageChanged((String) newValue);
+ return false;
+ } else if (preference == enableBluetooth) {
+ boolean btSetting = Boolean.valueOf((String) newValue);
storeBluetoothSettings(btSetting);
} else if (preference == torNetwork) {
- int torSetting = Integer.valueOf((String) o);
+ int torSetting = Integer.valueOf((String) newValue);
storeTorSettings(torSetting);
} else if (preference == notifyPrivateMessages) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyGroupMessages) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyForumPosts) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyBlogPosts) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyVibration) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyLockscreen) {
Settings s = new Settings();
- s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) o);
+ s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) newValue);
storeSettings(s);
}
return true;
}
+ private void languageChanged(String newValue) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.pref_language_title);
+ builder.setMessage(R.string.pref_language_changed);
+ builder.setPositiveButton(R.string.sign_out_button,
+ (dialogInterface, i) -> {
+ language.setValue(newValue);
+ SharedPreferences prefs =
+ Localizer.getInstance().getSharedPreferences();
+ prefs.edit().putString(LANGUAGE, newValue)
+ .commit();
+ Intent intent = new Intent(getContext(),
+ NavDrawerActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(INTENT_SIGN_OUT, true);
+ getActivity().startActivity(intent);
+ getActivity().finish();
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setCancelable(false);
+ builder.show();
+ }
+
private void storeTorSettings(int torSetting) {
listener.runOnDbThread(() -> {
try {
diff --git a/briar-android/src/main/res/values/arrays.xml b/briar-android/src/main/res/values/arrays.xml
index 3eea71ecd..6ce3f2425 100644
--- a/briar-android/src/main/res/values/arrays.xml
+++ b/briar-android/src/main/res/values/arrays.xml
@@ -18,4 +18,36 @@
- 1
- 2
-
\ No newline at end of file
+
+ - default
+ - en-US
+ - ast
+ - bg
+ - br
+ - ca
+ - cs
+ - de
+ - es
+ - eu
+ - fa
+ - fi
+ - fr
+ - gl
+ - he
+ - hi
+ - it
+ - ja
+ - ms
+ - nb
+ - nl
+ - oc
+ - pt-BR
+ - ro
+ - ru
+ - sq
+ - sr
+ - sv
+ - tr
+ - zh-CN
+
+
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 1306feae1..a0087bedd 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -421,4 +421,10 @@
Camera permission was not granted
QR code
Show QR code fullscreen
+
+
+ Language
+ This setting will take effect when you restart Briar. Please sign out and restart Briar.
+ System default
+ Display
diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml
index 452815014..7b3181fa0 100644
--- a/briar-android/src/main/res/xml/settings.xml
+++ b/briar-android/src/main/res/xml/settings.xml
@@ -1,6 +1,17 @@
+
+
+
+