diff --git a/lang/lang.go b/lang/lang.go index 6a2e58bfc7..3a8e971027 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -7,6 +7,7 @@ import ( "embed" "encoding/json" "log" + "sort" "strings" "text/template" @@ -35,6 +36,12 @@ var ( // More info available on the `LocalizePluralKey` function. XN = LocalizePluralKey + // This defines an order in which it will try to find a fallback in case localizer does not find a match. + // All other languages will be in alphabetical order. + languageOrder = []string{"en"} + + preferredLanguage string + bundle *i18n.Bundle localizer *i18n.Localizer @@ -159,6 +166,21 @@ func AddTranslationsFS(fs embed.FS, dir string) (retErr error) { return retErr } +// SetLanguageOrder allows an app to set the order in which translations are checked in case no locale matches. +// Since 2.6 +func SetLanguageOrder(order []string) { + languageOrder = order + updateLocalizer() +} + +// SetPreferredLocale allows an app to set the preferred locale for translations, overwriting the System Locale. +// locale can be in format en_US_someVariant, en_US, en-US-someVariant, en-US, en +// Since 2.6 +func SetPreferredLocale(locale string) { + preferredLanguage = locale + updateLocalizer() +} + func addLanguage(data []byte, name string) error { f, err := bundle.ParseMessageFileBytes(data, name) if err != nil { @@ -173,7 +195,6 @@ func init() { bundle = i18n.NewBundle(language.English) bundle.RegisterUnmarshalFunc("json", json.Unmarshal) - translated = []language.Tag{language.Make("en")} // the first item in this list will be the fallback if none match err := AddTranslationsFS(translations, "translations") if err != nil { fyne.LogError("Error occurred loading built-in translations", err) @@ -191,6 +212,32 @@ func fallbackWithData(key, fallback string, data any) string { return str.String() } +func orderLanguages(a, b language.Tag) bool { + indexA := -1 + indexB := -1 + for i, l := range languageOrder { + if a.String() == l { + indexA = i + } + if b.String() == l { + indexB = i + } + } + // Order both languages as defined in languageOrder + if indexA != -1 && indexB != -1 { + return indexA < indexB + } + // If it is the only language in languageOrder, it comes first + if indexA != -1 { + return true + } + if indexB != -1 { + return false + } + // If no language is in languageOrder, sort alphabetically + return strings.Compare(a.String(), b.String()) < 0 +} + // A utility for setting up languages - available to unit tests for overriding system func setupLang(lang string) { localizer = i18n.NewLocalizer(bundle, lang) @@ -198,11 +245,19 @@ func setupLang(lang string) { // updateLocalizer Finds the closest translation from the user's locale list and sets it up func updateLocalizer() { + // Sort the translated slice using the orderLanguages function + sort.SliceStable(translated, func(i, j int) bool { + return orderLanguages(translated[i], translated[j]) + }) + all, err := locale.GetLocales() if err != nil { fyne.LogError("Failed to load user locales", err) all = []string{"en"} } + if preferredLanguage != "" { + all = []string{preferredLanguage} + } str := closestSupportedLocale(all).LanguageString() setupLang(str) localizer = i18n.NewLocalizer(bundle, str) diff --git a/lang/lang_test.go b/lang/lang_test.go index ce76623ec8..5e4f4fbcae 100644 --- a/lang/lang_test.go +++ b/lang/lang_test.go @@ -23,11 +23,6 @@ func TestAddTranslations(t *testing.T) { assert.Equal(t, "Match2", L("Test2")) } -func TestLocalize_Default(t *testing.T) { - fallback := closestSupportedLocale([]string{"xx"}) - assert.Equal(t, fyne.Locale("en"), fallback[0:2]) -} - func TestLocalize_Fallback(t *testing.T) { assert.Equal(t, "Missing", L("Missing")) } @@ -81,3 +76,31 @@ func TestLocalizePluralKey_Fallback(t *testing.T) { assert.Equal(t, "Apple", XN("appleID", "Apple", 1)) assert.Equal(t, "Apples", XN("appleID", "Apple", 2)) } + +func TestSetPreferredLocale(t *testing.T) { + _ = AddTranslations(fyne.NewStaticResource("en.json", []byte(`{ + "Test": "Match" +}`))) + _ = AddTranslations(fyne.NewStaticResource("fr.json", []byte(`{ + "Test2": "Match2" +}`))) + setupLang("fr") + assert.Equal(t, "Match2", L("Test2")) + SetPreferredLocale("en") + assert.Equal(t, "Match", L("Test")) +} + +func TestSetLanguageOrder(t *testing.T) { + _ = AddTranslations(fyne.NewStaticResource("en.json", []byte(`{ + "Test": "Match" +}`))) + _ = AddTranslations(fyne.NewStaticResource("fr.json", []byte(`{ + "Test2": "Match2" +}`))) + setupLang("en") + SetPreferredLocale("xyz") // invalid language to test fallback + SetLanguageOrder([]string{"fr", "en"}) + assert.Equal(t, "Match2", L("Test2")) + SetLanguageOrder([]string{"en", "fr"}) + assert.Equal(t, "Match", L("Test")) +} diff --git a/widget/entry_password.go b/widget/entry_password.go index ff8f75d4c3..45f989271e 100644 --- a/widget/entry_password.go +++ b/widget/entry_password.go @@ -83,5 +83,5 @@ func (r *passwordRevealerRenderer) Refresh() { if r.entry.disabled.Load() { r.icon.Resource = theme.NewDisabledResource(r.icon.Resource) } - canvas.Refresh(r.icon) + r.icon.Refresh() }