Browse Source

Allow localizing the application name with project translations

Haoyu Qiu 1 month ago
parent
commit
b8a8f8b35a

+ 1 - 1
core/config/project_settings.cpp

@@ -1633,7 +1633,7 @@ ProjectSettings::ProjectSettings() {
 #endif
 #endif
 
 
 	GLOBAL_DEF_BASIC("application/config/name", "");
 	GLOBAL_DEF_BASIC("application/config/name", "");
-	GLOBAL_DEF_BASIC(PropertyInfo(Variant::DICTIONARY, "application/config/name_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary());
+	GLOBAL_DEF(PropertyInfo(Variant::DICTIONARY, "application/config/name_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary());
 	GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/config/description", PROPERTY_HINT_MULTILINE_TEXT), "");
 	GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/config/description", PROPERTY_HINT_MULTILINE_TEXT), "");
 	GLOBAL_DEF_BASIC("application/config/version", "");
 	GLOBAL_DEF_BASIC("application/config/version", "");
 	GLOBAL_DEF_INTERNAL(PropertyInfo(Variant::STRING, "application/config/tags"), PackedStringArray());
 	GLOBAL_DEF_INTERNAL(PropertyInfo(Variant::STRING, "application/config/tags"), PackedStringArray());

+ 5 - 2
core/string/translation_server.cpp

@@ -611,7 +611,10 @@ void TranslationServer::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled");
 }
 }
 
 
-void TranslationServer::load_translations() {
+void TranslationServer::load_project_translations(Ref<TranslationDomain> p_domain) {
+	DEV_ASSERT(p_domain.is_valid());
+
+	p_domain->clear();
 	const String prop = "internationalization/locale/translations";
 	const String prop = "internationalization/locale/translations";
 	if (!ProjectSettings::get_singleton()->has_setting(prop)) {
 	if (!ProjectSettings::get_singleton()->has_setting(prop)) {
 		return;
 		return;
@@ -620,7 +623,7 @@ void TranslationServer::load_translations() {
 	for (const String &path : translations) {
 	for (const String &path : translations) {
 		Ref<Translation> tr = ResourceLoader::load(path);
 		Ref<Translation> tr = ResourceLoader::load(path);
 		if (tr.is_valid()) {
 		if (tr.is_valid()) {
-			add_translation(tr);
+			p_domain->add_translation(tr);
 		}
 		}
 	}
 	}
 }
 }

+ 1 - 1
core/string/translation_server.h

@@ -151,7 +151,7 @@ public:
 
 
 	void clear();
 	void clear();
 
 
-	void load_translations();
+	void load_project_translations(Ref<TranslationDomain> p_domain);
 
 
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
 	virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
 	virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;

+ 1 - 0
doc/classes/ProjectSettings.xml

@@ -338,6 +338,7 @@
 		</member>
 		</member>
 		<member name="application/config/name_localized" type="Dictionary" setter="" getter="" default="{}">
 		<member name="application/config/name_localized" type="Dictionary" setter="" getter="" default="{}">
 			Translations of the project's name. This setting is used by OS tools to translate application name on Android, iOS and macOS.
 			Translations of the project's name. This setting is used by OS tools to translate application name on Android, iOS and macOS.
+			[b]Note:[/b] When left empty, the application name is translated using the project translations.
 		</member>
 		</member>
 		<member name="application/config/project_settings_override" type="String" setter="" getter="" default="&quot;&quot;">
 		<member name="application/config/project_settings_override" type="String" setter="" getter="" default="&quot;&quot;">
 			Specifies a file to override project settings. For example: [code]user://custom_settings.cfg[/code]. See "Overriding" in the [ProjectSettings] class description at the top for more information.
 			Specifies a file to override project settings. For example: [code]user://custom_settings.cfg[/code]. See "Overriding" in the [ProjectSettings] class description at the top for more information.

+ 1 - 2
editor/editor_node.cpp

@@ -578,8 +578,7 @@ void EditorNode::_gdextensions_reloaded() {
 void EditorNode::_update_translations() {
 void EditorNode::_update_translations() {
 	Ref<TranslationDomain> main = TranslationServer::get_singleton()->get_main_domain();
 	Ref<TranslationDomain> main = TranslationServer::get_singleton()->get_main_domain();
 
 
-	main->clear();
-	TranslationServer::get_singleton()->load_translations();
+	TranslationServer::get_singleton()->load_project_translations(main);
 
 
 	if (main->is_enabled()) {
 	if (main->is_enabled()) {
 		// Check for the exact locale.
 		// Check for the exact locale.

+ 22 - 13
editor/export/editor_export_platform_apple_embedded.cpp

@@ -32,7 +32,7 @@
 
 
 #include "core/io/json.h"
 #include "core/io/json.h"
 #include "core/io/plist.h"
 #include "core/io/plist.h"
-#include "core/string/translation.h"
+#include "core/string/translation_server.h"
 #include "editor/editor_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_string_names.h"
 #include "editor/editor_string_names.h"
 #include "editor/export/editor_export.h"
 #include "editor/export/editor_export.h"
@@ -1915,42 +1915,51 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref<Editor
 		return ERR_FILE_NOT_FOUND;
 		return ERR_FILE_NOT_FOUND;
 	}
 	}
 
 
-	Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
 	Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
 	Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
 	Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
 	Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
 	Dictionary photolibrary_usage_descriptions = p_preset->get("privacy/photolibrary_usage_description_localized");
 	Dictionary photolibrary_usage_descriptions = p_preset->get("privacy/photolibrary_usage_description_localized");
 
 
-	Vector<String> translations = get_project_setting(p_preset, "internationalization/locale/translations");
-	if (translations.size() > 0) {
+	const String project_name = get_project_setting(p_preset, "application/config/name");
+	const Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
+	const StringName domain_name = "godot.project_name_localization";
+	Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain(domain_name);
+	TranslationServer::get_singleton()->load_project_translations(domain);
+	const Vector<String> locales = domain->get_loaded_locales();
+
+	if (!locales.is_empty()) {
 		{
 		{
 			String fname = binary_dir + "/en.lproj";
 			String fname = binary_dir + "/en.lproj";
 			tmp_app_path->make_dir_recursive(fname);
 			tmp_app_path->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("");
 			f->store_line("");
-			f->store_line("CFBundleDisplayName = \"" + get_project_setting(p_preset, "application/config/name").operator String() + "\";");
+			f->store_line("CFBundleDisplayName = \"" + project_name + "\";");
 			f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");
 			f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";");
 			f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
 			f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
 			f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";");
 			f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";");
 		}
 		}
 
 
-		HashSet<String> languages;
-		for (const String &E : translations) {
-			Ref<Translation> tr = ResourceLoader::load(E);
-			if (tr.is_valid() && tr->get_locale() != "en") {
-				languages.insert(tr->get_locale());
+		for (const String &lang : locales) {
+			if (lang == "en") {
+				continue;
 			}
 			}
-		}
 
 
-		for (const String &lang : languages) {
 			String fname = binary_dir + "/" + lang + ".lproj";
 			String fname = binary_dir + "/" + lang + ".lproj";
 			tmp_app_path->make_dir_recursive(fname);
 			tmp_app_path->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("");
 			f->store_line("");
-			if (appnames.has(lang)) {
+
+			if (appnames.is_empty()) {
+				domain->set_locale_override(lang);
+				const String &name = domain->translate(project_name, String());
+				if (name != project_name) {
+					f->store_line("CFBundleDisplayName = \"" + name + "\";");
+				}
+			} else if (appnames.has(lang)) {
 				f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
 				f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
 			}
 			}
+
 			if (camera_usage_descriptions.has(lang)) {
 			if (camera_usage_descriptions.has(lang)) {
 				f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");
 				f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";");
 			}
 			}

+ 0 - 7
editor/translations/localization_editor.cpp

@@ -405,11 +405,6 @@ void LocalizationEditor::_template_generate_open() {
 void LocalizationEditor::_template_add_builtin_toggled() {
 void LocalizationEditor::_template_add_builtin_toggled() {
 	ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_add_builtin_strings_to_pot", template_add_builtin->is_pressed());
 	ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_add_builtin_strings_to_pot", template_add_builtin->is_pressed());
 	ProjectSettings::get_singleton()->save();
 	ProjectSettings::get_singleton()->save();
-
-	const PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
-	if (sources.is_empty()) {
-		template_generate_button->set_disabled(!template_add_builtin->is_pressed());
-	}
 }
 }
 
 
 void LocalizationEditor::_template_generate(const String &p_file) {
 void LocalizationEditor::_template_generate(const String &p_file) {
@@ -724,8 +719,6 @@ void LocalizationEditor::update_translations() {
 	// New translation parser plugin might extend possible file extensions in template generation.
 	// New translation parser plugin might extend possible file extensions in template generation.
 	_update_template_source_file_extensions();
 	_update_template_source_file_extensions();
 
 
-	template_generate_button->set_disabled(sources.is_empty() && !template_add_builtin->is_pressed());
-
 	updating_translations = false;
 	updating_translations = false;
 }
 }
 
 

+ 8 - 1
editor/translations/template_generator.cpp

@@ -64,6 +64,13 @@ TranslationTemplateGenerator::MessageMap TranslationTemplateGenerator::parse(con
 		}
 		}
 	}
 	}
 
 
+	if (GLOBAL_GET("application/config/name_localized").operator Dictionary().is_empty()) {
+		const String &project_name = GLOBAL_GET("application/config/name");
+		if (!project_name.is_empty()) {
+			raw.push_back({ project_name, String(), String(), String(), String() });
+		}
+	}
+
 	MessageMap result;
 	MessageMap result;
 	for (const Vector<String> &entry : raw) {
 	for (const Vector<String> &entry : raw) {
 		const String &msgid = entry[0];
 		const String &msgid = entry[0];
@@ -95,7 +102,7 @@ void TranslationTemplateGenerator::generate(const String &p_file) {
 
 
 	const MessageMap &map = parse(files, add_builtin);
 	const MessageMap &map = parse(files, add_builtin);
 	if (map.is_empty()) {
 	if (map.is_empty()) {
-		WARN_PRINT("No translatable strings found.");
+		WARN_PRINT_ED(TTR("No translatable strings found."));
 		return;
 		return;
 	}
 	}
 
 

+ 2 - 2
main/main.cpp

@@ -772,7 +772,7 @@ Error Main::test_setup() {
 	if (!locale.is_empty()) {
 	if (!locale.is_empty()) {
 		translation_server->set_locale(locale);
 		translation_server->set_locale(locale);
 	}
 	}
-	translation_server->load_translations();
+	translation_server->load_project_translations(translation_server->get_main_domain());
 	ResourceLoader::load_translation_remaps(); //load remaps for resources
 	ResourceLoader::load_translation_remaps(); //load remaps for resources
 
 
 	// Initialize ThemeDB early so that scene types can register their theme items.
 	// Initialize ThemeDB early so that scene types can register their theme items.
@@ -3456,7 +3456,7 @@ Error Main::setup2(bool p_show_boot_logo) {
 		if (!locale.is_empty()) {
 		if (!locale.is_empty()) {
 			translation_server->set_locale(locale);
 			translation_server->set_locale(locale);
 		}
 		}
-		translation_server->load_translations();
+		translation_server->load_project_translations(translation_server->get_main_domain());
 		ResourceLoader::load_translation_remaps(); //load remaps for resources
 		ResourceLoader::load_translation_remaps(); //load remaps for resources
 
 
 		OS::get_singleton()->benchmark_end_measure("Startup", "Translations and Remaps");
 		OS::get_singleton()->benchmark_end_measure("Startup", "Translations and Remaps");

+ 16 - 12
platform/android/export/export_plugin.cpp

@@ -39,6 +39,7 @@
 #include "core/io/image_loader.h"
 #include "core/io/image_loader.h"
 #include "core/io/json.h"
 #include "core/io/json.h"
 #include "core/io/marshalls.h"
 #include "core/io/marshalls.h"
+#include "core/string/translation_server.h"
 #include "core/version.h"
 #include "core/version.h"
 #include "editor/editor_log.h"
 #include "editor/editor_log.h"
 #include "editor/editor_node.h"
 #include "editor/editor_node.h"
@@ -1770,8 +1771,11 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &
 
 
 	Vector<String> string_table;
 	Vector<String> string_table;
 
 
-	String package_name = p_preset->get("package/name");
-	Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
+	const String project_name = get_project_name(p_preset, p_preset->get("package/name"));
+	const Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
+	const StringName domain_name = "godot.project_name_localization";
+	Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain(domain_name);
+	TranslationServer::get_singleton()->load_project_translations(domain);
 
 
 	for (uint32_t i = 0; i < string_count; i++) {
 	for (uint32_t i = 0; i < string_count; i++) {
 		uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]);
 		uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]);
@@ -1779,24 +1783,24 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &
 
 
 		String str = _parse_string(&r_manifest[offset], string_flags & UTF8_FLAG);
 		String str = _parse_string(&r_manifest[offset], string_flags & UTF8_FLAG);
 
 
-		if (str.begins_with("godot-project-name")) {
-			if (str == "godot-project-name") {
-				//project name
-				str = get_project_name(p_preset, package_name);
+		if (str == "godot-project-name") {
+			str = project_name;
+		} else if (str.begins_with("godot-project-name")) {
+			String lang = str.substr(str.rfind_char('-') + 1).replace_char('-', '_');
 
 
+			if (appnames.is_empty()) {
+				domain->set_locale_override(lang);
+				str = domain->translate(project_name, String());
 			} else {
 			} else {
-				String lang = str.substr(str.rfind_char('-') + 1).replace_char('-', '_');
-				if (appnames.has(lang)) {
-					str = appnames[lang];
-				} else {
-					str = get_project_name(p_preset, package_name);
-				}
+				str = appnames.get(lang, project_name);
 			}
 			}
 		}
 		}
 
 
 		string_table.push_back(str);
 		string_table.push_back(str);
 	}
 	}
 
 
+	TranslationServer::get_singleton()->remove_domain(domain_name);
+
 	//write a new string table, but use 16 bits
 	//write a new string table, but use 16 bits
 	Vector<uint8_t> ret;
 	Vector<uint8_t> ret;
 	ret.resize(string_table_begins + string_table.size() * 4);
 	ret.resize(string_table_begins + string_table.size() * 4);

+ 19 - 3
platform/android/export/gradle_export_util.cpp

@@ -30,7 +30,7 @@
 
 
 #include "gradle_export_util.h"
 #include "gradle_export_util.h"
 
 
-#include "core/config/project_settings.h"
+#include "core/string/translation_server.h"
 
 
 int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {
 int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {
 	switch (screen_orientation) {
 	switch (screen_orientation) {
@@ -219,6 +219,12 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
 		}
 		}
 		return ERR_CANT_OPEN;
 		return ERR_CANT_OPEN;
 	}
 	}
+
+	// Setup a temporary translation domain to translate the project name.
+	const StringName domain_name = "godot.project_name_localization";
+	Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain(domain_name);
+	TranslationServer::get_singleton()->load_project_translations(domain);
+
 	da->list_dir_begin();
 	da->list_dir_begin();
 	while (true) {
 	while (true) {
 		String file = da->get_next();
 		String file = da->get_next();
@@ -231,8 +237,15 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
 		}
 		}
 		String locale = file.replace("values-", "").replace("-r", "_");
 		String locale = file.replace("values-", "").replace("-r", "_");
 		String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
 		String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
-		if (p_appnames.has(locale)) {
-			String locale_project_name = p_appnames[locale];
+
+		String locale_project_name;
+		if (p_appnames.is_empty()) {
+			domain->set_locale_override(locale);
+			locale_project_name = domain->translate(p_project_name, String());
+		} else {
+			locale_project_name = p_appnames.get(locale, p_project_name);
+		}
+		if (locale_project_name != p_project_name) {
 			String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name));
 			String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name));
 			print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
 			print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
 			store_string_at_path(locale_directory, processed_xml_string);
 			store_string_at_path(locale_directory, processed_xml_string);
@@ -242,6 +255,9 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
 		}
 		}
 	}
 	}
 	da->list_dir_end();
 	da->list_dir_end();
+
+	TranslationServer::get_singleton()->remove_domain(domain_name);
+
 	return OK;
 	return OK;
 }
 }
 
 

+ 24 - 13
platform/macos/export/export_plugin.cpp

@@ -35,7 +35,7 @@
 
 
 #include "core/io/image_loader.h"
 #include "core/io/image_loader.h"
 #include "core/io/plist.h"
 #include "core/io/plist.h"
-#include "core/string/translation.h"
+#include "core/string/translation_server.h"
 #include "drivers/png/png_driver_common.h"
 #include "drivers/png/png_driver_common.h"
 #include "editor/editor_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_string_names.h"
 #include "editor/editor_string_names.h"
@@ -1757,7 +1757,6 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 		}
 		}
 	}
 	}
 
 
