Allow "System language" to be set as language option

Populate "System language" as a first class item in the language
preference list so that users can select it in scenarios where they
have previously selected a specific language, and wish to go back to
the default.
This commit is contained in:
Rick Brown
2023-10-11 15:31:39 -04:00
parent c16eeff130
commit 087e3f5931
2 changed files with 163 additions and 47 deletions

View File

@@ -78,8 +78,8 @@ namespace keepass2android
{ {
if (!languageLoaded) if (!languageLoaded)
{ {
var language = PreferenceManager.GetDefaultSharedPreferences(c).GetString(c.GetString(Resource.String.app_language_pref_key), null); var langPref = PreferenceManager.GetDefaultSharedPreferences(c).GetString(c.GetString(Resource.String.app_language_pref_key), null);
Language = language; Language = LanguageEntry.PrefCodeToLanguage(langPref);
} }
return Language; return Language;
} }
@@ -109,8 +109,68 @@ namespace keepass2android
} }
} }
/// <summary>
/// A small container/wrapper that helps with the building and sorting of the
/// Language Preference list, and interoperability between it and LocaleManager
/// </summary>
class LanguageEntry
{
// "System language" preference code; maps to LocaleManager.Language = null
private const string SYS_LANG_CODE = "SysLang";
public class Util { // Preference code (2-char lowercase, or SYS_LANG_CODE)
public readonly string Code;
// Localized display name
public readonly string Name;
// True if this LanguageEntry is the "System language", false otherwise
public readonly bool IsSystem;
/// <summary>
/// Creates a LanguageEntry from a Locale
/// </summary>
/// <param name="from"></param>
/// <returns></returns>
public static LanguageEntry OfLocale(Java.Util.Locale from)
{
return new LanguageEntry(from.Language, from.DisplayLanguage, false);
}
/// <summary>
/// Creates the "System language" LanguageEntry with the given localized display name,
/// special preference code, marked as a System entry.
/// </summary>
/// <param name="displayName"></param>
/// <returns></returns>
public static LanguageEntry SystemDefault(string displayName)
{
return new LanguageEntry(SYS_LANG_CODE, displayName, true);
}
private LanguageEntry(string code, string name, bool isSystem)
{
this.Code = code;
this.Name = name;
this.IsSystem = isSystem;
}
/// <summary>
/// Converts a language preference code to a code that is compatible with LocaleManager.Language
/// </summary>
/// <param name="code"></param>
/// <returns>A converted code, possibly null</returns>
public static string PrefCodeToLanguage(string code)
{
return string.IsNullOrEmpty(code) || SYS_LANG_CODE.Equals(code) ? null : code;
}
public override string ToString()
{
return "{" + this.Code + ":" + this.Name + "}";
}
}
public class Util {
public const String KeyFilename = "fileName"; public const String KeyFilename = "fileName";

View File

@@ -41,10 +41,12 @@ using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Interfaces; using KeePassLib.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
namespace keepass2android namespace keepass2android
{ {
//http://stackoverflow.com/a/27422401/292233 //http://stackoverflow.com/a/27422401/292233
public class SettingsFragment : PreferenceFragment #pragma warning disable CS0618 // Type or member is obsolete
public class SettingsFragment : PreferenceFragment
{ {
@@ -177,36 +179,8 @@ namespace keepass2android
FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug; FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug;
HashSet<string> supportedLocales = new HashSet<string>() { "en", "af", "ar", "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "eu", "fa", "fi", "fr", "gl", "he", "hr", "hu", "id", "in", "it", "iw", "ja", "ko", "ml", "nb", "nl", "nn", "no", "pl", "pt", "ro", "ru", "si", "sk", "sl", "sr", "sv", "tr", "uk", "vi", "zh" }; HashSet<string> supportedLocales = new HashSet<string>() { "en", "af", "ar", "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "eu", "fa", "fi", "fr", "gl", "he", "hr", "hu", "id", "in", "it", "iw", "ja", "ko", "ml", "nb", "nl", "nn", "no", "pl", "pt", "ro", "ru", "si", "sk", "sl", "sr", "sv", "tr", "uk", "vi", "zh" };
var languagePref = (ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key));
ListPreference appLanguagePref = (ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key)); new AppLanguageManager(this, languagePref, supportedLocales);
var localesByCode = new System.Collections.Generic.Dictionary<string, List<Java.Util.Locale>>();
foreach (var loc in Java.Util.Locale.GetAvailableLocales())
{
if (!supportedLocales.Contains(loc.Language))
continue;
if (!localesByCode.ContainsKey(loc.Language))
{
localesByCode[loc.Language] = new List<Java.Util.Locale>();
}
localesByCode[loc.Language].Add(loc);
}
var localesByCodeUnique = localesByCode.Select(l => new KeyValuePair<string, Java.Util.Locale>(l.Key, l.Value.First())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
List<KeyValuePair<string, List<Java.Util.Locale>>> codesWithMultiple = localesByCode.Where(l => l.Value.Count > 1).ToList();
List<KeyValuePair<string, Java.Util.Locale>> localesByLanguageList = localesByCodeUnique
.OrderBy(kvp => kvp.Value.DisplayLanguage).ToList();
appLanguagePref.SetEntries(localesByLanguageList.Select(kvp => kvp.Value.DisplayLanguage).ToArray());
appLanguagePref.SetEntryValues(localesByLanguageList.Select(kvp => kvp.Value.Language).ToArray());
string languageCode = appLanguagePref.Value;
string summary = GetDisplayLanguage(localesByCodeUnique, languageCode);
((ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key))).Summary = summary;
appLanguagePref.PreferenceChange += (sender, args) =>
{
((ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key))).Summary = GetDisplayLanguage(localesByCodeUnique, (string)args.NewValue);
LocaleManager.Language = (string)args.NewValue;
};
UpdateAutofillPref(); UpdateAutofillPref();
@@ -347,16 +321,6 @@ namespace keepass2android
} }
private string GetDisplayLanguage(Dictionary<string, Java.Util.Locale> localesByCode, string languageCode)
{
return languageCode != null && localesByCode.ContainsKey(languageCode) ? localesByCode[languageCode]?.DisplayLanguage : GetString(Resource.String.SystemLanguage);
}
private void AppLanguagePref_PreferenceChange(object sender, Preference.PreferenceChangeEventArgs e)
{
throw new NotImplementedException();
}
private void UpdateAutofillPref() private void UpdateAutofillPref()
{ {
var autofillScreen = FindPreference(GetString(Resource.String.AutoFill_prefs_screen_key)); var autofillScreen = FindPreference(GetString(Resource.String.AutoFill_prefs_screen_key));
@@ -897,10 +861,102 @@ namespace keepass2android
} }
/// <summary> /// <summary>
/// Activity to configure the application and database settings. The database must be unlocked, and this activity will close if it becomes locked. /// <para>
/// </summary> /// A helper class that manages language preference display and selection.
/// </para>
/// <para>
/// The idea is to provide a ListPreference with a "System language" item at the top, followed by
/// the localized list of supported language names. The items are backed by their corresponding "code".
/// For a langauge that's the 2-char lowercase language code, which is exactly the same code that
/// LocaleManager.Language expects.
/// </para>
/// <para>
/// "System language" is a special case. LocaleManager.Language expects null, but ListPreference
/// does not support null as a valid code. To work around this, LanguageEntry.SYS_LANG_CODE
/// is used as the preference code. LanguageEntry.PrefCodeToLanguage(string) is used to convert the
/// preference codes to language codes as needed.
/// </para>
/// </summary>
internal class AppLanguageManager
{
private readonly PreferenceFragment _fragment;
private readonly ListPreference _langPref;
private readonly Dictionary<string, LanguageEntry> _langEntriesByCodeUnique;
public AppLanguageManager(PreferenceFragment fragment, ListPreference langPref, HashSet<string> supportedLocales)
{
this._fragment = fragment;
this._langPref = langPref;
this._langEntriesByCodeUnique = CreateCodeToEntryMapping(fragment, supportedLocales);
ConfigureLanguageList();
}
private static Dictionary<string, LanguageEntry> CreateCodeToEntryMapping(PreferenceFragment fragment, HashSet<string> supportedLocales)
{
var localesByCode = new Dictionary<string, List<Java.Util.Locale>>();
foreach (var loc in Java.Util.Locale.GetAvailableLocales())
{
if (!supportedLocales.Contains(loc.Language))
continue;
if (!localesByCode.ContainsKey(loc.Language))
{
localesByCode[loc.Language] = new List<Java.Util.Locale>();
}
localesByCode[loc.Language].Add(loc);
}
var langEntriesByCodeUnique = localesByCode
.Select(l => new KeyValuePair<string, LanguageEntry>(l.Key, LanguageEntry.OfLocale(l.Value.First())))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var sysLangEntry = LanguageEntry.SystemDefault(fragment.GetString(Resource.String.SystemLanguage));
langEntriesByCodeUnique.Add(sysLangEntry.Code, sysLangEntry);
return langEntriesByCodeUnique;
}
private void ConfigureLanguageList()
{
List<KeyValuePair<string, LanguageEntry>> langEntriesList = _langEntriesByCodeUnique
.OrderByDescending(kvp => kvp.Value.IsSystem)
.ThenBy(kvp => kvp.Value.Name)
.ToList();
_langPref.SetEntries(langEntriesList
.Select(kvp => kvp.Value.Name)
.ToArray());
_langPref.SetEntryValues(langEntriesList
.Select(kvp => kvp.Value.Code)
.ToArray());
_langPref.Summary = GetDisplayLanguage(LanguageEntry.PrefCodeToLanguage(_langPref.Value));
_langPref.PreferenceChange += AppLanguagePrefChange;
}
private string GetDisplayLanguage(string languageCode)
{
if (languageCode != null && this._langEntriesByCodeUnique.ContainsKey(languageCode))
return this._langEntriesByCodeUnique[languageCode]?.Name;
else
return _fragment.GetString(Resource.String.SystemLanguage);
}
private void AppLanguagePrefChange(object sender, Preference.PreferenceChangeEventArgs args)
{
string langCode = LanguageEntry.PrefCodeToLanguage((string)args.NewValue);
LocaleManager.Language = langCode;
_langPref.Summary = GetDisplayLanguage(langCode);
}
}
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Activity to configure the application and database settings. The database must be unlocked, and this activity will close if it becomes locked.
/// </summary>
[Activity(Label = "@string/app_name", Theme = "@style/MyTheme", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)] [Activity(Label = "@string/app_name", Theme = "@style/MyTheme", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)]
public class DatabaseSettingsActivity : LockCloseActivity public class DatabaseSettingsActivity : LockCloseActivity
{ {