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
 
 	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("application/config/version", "");
 	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");
 }
 
-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";
 	if (!ProjectSettings::get_singleton()->has_setting(prop)) {
 		return;
@@ -620,7 +623,7 @@ void TranslationServer::load_translations() {
 	for (const String &path : translations) {
 		Ref<Translation> tr = ResourceLoader::load(path);
 		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 load_translations();
+	void load_project_translations(Ref<TranslationDomain> p_domain);
 
 #ifdef TOOLS_ENABLED
 	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 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.
+			[b]Note:[/b] When left empty, the application name is translated using the project translations.
 		</member>
 		<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.

+ 1 - 2
editor/editor_node.cpp

@@ -578,8 +578,7 @@ void EditorNode::_gdextensions_reloaded() {
 void EditorNode::_update_translations() {
 	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()) {
 		// 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/plist.h"
-#include "core/string/translation.h"
+#include "core/string/translation_server.h"
 #include "editor/editor_node.h"
 #include "editor/editor_string_names.h"
 #include "editor/export/editor_export.h"
@@ -1915,42 +1915,51 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref<Editor
 		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 microphone_usage_descriptions = p_preset->get("privacy/microphone_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";
 			tmp_app_path->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			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("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_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";
 			tmp_app_path->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			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() + "\";");
 			}
+
 			if (camera_usage_descriptions.has(lang)) {
 				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() {
 	ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_add_builtin_strings_to_pot", template_add_builtin->is_pressed());
 	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) {
@@ -724,8 +719,6 @@ void LocalizationEditor::update_translations() {
 	// New translation parser plugin might extend possible file extensions in template generation.
 	_update_template_source_file_extensions();
 
-	template_generate_button->set_disabled(sources.is_empty() && !template_add_builtin->is_pressed());
-
 	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;
 	for (const Vector<String> &entry : raw) {
 		const String &msgid = entry[0];
@@ -95,7 +102,7 @@ void TranslationTemplateGenerator::generate(const String &p_file) {
 
 	const MessageMap &map = parse(files, add_builtin);
 	if (map.is_empty()) {
-		WARN_PRINT("No translatable strings found.");
+		WARN_PRINT_ED(TTR("No translatable strings found."));
 		return;
 	}
 

+ 2 - 2
main/main.cpp

@@ -772,7 +772,7 @@ Error Main::test_setup() {
 	if (!locale.is_empty()) {
 		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
 
 	// 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()) {
 			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
 
 		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/json.h"
 #include "core/io/marshalls.h"
+#include "core/string/translation_server.h"
 #include "core/version.h"
 #include "editor/editor_log.h"
 #include "editor/editor_node.h"
@@ -1770,8 +1771,11 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &
 
 	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++) {
 		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);
 
-		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 {
-				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);
 	}
 
+	TranslationServer::get_singleton()->remove_domain(domain_name);
+
 	//write a new string table, but use 16 bits
 	Vector<uint8_t> ret;
 	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 "core/config/project_settings.h"
+#include "core/string/translation_server.h"
 
 int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {
 	switch (screen_orientation) {
@@ -219,6 +219,12 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
 		}
 		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();
 	while (true) {
 		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_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));
 			print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
 			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();
+
+	TranslationServer::get_singleton()->remove_domain(domain_name);
+
 	return OK;
 }
 

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

@@ -35,7 +35,7 @@
 
 #include "core/io/image_loader.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 "editor/editor_node.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 camera_usage_descriptions = p_preset->get("privacy/camera_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 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";
 			tmp_app_dir->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			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()) {
 				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() + "\";");
 		}
 
-		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";
 			tmp_app_dir->make_dir_recursive(fname);
 			Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE);
 			f->store_line("/* Localized versions of Info.plist keys */");
 			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() + "\";");
 			}
+
 			if (microphone_usage_descriptions.has(lang)) {
 				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.
 	bool found_binary = false;