-	Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
 	Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
 	Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
 	Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
 	Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
 	Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized");
 	Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized");
@@ -1771,15 +1770,21 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 	Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized");
 	Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized");
 	Dictionary copyrights = p_preset->get("application/copyright_localized");
 	Dictionary copyrights = p_preset->get("application/copyright_localized");
 
 
-	Vector<String> translations = get_project_setting(p_preset, "internationalization/locale/translations");
-	if (translations.size() > 0) {
+	const String project_name = get_project_setting(p_preset, "application/config/name");
+	const Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
+	const StringName domain_name = "godot.project_name_localization";
+	Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain(domain_name);
+	TranslationServer::get_singleton()->load_project_translations(domain);
+	const Vector<String> locales = domain->get_loaded_locales();
+
+	if (!locales.is_empty()) {
 		{
 		{
 			String fname = tmp_app_path_name + "/Contents/Resources/en.lproj";
 			String fname = tmp_app_path_name + "/Contents/Resources/en.lproj";
 			tmp_app_dir->make_dir_recursive(fname);
 			tmp_app_dir->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("");
 			f->store_line("");
-			f->store_line("CFBundleDisplayName = \"" + get_project_setting(p_preset, "application/config/name").operator String() + "\";");
+			f->store_line("CFBundleDisplayName = \"" + project_name + "\";");
 			if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
 			if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
 				f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
 				f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";");
 			}
 			}
@@ -1816,23 +1821,27 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 			f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";");
 			f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";");
 		}
 		}
 
 
