diff --git a/lang/lang.go b/lang/lang.go
index e3ea0fe06b..f8ba28ba86 100644
--- a/lang/lang.go
+++ b/lang/lang.go
@@ -1,5 +1,10 @@
 // Package lang introduces a translation and localisation API for Fyne applications
 //
+// By default, a few custom localisation keys for a few default languages are included.
+// They can be overwritten with custom localisations.
+// Without further intervention, the default language will be English. This is overwritten
+// with the language of the first translation that is being registered explicitly.
+//
 // Since 2.5
 package lang
 
@@ -7,6 +12,7 @@ import (
 	"embed"
 	"encoding/json"
 	"log"
+	"path"
 	"strings"
 	"text/template"
 
@@ -35,12 +41,15 @@ var (
 	// More info available on the `LocalizePluralKey` function.
 	XN = LocalizePluralKey
 
-	bundle    *i18n.Bundle
-	localizer *i18n.Localizer
+	bundle          *i18n.Bundle
+	localizer       *i18n.Localizer
+	bundleIsDefault bool
+	unmarshallers   map[string]i18n.UnmarshalFunc
 
 	//go:embed translations
-	translations embed.FS
-	translated   []language.Tag
+	defaultTranslationFS embed.FS
+	defaultTranslations  map[language.Tag]*i18n.MessageFile
+	translated           []language.Tag
 )
 
 // Localize asks the translation engine to translate a string, this behaves like the gettext "_" function.
@@ -137,7 +146,7 @@ func AddTranslationsFS(fs embed.FS, dir string) (retErr error) {
 
 	for _, f := range files {
 		name := f.Name()
-		data, err := fs.ReadFile(dir + "/" + name)
+		data, err := fs.ReadFile(path.Join(dir, name))
 		if err != nil {
 			if retErr == nil {
 				retErr = err
@@ -159,25 +168,107 @@ func AddTranslationsFS(fs embed.FS, dir string) (retErr error) {
 	return retErr
 }
 
+// RegisteredLanguages returns the list of locales we have localization for.
+func RegisteredLanguages() []fyne.Locale {
+	var ret []fyne.Locale
+	for _, l := range translated {
+		ret = append(ret, localeFromTag(l))
+	}
+	return ret
+}
+
 func addLanguage(data []byte, name string) error {
-	f, err := bundle.ParseMessageFileBytes(data, name)
+	mf, err := i18n.ParseMessageFileBytes(data, name, unmarshallers)
 	if err != nil {
 		return err
 	}
 
-	translated = append(translated, f.Tag)
+	return addLanguageByMessages(mf.Messages, mf.Tag)
+}
+
+func addLanguageByMessages(msgs []*i18n.Message, tag language.Tag) error {
+	if bundleIsDefault {
+		// This is the first time, the user adds a language on their own. This implies, that
+		// they are aware of localizations and how to use them.
+		// To prevent half-translated applications, we "forget" our base translations and only
+		// use languages for resolution, that the user supplied.
+		// The bundle will still contain our base translations; if a language overlaps with those,
+		// our base strings will be there unless they explicitly get overwritten.
+		bundleIsDefault = false
+		if err := setupBundle(tag); err != nil {
+			return err
+		}
+		translated = []language.Tag{tag}
+	}
+
+	languageKnown := false
+	for _, t := range translated {
+		if t == tag {
+			languageKnown = true
+			break
+		}
+	}
+	if err := bundle.AddMessages(tag, msgs...); err != nil {
+		return err
+	}
+	if !languageKnown {
+		translated = append(translated, tag)
+	}
 	return nil
 }
 
 func init() {
-	bundle = i18n.NewBundle(language.English)
-	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+	unmarshallers = map[string]i18n.UnmarshalFunc{
+		"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 {
+	// Put English first as our preferred fallback language.
+	translated = []language.Tag{language.English}
+	if err := loadDefaultTranslations(); err != nil {
+		fyne.LogError("Error occurred loading built-in translations", err)
+	}
+	if err := setupBundle(language.English); err != nil {
 		fyne.LogError("Error occurred loading built-in translations", err)
 	}
+	bundleIsDefault = true
+}
+
+func loadDefaultTranslations() error {
+	defaultTranslations = map[language.Tag]*i18n.MessageFile{}
+	const dir = "translations"
+	files, err := defaultTranslationFS.ReadDir(dir)
+	if err != nil {
+		return err
+	}
+	for _, f := range files {
+		name := f.Name()
+		data, err := defaultTranslationFS.ReadFile(path.Join(dir, name))
+		if err != nil {
+			return err
+		}
+
+		mf, err := i18n.ParseMessageFileBytes(data, name, unmarshallers)
+		if err != nil {
+			return err
+		}
+		defaultTranslations[mf.Tag] = mf
+	}
+	return nil
+}
+
+func setupBundle(lng language.Tag) error {
+	bundle = i18n.NewBundle(lng)
+	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+
+	translated = []language.Tag{lng} // the first item in this list will be the fallback if none match
+	for tag, mf := range defaultTranslations {
+		if err := addLanguageByMessages(mf.Messages, tag); err != nil {
+			return err
+		}
+	}
+	updateLocalizer()
+
+	return nil
 }
 
 func fallbackWithData(key, fallback string, data any) string {
@@ -198,10 +289,13 @@ func setupLang(lang string) {
 
 // updateLocalizer Finds the closest translation from the user's locale list and sets it up
 func updateLocalizer() {
+	var languageStr string
 	all, err := locale.GetLocales()
-	if err != nil {
+	if err == nil {
+		languageStr = closestSupportedLocale(all).LanguageString()
+	} else {
 		fyne.LogError("Failed to load user locales", err)
-		all = []string{"en"}
+		languageStr = translated[0].String()
 	}
-	setupLang(closestSupportedLocale(all).LanguageString())
+	setupLang(languageStr)
 }
diff --git a/lang/lang_test.go b/lang/lang_test.go
index 7667c0bac2..057766bc8c 100644
--- a/lang/lang_test.go
+++ b/lang/lang_test.go
@@ -3,10 +3,10 @@ package lang
 import (
 	"testing"
 
+	"fyne.io/fyne/v2"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
-	"fyne.io/fyne/v2"
+	"golang.org/x/text/language"
 )
 
 func TestAddTranslations(t *testing.T) {
@@ -85,3 +85,56 @@ func TestLocalizePluralKey_Fallback(t *testing.T) {
 	assert.Equal(t, "Apple", XN("appleID", "Apple", 1))
 	assert.Equal(t, "Apples", XN("appleID", "Apple", 2))
 }
+
+func TestDefaultLocalizations(t *testing.T) {
+	// Not ideal, but other tests might manipulate the global state.
+	// Reset it manually.
+	require.NoError(t, setupBundle(language.English))
+	bundleIsDefault = true
+
+	t.Run("base localizations are loaded by default", func(t *testing.T) {
+		languages := RegisteredLanguages()
+
+		translationFiles, err := defaultTranslationFS.ReadDir("translations")
+		require.NoError(t, err)
+		assert.Len(t, languages, len(translationFiles))
+
+		// Two samples for sanity check.
+		assert.Contains(t, languages, fyne.Locale("en-US-Latn"))
+		assert.Contains(t, languages, fyne.Locale("de-DE-Latn"))
+	})
+
+	t.Run("registering custom localizations should wipe default localizations first", func(t *testing.T) {
+		err := AddTranslations(fyne.NewStaticResource("de.json", []byte(`{
+		  "Test": "Passt"
+		}`)))
+		require.NoError(t, err)
+
+		err = AddTranslations(fyne.NewStaticResource("en_GB.json", []byte(`{
+		  "Test": "Match"
+		}`)))
+		require.NoError(t, err)
+
+		languages := RegisteredLanguages()
+		assert.Equal(t, []fyne.Locale{"de-DE-Latn", "en-GB-Latn"}, languages)
+
+		setupLang("de")
+		assert.Equal(t, "Passt", L("Test"))
+
+		setupLang("en-GB")
+		assert.Equal(t, "Match", L("Test"))
+
+		t.Run("first registered language becomes default", func(t *testing.T) {
+			setupLang("it")
+			assert.Equal(t, "Passt", L("Test"))
+		})
+
+		t.Run("base translations are still present", func(t *testing.T) {
+			setupLang("de")
+			assert.Equal(t, "Beenden", L("Quit"))
+
+			setupLang("en")
+			assert.Equal(t, "Name", X("file.name", "nope"))
+		})
+	})
+}