-		HashSet<String> languages;
-		for (const String &E : translations) {
-			Ref<Translation> tr = ResourceLoader::load(E);
-			if (tr.is_valid() && tr->get_locale() != "en") {
-				languages.insert(tr->get_locale());
+		for (const String &lang : locales) {
+			if (lang == "en") {
+				continue;
 			}
 			}
-		}
 
 
-		for (const String &lang : languages) {
 			String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj";
 			String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj";
 			tmp_app_dir->make_dir_recursive(fname);
 			tmp_app_dir->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("/* Localized versions of Info.plist keys */");
 			f->store_line("");
 			f->store_line("");
-			if (appnames.has(lang)) {
+
+			if (appnames.is_empty()) {
+				domain->set_locale_override(lang);
+				const String &name = domain->translate(project_name, String());
+				if (name != project_name) {
+					f->store_line("CFBundleDisplayName = \"" + name + "\";");
+				}
+			} else if (appnames.has(lang)) {
 				f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
 				f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";");
 			}
 			}
+
 			if (microphone_usage_descriptions.has(lang)) {
 			if (microphone_usage_descriptions.has(lang)) {
 				f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");
 				f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";");
 			}
 			}
@@ -1872,6 +1881,8 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
 		}
 		}
 	}
 	}
 
 
+	TranslationServer::get_singleton()->remove_domain(domain_name);
+
 	// Now process our template.
 	// Now process our template.
 	bool found_binary = false;
 	bool found_binary = false;