Parcourir la source

Merge pull request #52969 from bruvzg/locale_detection

Rémi Verschelde il y a 3 ans
Parent
commit
ce2b5bdfa8

+ 1 - 0
core/core_constants.cpp

@@ -584,6 +584,7 @@ void register_global_constants() {
 	BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_OBJECTID);
 	BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_POINTER);
 	BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);
+	BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALE_ID);
 	BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_MAX);
 
 	BIND_CORE_ENUM_CONSTANT(PROPERTY_USAGE_NONE);

+ 7 - 19
core/io/resource_loader.cpp

@@ -806,38 +806,26 @@ String ResourceLoader::_path_remap(const String &p_path, bool *r_translation_rem
 
 		// To find the path of the remapped resource, we extract the locale name after
 		// the last ':' to match the project locale.
-		// We also fall back in case of regional locales as done in TranslationServer::translate
-		// (e.g. 'ru_RU' -> 'ru' if the former has no specific mapping).
 
 		String locale = TranslationServer::get_singleton()->get_locale();
 		ERR_FAIL_COND_V_MSG(locale.length() < 2, p_path, "Could not remap path '" + p_path + "' for translation as configured locale '" + locale + "' is invalid.");
-		String lang = TranslationServer::get_language_code(locale);
 
 		Vector<String> &res_remaps = *translation_remaps.getptr(new_path);
-		bool near_match = false;
 
+		int best_score = 0;
 		for (int i = 0; i < res_remaps.size(); i++) {
 			int split = res_remaps[i].rfind(":");
 			if (split == -1) {
 				continue;
 			}
-
 			String l = res_remaps[i].substr(split + 1).strip_edges();
-			if (l == locale) { // Exact match.
-				new_path = res_remaps[i].left(split);
-				break;
-			} else if (near_match) {
-				continue; // Already found near match, keep going for potential exact match.
-			}
-
-			// No exact match (e.g. locale 'ru_RU' but remap is 'ru'), let's look further
-			// for a near match (same language code, i.e. first 2 or 3 letters before
-			// regional code, if included).
-			if (TranslationServer::get_language_code(l) == lang) {
-				// Language code matches, that's a near match. Keep looking for exact match.
-				near_match = true;
+			int score = TranslationServer::get_singleton()->compare_locales(locale, l);
+			if (score >= best_score) {
 				new_path = res_remaps[i].left(split);
-				continue;
+				best_score = score;
+				if (score == 10) {
+					break; // Exact match, skip the rest.
+				}
 			}
 		}
 

+ 1 - 0
core/object/object.h

@@ -94,6 +94,7 @@ enum PropertyHint {
 	PROPERTY_HINT_INT_IS_OBJECTID,
 	PROPERTY_HINT_ARRAY_TYPE,
 	PROPERTY_HINT_INT_IS_POINTER,
+	PROPERTY_HINT_LOCALE_ID,
 	PROPERTY_HINT_MAX,
 	// When updating PropertyHint, also sync the hardcoded list in VisualScriptEditorVariableEdit
 };

+ 1196 - 0
core/string/locales.h

@@ -0,0 +1,1196 @@
+/*************************************************************************/
+/*  locales.h                                                            */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef LOCALES_H
+#define LOCALES_H
+
+// Windows has some weird locale identifiers which do not honor the ISO 639-1
+// standardized nomenclature. Whenever those don't conflict with existing ISO
+// identifiers, we override them.
+//
+// Reference:
+// - https://msdn.microsoft.com/en-us/library/windows/desktop/ms693062(v=vs.85).aspx
+
+static const char *locale_renames[][2] = {
+	{ "in", "id" }, //  Indonesian
+	{ "iw", "he" }, //  Hebrew
+	{ "no", "nb" }, //  Norwegian Bokmål
+	{ nullptr, nullptr }
+};
+
+// Additional script information to preferred scripts.
+// Language code, script code, default country, supported countries.
+// Reference:
+// - https://lh.2xlibre.net/locales/
+// - https://www.localeplanet.com/icu/index.html
+// - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
+
+static const char *locale_scripts[][4] = {
+	{ "az", "Latn", "", "AZ" },
+	{ "az", "Arab", "", "IR" },
+	{ "bs", "Latn", "", "BA" },
+	{ "ff", "Latn", "", "BF,CM,GH,GM,GN,GW,LR,MR,NE,NG,SL,SN" },
+	{ "pa", "Arab", "PK", "PK" },
+	{ "pa", "Guru", "IN", "IN" },
+	{ "sd", "Arab", "PK", "PK" },
+	{ "sd", "Deva", "IN", "IN" },
+	{ "shi", "Tfng", "", "MA" },
+	{ "sr", "Cyrl", "", "BA,RS,XK" },
+	{ "sr", "Latn", "", "ME" },
+	{ "uz", "Latn", "", "UZ" },
+	{ "uz", "Arab", "AF", "AF" },
+	{ "vai", "Vaii", "", "LR" },
+	{ "yue", "Hans", "CN", "CN" },
+	{ "yue", "Hant", "HK", "HK" },
+	{ "zh", "Hans", "CN", "CN,SG" },
+	{ "zh", "Hant", "TW", "HK,MO,TW" },
+	{ nullptr, nullptr, nullptr, nullptr }
+};
+
+// Additional mapping for outdated, temporary or exceptionally reserved country codes.
+// Reference:
+// - https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
+// - https://www.iso.org/obp/ui/#search/code/
+
+static const char *country_renames[][2] = {
+	{ "BU", "MM" }, // Burma, name changed to Myanmar.
+	{ "KV", "XK" }, // Kosovo (temporary FIPS code to European Commission code), no official ISO code assigned.
+	{ "TP", "TL" }, // East Timor, name changed to Timor-Leste.
+	{ "UK", "GB" }, // United Kingdom, exceptionally reserved code.
+	{ nullptr, nullptr }
+};
+
+// Country code, country name.
+// Reference:
+// - https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
+// - https://www.iso.org/obp/ui/#search/code/
+
+static const char *country_names[][2] = {
+	{ "AC", "Ascension Island" }, // Exceptionally reserved.
+	{ "AD", "Andorra" },
+	{ "AE", "United Arab Emirates" },
+	{ "AF", "Afghanistan" },
+	{ "AG", "Antigua and Barbuda" },
+	{ "AI", "Anguilla" },
+	{ "AL", "Albania" },
+	{ "AM", "Armenia" },
+	{ "AN", "Netherlands Antilles" }, // Transitionally reserved, divided into BQ, CW and SX.
+	{ "AO", "Angola" },
+	{ "AQ", "Antarctica" },
+	{ "AR", "Argentina" },
+	{ "AS", "American Samoa" },
+	{ "AT", "Austria" },
+	{ "AU", "Australia" },
+	{ "AW", "Aruba" },
+	{ "AX", "Åland Islands" },
+	{ "AZ", "Azerbaijan" },
+	{ "BA", "Bosnia and Herzegovina" },
+	{ "BB", "Barbados" },
+	{ "BD", "Bangladesh" },
+	{ "BE", "Belgium" },
+	{ "BF", "Burkina Faso" },
+	{ "BG", "Bulgaria" },
+	{ "BH", "Bahrain" },
+	{ "BI", "Burundi" },
+	{ "BJ", "Benin" },
+	{ "BL", "St. Barthélemy" },
+	{ "BM", "Bermuda" },
+	{ "BN", "Brunei" },
+	{ "BO", "Bolivia" },
+	{ "BQ", "Caribbean Netherlands" },
+	{ "BR", "Brazil" },
+	{ "BS", "Bahamas" },
+	{ "BT", "Bhutan" },
+	{ "BV", "Bouvet Island" },
+	{ "BW", "Botswana" },
+	{ "BY", "Belarus" },
+	{ "BZ", "Belize" },
+	{ "CA", "Canada" },
+	{ "CC", "Cocos (Keeling) Islands" },
+	{ "CD", "Congo - Kinshasa" },
+	{ "CF", "Central African Republic" },
+	{ "CG", "Congo - Brazzaville" },
+	{ "CH", "Switzerland" },
+	{ "CI", "Côte d'Ivoire" },
+	{ "CK", "Cook Islands" },
+	{ "CL", "Chile" },
+	{ "CM", "Cameroon" },
+	{ "CN", "China" },
+	{ "CO", "Colombia" },
+	{ "CP", "Clipperton Island" }, // Exceptionally reserved.
+	{ "CR", "Costa Rica" },
+	{ "CQ", "Island of Sark" }, // Exceptionally reserved.
+	{ "CU", "Cuba" },
+	{ "CV", "Cabo Verde" },
+	{ "CW", "Curaçao" },
+	{ "CX", "Christmas Island" },
+	{ "CY", "Cyprus" },
+	{ "CZ", "Czechia" },
+	{ "DE", "Germany" },
+	{ "DG", "Diego Garcia" }, // Exceptionally reserved.
+	{ "DJ", "Djibouti" },
+	{ "DK", "Denmark" },
+	{ "DM", "Dominica" },
+	{ "DO", "Dominican Republic" },
+	{ "DZ", "Algeria" },
+	{ "EA", "Ceuta and Melilla" }, // Exceptionally reserved.
+	{ "EC", "Ecuador" },
+	{ "EE", "Estonia" },
+	{ "EG", "Egypt" },
+	{ "EH", "Western Sahara" },
+	{ "ER", "Eritrea" },
+	{ "ES", "Spain" },
+	{ "ET", "Ethiopia" },
+	{ "EU", "European Union" }, // Exceptionally reserved.
+	{ "EZ", "Eurozone" }, // Exceptionally reserved.
+	{ "FI", "Finland" },
+	{ "FJ", "Fiji" },
+	{ "FK", "Falkland Islands" },
+	{ "FM", "Micronesia" },
+	{ "FO", "Faroe Islands" },
+	{ "FR", "France" },
+	{ "FX", "France, Metropolitan" }, // Exceptionally reserved.
+	{ "GA", "Gabon" },
+	{ "GB", "United Kingdom" },
+	{ "GD", "Grenada" },
+	{ "GE", "Georgia" },
+	{ "GF", "French Guiana" },
+	{ "GG", "Guernsey" },
+	{ "GH", "Ghana" },
+	{ "GI", "Gibraltar" },
+	{ "GL", "Greenland" },
+	{ "GM", "Gambia" },
+	{ "GN", "Guinea" },
+	{ "GP", "Guadeloupe" },
+	{ "GQ", "Equatorial Guinea" },
+	{ "GR", "Greece" },
+	{ "GS", "South Georgia and South Sandwich Islands" },
+	{ "GT", "Guatemala" },
+	{ "GU", "Guam" },
+	{ "GW", "Guinea-Bissau" },
+	{ "GY", "Guyana" },
+	{ "HK", "Hong Kong" },
+	{ "HM", "Heard Island and McDonald Islands" },
+	{ "HN", "Honduras" },
+	{ "HR", "Croatia" },
+	{ "HT", "Haiti" },
+	{ "HU", "Hungary" },
+	{ "IC", "Canary Islands" }, // Exceptionally reserved.
+	{ "ID", "Indonesia" },
+	{ "IE", "Ireland" },
+	{ "IL", "Israel" },
+	{ "IM", "Isle of Man" },
+	{ "IN", "India" },
+	{ "IO", "British Indian Ocean Territory" },
+	{ "IQ", "Iraq" },
+	{ "IR", "Iran" },
+	{ "IS", "Iceland" },
+	{ "IT", "Italy" },
+	{ "JE", "Jersey" },
+	{ "JM", "Jamaica" },
+	{ "JO", "Jordan" },
+	{ "JP", "Japan" },
+	{ "KE", "Kenya" },
+	{ "KG", "Kyrgyzstan" },
+	{ "KH", "Cambodia" },
+	{ "KI", "Kiribati" },
+	{ "KM", "Comoros" },
+	{ "KN", "St. Kitts and Nevis" },
+	{ "KP", "North Korea" },
+	{ "KR", "South Korea" },
+	{ "KW", "Kuwait" },
+	{ "KY", "Cayman Islands" },
+	{ "KZ", "Kazakhstan" },
+	{ "LA", "Laos" },
+	{ "LB", "Lebanon" },
+	{ "LC", "St. Lucia" },
+	{ "LI", "Liechtenstein" },
+	{ "LK", "Sri Lanka" },
+	{ "LR", "Liberia" },
+	{ "LS", "Lesotho" },
+	{ "LT", "Lithuania" },
+	{ "LU", "Luxembourg" },
+	{ "LV", "Latvia" },
+	{ "LY", "Libya" },
+	{ "MA", "Morocco" },
+	{ "MC", "Monaco" },
+	{ "MD", "Moldova" },
+	{ "ME", "Montenegro" },
+	{ "MF", "St. Martin" },
+	{ "MG", "Madagascar" },
+	{ "MH", "Marshall Islands" },
+	{ "MK", "North Macedonia" },
+	{ "ML", "Mali" },
+	{ "MM", "Myanmar" },
+	{ "MN", "Mongolia" },
+	{ "MO", "Macao" },
+	{ "MP", "Northern Mariana Islands" },
+	{ "MQ", "Martinique" },
+	{ "MR", "Mauritania" },
+	{ "MS", "Montserrat" },
+	{ "MT", "Malta" },
+	{ "MU", "Mauritius" },
+	{ "MV", "Maldives" },
+	{ "MW", "Malawi" },
+	{ "MX", "Mexico" },
+	{ "MY", "Malaysia" },
+	{ "MZ", "Mozambique" },
+	{ "NA", "Namibia" },
+	{ "NC", "New Caledonia" },
+	{ "NE", "Niger" },
+	{ "NF", "Norfolk Island" },
+	{ "NG", "Nigeria" },
+	{ "NI", "Nicaragua" },
+	{ "NL", "Netherlands" },
+	{ "NO", "Norway" },
+	{ "NP", "Nepal" },
+	{ "NR", "Nauru" },
+	{ "NU", "Niue" },
+	{ "NZ", "New Zealand" },
+	{ "OM", "Oman" },
+	{ "PA", "Panama" },
+	{ "PE", "Peru" },
+	{ "PF", "French Polynesia" },
+	{ "PG", "Papua New Guinea" },
+	{ "PH", "Philippines" },
+	{ "PK", "Pakistan" },
+	{ "PL", "Poland" },
+	{ "PM", "St. Pierre and Miquelon" },
+	{ "PN", "Pitcairn Islands" },
+	{ "PR", "Puerto Rico" },
+	{ "PS", "Palestine" },
+	{ "PT", "Portugal" },
+	{ "PW", "Palau" },
+	{ "PY", "Paraguay" },
+	{ "QA", "Qatar" },
+	{ "RE", "Réunion" },
+	{ "RO", "Romania" },
+	{ "RS", "Serbia" },
+	{ "RU", "Russia" },
+	{ "RW", "Rwanda" },
+	{ "SA", "Saudi Arabia" },
+	{ "SB", "Solomon Islands" },
+	{ "SC", "Seychelles" },
+	{ "SD", "Sudan" },
+	{ "SE", "Sweden" },
+	{ "SG", "Singapore" },
+	{ "SH", "St. Helena, Ascension and Tristan da Cunha" },
+	{ "SI", "Slovenia" },
+	{ "SJ", "Svalbard and Jan Mayen" },
+	{ "SK", "Slovakia" },
+	{ "SL", "Sierra Leone" },
+	{ "SM", "San Marino" },
+	{ "SN", "Senegal" },
+	{ "SO", "Somalia" },
+	{ "SR", "Suriname" },
+	{ "SS", "South Sudan" },
+	{ "ST", "Sao Tome and Principe" },
+	{ "SV", "El Salvador" },
+	{ "SX", "Sint Maarten" },
+	{ "SY", "Syria" },
+	{ "SZ", "Eswatini" },
+	{ "TA", "Tristan da Cunha" }, // Exceptionally reserved.
+	{ "TC", "Turks and Caicos Islands" },
+	{ "TD", "Chad" },
+	{ "TF", "French Southern Territories" },
+	{ "TG", "Togo" },
+	{ "TH", "Thailand" },
+	{ "TJ", "Tajikistan" },
+	{ "TK", "Tokelau" },
+	{ "TL", "Timor-Leste" },
+	{ "TM", "Turkmenistan" },
+	{ "TN", "Tunisia" },
+	{ "TO", "Tonga" },
+	{ "TR", "Turkey" },
+	{ "TT", "Trinidad and Tobago" },
+	{ "TV", "Tuvalu" },
+	{ "TW", "Taiwan" },
+	{ "TZ", "Tanzania" },
+	{ "UA", "Ukraine" },
+	{ "UG", "Uganda" },
+	{ "UM", "U.S. Outlying Islands" },
+	{ "US", "United States of America" },
+	{ "UY", "Uruguay" },
+	{ "UZ", "Uzbekistan" },
+	{ "VA", "Holy See" },
+	{ "VC", "St. Vincent and the Grenadines" },
+	{ "VE", "Venezuela" },
+	{ "VG", "British Virgin Islands" },
+	{ "VI", "U.S. Virgin Islands" },
+	{ "VN", "Viet Nam" },
+	{ "VU", "Vanuatu" },
+	{ "WF", "Wallis and Futuna" },
+	{ "WS", "Samoa" },
+	{ "XK", "Kosovo" }, // Temporary code, no official ISO code assigned.
+	{ "YE", "Yemen" },
+	{ "YT", "Mayotte" },
+	{ "ZA", "South Africa" },
+	{ "ZM", "Zambia" },
+	{ "ZW", "Zimbabwe" },
+	{ nullptr, nullptr }
+};
+
+// Languages code, language name.
+// Reference:
+// - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+// - https://www.localeplanet.com/icu/index.html
+// - https://lh.2xlibre.net/locales/
+
+static const char *language_list[][2] = {
+	{ "aa", "Afar" },
+	{ "ab", "Abkhazian" },
+	{ "ace", "Achinese" },
+	{ "ach", "Acoli" },
+	{ "ada", "Adangme" },
+	{ "ady", "Adyghe" },
+	{ "ae", "Avestan" },
+	{ "aeb", "Tunisian Arabic" },
+	{ "af", "Afrikaans" },
+	{ "afh", "Afrihili" },
+	{ "agq", "Aghem" },
+	{ "ain", "Ainu" },
+	{ "agr", "Aguaruna" },
+	{ "ak", "Akan" },
+	{ "akk", "Akkadian" },
+	{ "akz", "Alabama" },
+	{ "ale", "Aleut" },
+	{ "aln", "Gheg Albanian" },
+	{ "alt", "Southern Altai" },
+	{ "am", "Amharic" },
+	{ "an", "Aragonese" },
+	{ "ang", "Old English" },
+	{ "anp", "Angika" },
+	{ "ar", "Arabic" },
+	{ "arc", "Aramaic" },
+	{ "arn", "Mapudungun" },
+	{ "aro", "Araona" },
+	{ "arp", "Arapaho" },
+	{ "arq", "Algerian Arabic" },
+	{ "ars", "Najdi Arabic" },
+	{ "arw", "Arawak" },
+	{ "ary", "Moroccan Arabic" },
+	{ "arz", "Egyptian Arabic" },
+	{ "as", "Assamese" },
+	{ "asa", "Asu" },
+	{ "ase", "American Sign Language" },
+	{ "ast", "Asturian" },
+	{ "av", "Avaric" },
+	{ "avk", "Kotava" },
+	{ "awa", "Awadhi" },
+	{ "ayc", "Southern Aymara" },
+	{ "ay", "Aymara" },
+	{ "az", "Azerbaijani" },
+	{ "ba", "Bashkir" },
+	{ "bal", "Baluchi" },
+	{ "ban", "Balinese" },
+	{ "bar", "Bavarian" },
+	{ "bas", "Bassa" },
+	{ "bax", "Bamun" },
+	{ "bbc", "Batak Toba" },
+	{ "bbj", "Ghomala" },
+	{ "be", "Belarusian" },
+	{ "bej", "Beja" },
+	{ "bem", "Bemba" },
+	{ "ber", "Berber" },
+	{ "bew", "Betawi" },
+	{ "bez", "Bena" },
+	{ "bfd", "Bafut" },
+	{ "bfq", "Badaga" },
+	{ "bg", "Bulgarian" },
+	{ "bhb", "Bhili" },
+	{ "bgn", "Western Balochi" },
+	{ "bho", "Bhojpuri" },
+	{ "bi", "Bislama" },
+	{ "bik", "Bikol" },
+	{ "bin", "Bini" },
+	{ "bjn", "Banjar" },
+	{ "bkm", "Kom" },
+	{ "bla", "Siksika" },
+	{ "bm", "Bambara" },
+	{ "bn", "Bengali" },
+	{ "bo", "Tibetan" },
+	{ "bpy", "Bishnupriya" },
+	{ "bqi", "Bakhtiari" },
+	{ "br", "Breton" },
+	{ "brh", "Brahui" },
+	{ "brx", "Bodo" },
+	{ "bs", "Bosnian" },
+	{ "bss", "Akoose" },
+	{ "bua", "Buriat" },
+	{ "bug", "Buginese" },
+	{ "bum", "Bulu" },
+	{ "byn", "Bilin" },
+	{ "byv", "Medumba" },
+	{ "ca", "Catalan" },
+	{ "cad", "Caddo" },
+	{ "car", "Carib" },
+	{ "cay", "Cayuga" },
+	{ "cch", "Atsam" },
+	{ "ccp", "Chakma" },
+	{ "ce", "Chechen" },
+	{ "ceb", "Cebuano" },
+	{ "cgg", "Chiga" },
+	{ "ch", "Chamorro" },
+	{ "chb", "Chibcha" },
+	{ "chg", "Chagatai" },
+	{ "chk", "Chuukese" },
+	{ "chm", "Mari" },
+	{ "chn", "Chinook Jargon" },
+	{ "cho", "Choctaw" },
+	{ "chp", "Chipewyan" },
+	{ "chr", "Cherokee" },
+	{ "chy", "Cheyenne" },
+	{ "cic", "Chickasaw" },
+	{ "ckb", "Central Kurdish" },
+	{ "csb", "Kashubian" },
+	{ "cmn", "Mandarin Chinese" },
+	{ "co", "Corsican" },
+	{ "cop", "Coptic" },
+	{ "cps", "Capiznon" },
+	{ "cr", "Cree" },
+	{ "crh", "Crimean Tatar" },
+	{ "crs", "Seselwa Creole French" },
+	{ "cs", "Czech" },
+	{ "csb", "Kashubian" },
+	{ "cu", "Church Slavic" },
+	{ "cv", "Chuvash" },
+	{ "cy", "Welsh" },
+	{ "da", "Danish" },
+	{ "dak", "Dakota" },
+	{ "dar", "Dargwa" },
+	{ "dav", "Taita" },
+	{ "de", "German" },
+	{ "del", "Delaware" },
+	{ "den", "Slave" },
+	{ "dgr", "Dogrib" },
+	{ "din", "Dinka" },
+	{ "dje", "Zarma" },
+	{ "doi", "Dogri" },
+	{ "dsb", "Lower Sorbian" },
+	{ "dtp", "Central Dusun" },
+	{ "dua", "Duala" },
+	{ "dum", "Middle Dutch" },
+	{ "dv", "Dhivehi" },
+	{ "dyo", "Jola-Fonyi" },
+	{ "dyu", "Dyula" },
+	{ "dz", "Dzongkha" },
+	{ "dzg", "Dazaga" },
+	{ "ebu", "Embu" },
+	{ "ee", "Ewe" },
+	{ "efi", "Efik" },
+	{ "egl", "Emilian" },
+	{ "egy", "Ancient Egyptian" },
+	{ "eka", "Ekajuk" },
+	{ "el", "Greek" },
+	{ "elx", "Elamite" },
+	{ "en", "English" },
+	{ "enm", "Middle English" },
+	{ "eo", "Esperanto" },
+	{ "es", "Spanish" },
+	{ "esu", "Central Yupik" },
+	{ "et", "Estonian" },
+	{ "eu", "Basque" },
+	{ "ewo", "Ewondo" },
+	{ "ext", "Extremaduran" },
+	{ "fa", "Persian" },
+	{ "fan", "Fang" },
+	{ "fat", "Fanti" },
+	{ "ff", "Fulah" },
+	{ "fi", "Finnish" },
+	{ "fil", "Filipino" },
+	{ "fit", "Tornedalen Finnish" },
+	{ "fj", "Fijian" },
+	{ "fo", "Faroese" },
+	{ "fon", "Fon" },
+	{ "fr", "French" },
+	{ "frc", "Cajun French" },
+	{ "frm", "Middle French" },
+	{ "fro", "Old French" },
+	{ "frp", "Arpitan" },
+	{ "frr", "Northern Frisian" },
+	{ "frs", "Eastern Frisian" },
+	{ "fur", "Friulian" },
+	{ "fy", "Western Frisian" },
+	{ "ga", "Irish" },
+	{ "gaa", "Ga" },
+	{ "gag", "Gagauz" },
+	{ "gan", "Gan Chinese" },
+	{ "gay", "Gayo" },
+	{ "gba", "Gbaya" },
+	{ "gbz", "Zoroastrian Dari" },
+	{ "gd", "Scottish Gaelic" },
+	{ "gez", "Geez" },
+	{ "gil", "Gilbertese" },
+	{ "gl", "Galician" },
+	{ "glk", "Gilaki" },
+	{ "gmh", "Middle High German" },
+	{ "gn", "Guarani" },
+	{ "goh", "Old High German" },
+	{ "gom", "Goan Konkani" },
+	{ "gon", "Gondi" },
+	{ "gor", "Gorontalo" },
+	{ "got", "Gothic" },
+	{ "grb", "Grebo" },
+	{ "grc", "Ancient Greek" },
+	{ "gsw", "Swiss German" },
+	{ "gu", "Gujarati" },
+	{ "guc", "Wayuu" },
+	{ "gur", "Frafra" },
+	{ "guz", "Gusii" },
+	{ "gv", "Manx" },
+	{ "gwi", "Gwichʼin" },
+	{ "ha", "Hausa" },
+	{ "hai", "Haida" },
+	{ "hak", "Hakka Chinese" },
+	{ "haw", "Hawaiian" },
+	{ "he", "Hebrew" },
+	{ "hi", "Hindi" },
+	{ "hif", "Fiji Hindi" },
+	{ "hil", "Hiligaynon" },
+	{ "hit", "Hittite" },
+	{ "hmn", "Hmong" },
+	{ "ho", "Hiri Motu" },
+	{ "hne", "Chhattisgarhi" },
+	{ "hr", "Croatian" },
+	{ "hsb", "Upper Sorbian" },
+	{ "hsn", "Xiang Chinese" },
+	{ "ht", "Haitian" },
+	{ "hu", "Hungarian" },
+	{ "hup", "Hupa" },
+	{ "hus", "Huastec" },
+	{ "hy", "Armenian" },
+	{ "hz", "Herero" },
+	{ "ia", "Interlingua" },
+	{ "iba", "Iban" },
+	{ "ibb", "Ibibio" },
+	{ "id", "Indonesian" },
+	{ "ie", "Interlingue" },
+	{ "ig", "Igbo" },
+	{ "ii", "Sichuan Yi" },
+	{ "ik", "Inupiaq" },
+	{ "ilo", "Iloko" },
+	{ "inh", "Ingush" },
+	{ "io", "Ido" },
+	{ "is", "Icelandic" },
+	{ "it", "Italian" },
+	{ "iu", "Inuktitut" },
+	{ "izh", "Ingrian" },
+	{ "ja", "Japanese" },
+	{ "jam", "Jamaican Creole English" },
+	{ "jbo", "Lojban" },
+	{ "jgo", "Ngomba" },
+	{ "jmc", "Machame" },
+	{ "jpr", "Judeo-Persian" },
+	{ "jrb", "Judeo-Arabic" },
+	{ "jut", "Jutish" },
+	{ "jv", "Javanese" },
+	{ "ka", "Georgian" },
+	{ "kaa", "Kara-Kalpak" },
+	{ "kab", "Kabyle" },
+	{ "kac", "Kachin" },
+	{ "kaj", "Jju" },
+	{ "kam", "Kamba" },
+	{ "kaw", "Kawi" },
+	{ "kbd", "Kabardian" },
+	{ "kbl", "Kanembu" },
+	{ "kcg", "Tyap" },
+	{ "kde", "Makonde" },
+	{ "kea", "Kabuverdianu" },
+	{ "ken", "Kenyang" },
+	{ "kfo", "Koro" },
+	{ "kg", "Kongo" },
+	{ "kgp", "Kaingang" },
+	{ "kha", "Khasi" },
+	{ "kho", "Khotanese" },
+	{ "khq", "Koyra Chiini" },
+	{ "khw", "Khowar" },
+	{ "ki", "Kikuyu" },
+	{ "kiu", "Kirmanjki" },
+	{ "kj", "Kuanyama" },
+	{ "kk", "Kazakh" },
+	{ "kkj", "Kako" },
+	{ "kl", "Kalaallisut" },
+	{ "kln", "Kalenjin" },
+	{ "km", "Central Khmer" },
+	{ "kmb", "Kimbundu" },
+	{ "kn", "Kannada" },
+	{ "ko", "Korean" },
+	{ "koi", "Komi-Permyak" },
+	{ "kok", "Konkani" },
+	{ "kos", "Kosraean" },
+	{ "kpe", "Kpelle" },
+	{ "kr", "Kanuri" },
+	{ "krc", "Karachay-Balkar" },
+	{ "kri", "Krio" },
+	{ "krj", "Kinaray-a" },
+	{ "krl", "Karelian" },
+	{ "kru", "Kurukh" },
+	{ "ks", "Kashmiri" },
+	{ "ksb", "Shambala" },
+	{ "ksf", "Bafia" },
+	{ "ksh", "Colognian" },
+	{ "ku", "Kurdish" },
+	{ "kum", "Kumyk" },
+	{ "kut", "Kutenai" },
+	{ "kv", "Komi" },
+	{ "kw", "Cornish" },
+	{ "ky", "Kirghiz" },
+	{ "lag", "Langi" },
+	{ "la", "Latin" },
+	{ "lad", "Ladino" },
+	{ "lag", "Langi" },
+	{ "lah", "Lahnda" },
+	{ "lam", "Lamba" },
+	{ "lb", "Luxembourgish" },
+	{ "lez", "Lezghian" },
+	{ "lfn", "Lingua Franca Nova" },
+	{ "lg", "Ganda" },
+	{ "li", "Limburgan" },
+	{ "lij", "Ligurian" },
+	{ "liv", "Livonian" },
+	{ "lkt", "Lakota" },
+	{ "lmo", "Lombard" },
+	{ "ln", "Lingala" },
+	{ "lo", "Lao" },
+	{ "lol", "Mongo" },
+	{ "lou", "Louisiana Creole" },
+	{ "loz", "Lozi" },
+	{ "lrc", "Northern Luri" },
+	{ "lt", "Lithuanian" },
+	{ "ltg", "Latgalian" },
+	{ "lu", "Luba-Katanga" },
+	{ "lua", "Luba-Lulua" },
+	{ "lui", "Luiseno" },
+	{ "lun", "Lunda" },
+	{ "luo", "Luo" },
+	{ "lus", "Mizo" },
+	{ "luy", "Luyia" },
+	{ "lv", "Latvian" },
+	{ "lzh", "Literary Chinese" },
+	{ "lzz", "Laz" },
+	{ "mad", "Madurese" },
+	{ "maf", "Mafa" },
+	{ "mag", "Magahi" },
+	{ "mai", "Maithili" },
+	{ "mak", "Makasar" },
+	{ "man", "Mandingo" },
+	{ "mas", "Masai" },
+	{ "mde", "Maba" },
+	{ "mdf", "Moksha" },
+	{ "mdr", "Mandar" },
+	{ "men", "Mende" },
+	{ "mer", "Meru" },
+	{ "mfe", "Morisyen" },
+	{ "mg", "Malagasy" },
+	{ "mga", "Middle Irish" },
+	{ "mgh", "Makhuwa-Meetto" },
+	{ "mgo", "Metaʼ" },
+	{ "mh", "Marshallese" },
+	{ "mhr", "Eastern Mari" },
+	{ "mi", "Māori" },
+	{ "mic", "Mi'kmaq" },
+	{ "min", "Minangkabau" },
+	{ "miq", "Mískito" },
+	{ "mjw", "Karbi" },
+	{ "mk", "Macedonian" },
+	{ "ml", "Malayalam" },
+	{ "mn", "Mongolian" },
+	{ "mnc", "Manchu" },
+	{ "mni", "Manipuri" },
+	{ "mnw", "Mon" },
+	{ "mos", "Mossi" },
+	{ "moh", "Mohawk" },
+	{ "mr", "Marathi" },
+	{ "mrj", "Western Mari" },
+	{ "ms", "Malay" },
+	{ "mt", "Maltese" },
+	{ "mua", "Mundang" },
+	{ "mus", "Muscogee" },
+	{ "mwl", "Mirandese" },
+	{ "mwr", "Marwari" },
+	{ "mwv", "Mentawai" },
+	{ "my", "Burmese" },
+	{ "mye", "Myene" },
+	{ "myv", "Erzya" },
+	{ "mzn", "Mazanderani" },
+	{ "na", "Nauru" },
+	{ "nah", "Nahuatl" },
+	{ "nan", "Min Nan Chinese" },
+	{ "nap", "Neapolitan" },
+	{ "naq", "Nama" },
+	{ "nan", "Min Nan Chinese" },
+	{ "nb", "Norwegian Bokmål" },
+	{ "nd", "North Ndebele" },
+	{ "nds", "Low German" },
+	{ "ne", "Nepali" },
+	{ "new", "Newari" },
+	{ "nhn", "Central Nahuatl" },
+	{ "ng", "Ndonga" },
+	{ "nia", "Nias" },
+	{ "niu", "Niuean" },
+	{ "njo", "Ao Naga" },
+	{ "nl", "Dutch" },
+	{ "nmg", "Kwasio" },
+	{ "nn", "Norwegian Nynorsk" },
+	{ "nnh", "Ngiemboon" },
+	{ "nog", "Nogai" },
+	{ "non", "Old Norse" },
+	{ "nov", "Novial" },
+	{ "nqo", "N'ko" },
+	{ "nr", "South Ndebele" },
+	{ "nso", "Pedi" },
+	{ "nus", "Nuer" },
+	{ "nv", "Navajo" },
+	{ "nwc", "Classical Newari" },
+	{ "ny", "Nyanja" },
+	{ "nym", "Nyamwezi" },
+	{ "nyn", "Nyankole" },
+	{ "nyo", "Nyoro" },
+	{ "nzi", "Nzima" },
+	{ "oc", "Occitan" },
+	{ "oj", "Ojibwa" },
+	{ "om", "Oromo" },
+	{ "or", "Odia" },
+	{ "os", "Ossetic" },
+	{ "osa", "Osage" },
+	{ "ota", "Ottoman Turkish" },
+	{ "pa", "Panjabi" },
+	{ "pag", "Pangasinan" },
+	{ "pal", "Pahlavi" },
+	{ "pam", "Pampanga" },
+	{ "pap", "Papiamento" },
+	{ "pau", "Palauan" },
+	{ "pcd", "Picard" },
+	{ "pcm", "Nigerian Pidgin" },
+	{ "pdc", "Pennsylvania German" },
+	{ "pdt", "Plautdietsch" },
+	{ "peo", "Old Persian" },
+	{ "pfl", "Palatine German" },
+	{ "phn", "Phoenician" },
+	{ "pi", "Pali" },
+	{ "pl", "Polish" },
+	{ "pms", "Piedmontese" },
+	{ "pnt", "Pontic" },
+	{ "pon", "Pohnpeian" },
+	{ "pr", "Pirate" },
+	{ "prg", "Prussian" },
+	{ "pro", "Old Provençal" },
+	{ "prs", "Dari" },
+	{ "ps", "Pushto" },
+	{ "pt", "Portuguese" },
+	{ "qu", "Quechua" },
+	{ "quc", "K'iche" },
+	{ "qug", "Chimborazo Highland Quichua" },
+	{ "quy", "Ayacucho Quechua" },
+	{ "quz", "Cusco Quechua" },
+	{ "raj", "Rajasthani" },
+	{ "rap", "Rapanui" },
+	{ "rar", "Rarotongan" },
+	{ "rgn", "Romagnol" },
+	{ "rif", "Riffian" },
+	{ "rm", "Romansh" },
+	{ "rn", "Rundi" },
+	{ "ro", "Romanian" },
+	{ "rof", "Rombo" },
+	{ "rom", "Romany" },
+	{ "rtm", "Rotuman" },
+	{ "ru", "Russian" },
+	{ "rue", "Rusyn" },
+	{ "rug", "Roviana" },
+	{ "rup", "Aromanian" },
+	{ "rw", "Kinyarwanda" },
+	{ "rwk", "Rwa" },
+	{ "sa", "Sanskrit" },
+	{ "sad", "Sandawe" },
+	{ "sah", "Sakha" },
+	{ "sam", "Samaritan Aramaic" },
+	{ "saq", "Samburu" },
+	{ "sas", "Sasak" },
+	{ "sat", "Santali" },
+	{ "saz", "Saurashtra" },
+	{ "sba", "Ngambay" },
+	{ "sbp", "Sangu" },
+	{ "sc", "Sardinian" },
+	{ "scn", "Sicilian" },
+	{ "sco", "Scots" },
+	{ "sd", "Sindhi" },
+	{ "sdc", "Sassarese Sardinian" },
+	{ "sdh", "Southern Kurdish" },
+	{ "se", "Northern Sami" },
+	{ "see", "Seneca" },
+	{ "seh", "Sena" },
+	{ "sei", "Seri" },
+	{ "sel", "Selkup" },
+	{ "ses", "Koyraboro Senni" },
+	{ "sg", "Sango" },
+	{ "sga", "Old Irish" },
+	{ "sgs", "Samogitian" },
+	{ "sh", "Serbo-Croatian" },
+	{ "shi", "Tachelhit" },
+	{ "shn", "Shan" },
+	{ "shs", "Shuswap" },
+	{ "shu", "Chadian Arabic" },
+	{ "si", "Sinhala" },
+	{ "sid", "Sidamo" },
+	{ "sk", "Slovak" },
+	{ "sl", "Slovenian" },
+	{ "sli", "Lower Silesian" },
+	{ "sly", "Selayar" },
+	{ "sm", "Samoan" },
+	{ "sma", "Southern Sami" },
+	{ "smj", "Lule Sami" },
+	{ "smn", "Inari Sami" },
+	{ "sms", "Skolt Sami" },
+	{ "sn", "Shona" },
+	{ "snk", "Soninke" },
+	{ "so", "Somali" },
+	{ "sog", "Sogdien" },
+	{ "son", "Songhai" },
+	{ "sq", "Albanian" },
+	{ "sr", "Serbian" },
+	{ "srn", "Sranan Tongo" },
+	{ "srr", "Serer" },
+	{ "ss", "Swati" },
+	{ "ssy", "Saho" },
+	{ "st", "Southern Sotho" },
+	{ "stq", "Saterland Frisian" },
+	{ "su", "Sundanese" },
+	{ "suk", "Sukuma" },
+	{ "sus", "Susu" },
+	{ "sux", "Sumerian" },
+	{ "sv", "Swedish" },
+	{ "sw", "Swahili" },
+	{ "swb", "Comorian" },
+	{ "swc", "Congo Swahili" },
+	{ "syc", "Classical Syriac" },
+	{ "syr", "Syriac" },
+	{ "szl", "Silesian" },
+	{ "ta", "Tamil" },
+	{ "tcy", "Tulu" },
+	{ "te", "Telugu" },
+	{ "tem", "Timne" },
+	{ "teo", "Teso" },
+	{ "ter", "Tereno" },
+	{ "tet", "Tetum" },
+	{ "tg", "Tajik" },
+	{ "th", "Thai" },
+	{ "the", "Chitwania Tharu" },
+	{ "ti", "Tigrinya" },
+	{ "tig", "Tigre" },
+	{ "tiv", "Tiv" },
+	{ "tk", "Turkmen" },
+	{ "tkl", "Tokelau" },
+	{ "tkr", "Tsakhur" },
+	{ "tl", "Tagalog" },
+	{ "tlh", "Klingon" },
+	{ "tli", "Tlingit" },
+	{ "tly", "Talysh" },
+	{ "tmh", "Tamashek" },
+	{ "tn", "Tswana" },
+	{ "to", "Tongan" },
+	{ "tog", "Nyasa Tonga" },
+	{ "tpi", "Tok Pisin" },
+	{ "tr", "Turkish" },
+	{ "tru", "Turoyo" },
+	{ "trv", "Taroko" },
+	{ "ts", "Tsonga" },
+	{ "tsd", "Tsakonian" },
+	{ "tsi", "Tsimshian" },
+	{ "tt", "Tatar" },
+	{ "ttt", "Muslim Tat" },
+	{ "tum", "Tumbuka" },
+	{ "tvl", "Tuvalu" },
+	{ "tw", "Twi" },
+	{ "twq", "Tasawaq" },
+	{ "ty", "Tahitian" },
+	{ "tyv", "Tuvinian" },
+	{ "tzm", "Central Atlas Tamazight" },
+	{ "udm", "Udmurt" },
+	{ "ug", "Uyghur" },
+	{ "uga", "Ugaritic" },
+	{ "uk", "Ukrainian" },
+	{ "umb", "Umbundu" },
+	{ "unm", "Unami" },
+	{ "ur", "Urdu" },
+	{ "uz", "Uzbek" },
+	{ "vai", "Vai" },
+	{ "ve", "Venda" },
+	{ "vec", "Venetian" },
+	{ "vep", "Veps" },
+	{ "vi", "Vietnamese" },
+	{ "vls", "West Flemish" },
+	{ "vmf", "Main-Franconian" },
+	{ "vo", "Volapük" },
+	{ "vot", "Votic" },
+	{ "vro", "Võro" },
+	{ "vun", "Vunjo" },
+	{ "wa", "Walloon" },
+	{ "wae", "Walser" },
+	{ "wal", "Wolaytta" },
+	{ "war", "Waray" },
+	{ "was", "Washo" },
+	{ "wbp", "Warlpiri" },
+	{ "wo", "Wolof" },
+	{ "wuu", "Wu Chinese" },
+	{ "xal", "Kalmyk" },
+	{ "xh", "Xhosa" },
+	{ "xmf", "Mingrelian" },
+	{ "xog", "Soga" },
+	{ "yao", "Yao" },
+	{ "yap", "Yapese" },
+	{ "yav", "Yangben" },
+	{ "ybb", "Yemba" },
+	{ "yi", "Yiddish" },
+	{ "yo", "Yoruba" },
+	{ "yrl", "Nheengatu" },
+	{ "yue", "Yue Chinese" },
+	{ "yuw", "Papua New Guinea" },
+	{ "za", "Zhuang" },
+	{ "zap", "Zapotec" },
+	{ "zbl", "Blissymbols" },
+	{ "zea", "Zeelandic" },
+	{ "zen", "Zenaga" },
+	{ "zgh", "Standard Moroccan Tamazight" },
+	{ "zh", "Chinese" },
+	{ "zu", "Zulu" },
+	{ "zun", "Zuni" },
+	{ "zza", "Zaza" },
+	{ nullptr, nullptr }
+};
+
+// Additional regional variants.
+// Variant name, supported languages.
+
+static const char *locale_variants[][2] = {
+	{ "valencia", "ca" },
+	{ "iqtelif", "tt" },
+	{ "saaho", "aa" },
+	{ "tradnl", "es" },
+	{ nullptr, nullptr },
+};
+
+// Script names and codes (excludes typographic variants, special codes, reserved codes and aliases for combined scripts).
+// Reference:
+// - https://en.wikipedia.org/wiki/ISO_15924
+
+static const char *script_list[][2] = {
+	{ "Adlam", "Adlm" },
+	{ "Afaka", "Afak" },
+	{ "Caucasian Albanian", "Aghb" },
+	{ "Ahom", "Ahom" },
+	{ "Arabic", "Arab" },
+	{ "Imperial Aramaic", "Armi" },
+	{ "Armenian", "Armn" },
+	{ "Avestan", "Avst" },
+	{ "Balinese", "Bali" },
+	{ "Bamum", "Bamu" },
+	{ "Bassa Vah", "Bass" },
+	{ "Batak", "Batk" },
+	{ "Bengali", "Beng" },
+	{ "Bhaiksuki", "Bhks" },
+	{ "Blissymbols", "Blis" },
+	{ "Bopomofo", "Bopo" },
+	{ "Brahmi", "Brah" },
+	{ "Braille", "Brai" },
+	{ "Buginese", "Bugi" },
+	{ "Buhid", "Buhd" },
+	{ "Chakma", "Cakm" },
+	{ "Unified Canadian Aboriginal", "Cans" },
+	{ "Carian", "Cari" },
+	{ "Cham", "Cham" },
+	{ "Cherokee", "Cher" },
+	{ "Chorasmian", "Chrs" },
+	{ "Cirth", "Cirt" },
+	{ "Coptic", "Copt" },
+	{ "Cypro-Minoan", "Cpmn" },
+	{ "Cypriot", "Cprt" },
+	{ "Cyrillic", "Cyrl" },
+	{ "Devanagari", "Deva" },
+	{ "Dives Akuru", "Diak" },
+	{ "Dogra", "Dogr" },
+	{ "Deseret", "Dsrt" },
+	{ "Duployan", "Dupl" },
+	{ "Egyptian demotic", "Egyd" },
+	{ "Egyptian hieratic", "Egyh" },
+	{ "Egyptian hieroglyphs", "Egyp" },
+	{ "Elbasan", "Elba" },
+	{ "Elymaic", "Elym" },
+	{ "Ethiopic", "Ethi" },
+	{ "Khutsuri", "Geok" },
+	{ "Georgian", "Geor" },
+	{ "Glagolitic", "Glag" },
+	{ "Gunjala Gondi", "Gong" },
+	{ "Masaram Gondi", "Gonm" },
+	{ "Gothic", "Goth" },
+	{ "Grantha", "Gran" },
+	{ "Greek", "Grek" },
+	{ "Gujarati", "Gujr" },
+	{ "Gurmukhi", "Guru" },
+	{ "Hangul", "Hang" },
+	{ "Han", "Hani" },
+	{ "Hanunoo", "Hano" },
+	{ "Simplified", "Hans" },
+	{ "Traditional", "Hant" },
+	{ "Hatran", "Hatr" },
+	{ "Hebrew", "Hebr" },
+	{ "Hiragana", "Hira" },
+	{ "Anatolian Hieroglyphs", "Hluw" },
+	{ "Pahawh Hmong", "Hmng" },
+	{ "Nyiakeng Puachue Hmong", "Hmnp" },
+	{ "Old Hungarian", "Hung" },
+	{ "Indus", "Inds" },
+	{ "Old Italic", "Ital" },
+	{ "Javanese", "Java" },
+	{ "Jurchen", "Jurc" },
+	{ "Kayah Li", "Kali" },
+	{ "Katakana", "Kana" },
+	{ "Kharoshthi", "Khar" },
+	{ "Khmer", "Khmr" },
+	{ "Khojki", "Khoj" },
+	{ "Khitan large script", "Kitl" },
+	{ "Khitan small script", "Kits" },
+	{ "Kannada", "Knda" },
+	{ "Kpelle", "Kpel" },
+	{ "Kaithi", "Kthi" },
+	{ "Tai Tham", "Lana" },
+	{ "Lao", "Laoo" },
+	{ "Latin", "Latn" },
+	{ "Leke", "Leke" },
+	{ "Lepcha", "Lepc" },
+	{ "Limbu", "Limb" },
+	{ "Linear A", "Lina" },
+	{ "Linear B", "Linb" },
+	{ "Lisu", "Lisu" },
+	{ "Loma", "Loma" },
+	{ "Lycian", "Lyci" },
+	{ "Lydian", "Lydi" },
+	{ "Mahajani", "Mahj" },
+	{ "Makasar", "Maka" },
+	{ "Mandaic", "Mand" },
+	{ "Manichaean", "Mani" },
+	{ "Marchen", "Marc" },
+	{ "Mayan Hieroglyphs", "Maya" },
+	{ "Medefaidrin", "Medf" },
+	{ "Mende Kikakui", "Mend" },
+	{ "Meroitic Cursive", "Merc" },
+	{ "Meroitic Hieroglyphs", "Mero" },
+	{ "Malayalam", "Mlym" },
+	{ "Modi", "Modi" },
+	{ "Mongolian", "Mong" },
+	{ "Moon", "Moon" },
+	{ "Mro", "Mroo" },
+	{ "Meitei Mayek", "Mtei" },
+	{ "Multani", "Mult" },
+	{ "Myanmar (Burmese)", "Mymr" },
+	{ "Nandinagari", "Nand" },
+	{ "Old North Arabian", "Narb" },
+	{ "Nabataean", "Nbat" },
+	{ "Newa", "Newa" },
+	{ "Naxi Dongba", "Nkdb" },
+	{ "Nakhi Geba", "Nkgb" },
+	{ "N'ko", "Nkoo" },
+	{ "Nüshu", "Nshu" },
+	{ "Ogham", "Ogam" },
+	{ "Ol Chiki", "Olck" },
+	{ "Old Turkic", "Orkh" },
+	{ "Oriya", "Orya" },
+	{ "Osage", "Osge" },
+	{ "Osmanya", "Osma" },
+	{ "Old Uyghur", "Ougr" },
+	{ "Palmyrene", "Palm" },
+	{ "Pau Cin Hau", "Pauc" },
+	{ "Proto-Cuneiform", "Pcun" },
+	{ "Proto-Elamite", "Pelm" },
+	{ "Old Permic", "Perm" },
+	{ "Phags-pa", "Phag" },
+	{ "Inscriptional Pahlavi", "Phli" },
+	{ "Psalter Pahlavi", "Phlp" },
+	{ "Book Pahlavi", "Phlv" },
+	{ "Phoenician", "Phnx" },
+	{ "Klingon", "Piqd" },
+	{ "Miao", "Plrd" },
+	{ "Inscriptional Parthian", "Prti" },
+	{ "Proto-Sinaitic", "Psin" },
+	{ "Ranjana", "Ranj" },
+	{ "Rejang", "Rjng" },
+	{ "Hanifi Rohingya", "Rohg" },
+	{ "Rongorongo", "Roro" },
+	{ "Runic", "Runr" },
+	{ "Samaritan", "Samr" },
+	{ "Sarati", "Sara" },
+	{ "Old South Arabian", "Sarb" },
+	{ "Saurashtra", "Saur" },
+	{ "SignWriting", "Sgnw" },
+	{ "Shavian", "Shaw" },
+	{ "Sharada", "Shrd" },
+	{ "Shuishu", "Shui" },
+	{ "Siddham", "Sidd" },
+	{ "Khudawadi", "Sind" },
+	{ "Sinhala", "Sinh" },
+	{ "Sogdian", "Sogd" },
+	{ "Old Sogdian", "Sogo" },
+	{ "Sora Sompeng", "Sora" },
+	{ "Soyombo", "Soyo" },
+	{ "Sundanese", "Sund" },
+	{ "Syloti Nagri", "Sylo" },
+	{ "Syriac", "Syrc" },
+	{ "Tagbanwa", "Tagb" },
+	{ "Takri", "Takr" },
+	{ "Tai Le", "Tale" },
+	{ "New Tai Lue", "Talu" },
+	{ "Tamil", "Taml" },
+	{ "Tangut", "Tang" },
+	{ "Tai Viet", "Tavt" },
+	{ "Telugu", "Telu" },
+	{ "Tengwar", "Teng" },
+	{ "Tifinagh", "Tfng" },
+	{ "Tagalog", "Tglg" },
+	{ "Thaana", "Thaa" },
+	{ "Thai", "Thai" },
+	{ "Tibetan", "Tibt" },
+	{ "Tirhuta", "Tirh" },
+	{ "Tangsa", "Tnsa" },
+	{ "Toto", "Toto" },
+	{ "Ugaritic", "Ugar" },
+	{ "Vai", "Vaii" },
+	{ "Visible Speech", "Visp" },
+	{ "Vithkuqi", "Vith" },
+	{ "Warang Citi", "Wara" },
+	{ "Wancho", "Wcho" },
+	{ "Woleai", "Wole" },
+	{ "Old Persian", "Xpeo" },
+	{ "Cuneiform", "Xsux" },
+	{ "Yezidi", "Yezi" },
+	{ "Yi", "Yiii" },
+	{ "Zanabazar Square", "Zanb" },
+	{ nullptr, nullptr }
+};
+
+#endif // LOCALES_H

Fichier diff supprimé car celui-ci est trop grand
+ 266 - 867
core/string/translation.cpp


+ 29 - 7
core/string/translation.h

@@ -78,8 +78,6 @@ class TranslationServer : public Object {
 	Ref<Translation> tool_translation;
 	Ref<Translation> doc_translation;
 
-	Map<String, String> locale_name_map;
-
 	bool enabled = true;
 
 	bool pseudolocalization_enabled = false;
@@ -109,6 +107,23 @@ class TranslationServer : public Object {
 
 	static void _bind_methods();
 
+	struct LocaleScriptInfo {
+		String name;
+		String script;
+		String default_country;
+		Set<String> supported_countries;
+	};
+	static Vector<LocaleScriptInfo> locale_script_info;
+
+	static Map<String, String> language_map;
+	static Map<String, String> script_map;
+	static Map<String, String> locale_rename_map;
+	static Map<String, String> country_name_map;
+	static Map<String, String> country_rename_map;
+	static Map<String, String> variant_map;
+
+	void init_locale_info();
+
 public:
 	_FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; }
 
@@ -119,6 +134,15 @@ public:
 	String get_locale() const;
 	Ref<Translation> get_translation_object(const String &p_locale);
 
+	Vector<String> get_all_languages() const;
+	String get_language_name(const String &p_language) const;
+
+	Vector<String> get_all_scripts() const;
+	String get_script_name(const String &p_script) const;
+
+	Vector<String> get_all_countries() const;
+	String get_country_name(const String &p_country) const;
+
 	String get_locale_name(const String &p_locale) const;
 
 	Array get_loaded_locales() const;
@@ -136,11 +160,9 @@ public:
 	void set_editor_pseudolocalization(bool p_enabled);
 	void reload_pseudolocalization();
 
-	static Vector<String> get_all_locales();
-	static Vector<String> get_all_locale_names();
-	static bool is_locale_valid(const String &p_locale);
-	static String standardize_locale(const String &p_locale);
-	static String get_language_code(const String &p_locale);
+	String standardize_locale(const String &p_locale) const;
+
+	int compare_locales(const String &p_locale_a, const String &p_locale_b) const;
 
 	String get_tool_locale();
 	void set_tool_translation(const Ref<Translation> &p_translation);

+ 4 - 1
doc/classes/@GlobalScope.xml

@@ -2493,7 +2493,10 @@
 		</constant>
 		<constant name="PROPERTY_HINT_ARRAY_TYPE" value="39" enum="PropertyHint">
 		</constant>
-		<constant name="PROPERTY_HINT_MAX" value="41" enum="PropertyHint">
+		<constant name="PROPERTY_HINT_LOCALE_ID" value="41" enum="PropertyHint">
+			Hints that a string property is a locale code. Editing it will show a locale dialog for picking language and country.
+		</constant>
+		<constant name="PROPERTY_HINT_MAX" value="42" enum="PropertyHint">
 		</constant>
 		<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags">
 		</constant>

+ 54 - 0
doc/classes/TranslationServer.xml

@@ -24,6 +24,46 @@
 				Clears the server from all translations.
 			</description>
 		</method>
+		<method name="compare_locales" qualifiers="const">
+			<return type="int" />
+			<argument index="0" name="locale_a" type="String" />
+			<argument index="1" name="locale_b" type="String" />
+			<description>
+				Compares two locales and return similarity score between [code]0[/code](no match) and [code]10[/code](full match).
+			</description>
+		</method>
+		<method name="get_all_countries" qualifiers="const">
+			<return type="PackedStringArray" />
+			<description>
+				Returns array of known country codes.
+			</description>
+		</method>
+		<method name="get_all_languages" qualifiers="const">
+			<return type="PackedStringArray" />
+			<description>
+				Returns array of known language codes.
+			</description>
+		</method>
+		<method name="get_all_scripts" qualifiers="const">
+			<return type="PackedStringArray" />
+			<description>
+				Returns array of known script codes.
+			</description>
+		</method>
+		<method name="get_country_name" qualifiers="const">
+			<return type="String" />
+			<argument index="0" name="country" type="String" />
+			<description>
+				Returns readable country name for the [code]country[/code] code.
+			</description>
+		</method>
+		<method name="get_language_name" qualifiers="const">
+			<return type="String" />
+			<argument index="0" name="language" type="String" />
+			<description>
+				Returns readable language name for the [code]language[/code] code.
+			</description>
+		</method>
 		<method name="get_loaded_locales" qualifiers="const">
 			<return type="Array" />
 			<description>
@@ -44,6 +84,13 @@
 				Returns a locale's language and its variant (e.g. [code]"en_US"[/code] would return [code]"English (United States)"[/code]).
 			</description>
 		</method>
+		<method name="get_script_name" qualifiers="const">
+			<return type="String" />
+			<argument index="0" name="script" type="String" />
+			<description>
+				Returns readable script name for the [code]script[/code] code.
+			</description>
+		</method>
 		<method name="get_translation_object">
 			<return type="Translation" />
 			<argument index="0" name="locale" type="String" />
@@ -80,6 +127,13 @@
 				If translations have been loaded beforehand for the new locale, they will be applied.
 			</description>
 		</method>
+		<method name="standardize_locale" qualifiers="const">
+			<return type="String" />
+			<argument index="0" name="locale" type="String" />
+			<description>
+				Retunrs [code]locale[/code] string standardized to match known locales (e.g. [code]en-US[/code] would be matched to [code]en_US[/code]).
+			</description>
+		</method>
 		<method name="translate" qualifiers="const">
 			<return type="StringName" />
 			<argument index="0" name="message" type="StringName" />

+ 563 - 0
editor/editor_locale_dialog.cpp

@@ -0,0 +1,563 @@
+/*************************************************************************/
+/*  editor_locale_dialog.cpp                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "editor_locale_dialog.h"
+
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "scene/gui/check_button.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/tree.h"
+
+static _FORCE_INLINE_ bool is_upper_case(char32_t c) {
+	return (c >= 'A' && c <= 'Z');
+}
+
+static _FORCE_INLINE_ bool is_lower_case(char32_t c) {
+	return (c >= 'a' && c <= 'z');
+}
+
+void EditorLocaleDialog::_bind_methods() {
+	ADD_SIGNAL(MethodInfo("locale_selected", PropertyInfo(Variant::STRING, "locale")));
+}
+
+void EditorLocaleDialog::ok_pressed() {
+	if (edit_filters->is_pressed()) {
+		return; // Do not update, if in filter edit mode.
+	}
+
+	String locale;
+	if (lang_code->get_text().is_empty()) {
+		return; // Language code is required.
+	}
+	locale = lang_code->get_text();
+
+	if (!script_code->get_text().is_empty()) {
+		locale += "_" + script_code->get_text();
+	}
+	if (!country_code->get_text().is_empty()) {
+		locale += "_" + country_code->get_text();
+	}
+	if (!variant_code->get_text().is_empty()) {
+		locale += "_" + variant_code->get_text();
+	}
+
+	emit_signal(SNAME("locale_selected"), TranslationServer::get_singleton()->standardize_locale(locale));
+	hide();
+}
+
+void EditorLocaleDialog::_item_selected() {
+	if (updating_lists) {
+		return;
+	}
+
+	if (edit_filters->is_pressed()) {
+		return; // Do not update, if in filter edit mode.
+	}
+
+	TreeItem *l = lang_list->get_selected();
+	if (l) {
+		lang_code->set_text(l->get_metadata(0).operator String());
+	}
+
+	TreeItem *s = script_list->get_selected();
+	if (s) {
+		script_code->set_text(s->get_metadata(0).operator String());
+	}
+
+	TreeItem *c = cnt_list->get_selected();
+	if (c) {
+		country_code->set_text(c->get_metadata(0).operator String());
+	}
+}
+
+void EditorLocaleDialog::_toggle_advanced(bool p_checked) {
+	if (!p_checked) {
+		script_code->set_text("");
+		variant_code->set_text("");
+	}
+	_update_tree();
+}
+
+void EditorLocaleDialog::_post_popup() {
+	ConfirmationDialog::_post_popup();
+
+	if (!locale_set) {
+		lang_code->set_text("");
+		script_code->set_text("");
+		country_code->set_text("");
+		variant_code->set_text("");
+	}
+	edit_filters->set_pressed(false);
+	_update_tree();
+}
+
+void EditorLocaleDialog::_filter_lang_option_changed() {
+	TreeItem *t = lang_list->get_edited();
+	String lang = t->get_metadata(0);
+	bool checked = t->is_checked(0);
+
+	Variant prev;
+	Array f_lang_all;
+
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/language_filter")) {
+		f_lang_all = ProjectSettings::get_singleton()->get("internationalization/locale/language_filter");
+		prev = f_lang_all;
+	}
+
+	int l_idx = f_lang_all.find(lang);
+
+	if (checked) {
+		if (l_idx == -1) {
+			f_lang_all.append(lang);
+		}
+	} else {
+		if (l_idx != -1) {
+			f_lang_all.remove_at(l_idx);
+		}
+	}
+
+	f_lang_all.sort();
+
+	undo_redo->create_action(TTR("Changed Locale Language Filter"));
+	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/language_filter", f_lang_all);
+	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/language_filter", prev);
+	undo_redo->commit_action();
+}
+
+void EditorLocaleDialog::_filter_script_option_changed() {
+	TreeItem *t = script_list->get_edited();
+	String script = t->get_metadata(0);
+	bool checked = t->is_checked(0);
+
+	Variant prev;
+	Array f_script_all;
+
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/script_filter")) {
+		f_script_all = ProjectSettings::get_singleton()->get("internationalization/locale/script_filter");
+		prev = f_script_all;
+	}
+
+	int l_idx = f_script_all.find(script);
+
+	if (checked) {
+		if (l_idx == -1) {
+			f_script_all.append(script);
+		}
+	} else {
+		if (l_idx != -1) {
+			f_script_all.remove_at(l_idx);
+		}
+	}
+
+	f_script_all.sort();
+
+	undo_redo->create_action(TTR("Changed Locale Script Filter"));
+	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/script_filter", f_script_all);
+	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/script_filter", prev);
+	undo_redo->commit_action();
+}
+
+void EditorLocaleDialog::_filter_cnt_option_changed() {
+	TreeItem *t = cnt_list->get_edited();
+	String cnt = t->get_metadata(0);
+	bool checked = t->is_checked(0);
+
+	Variant prev;
+	Array f_cnt_all;
+
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/country_filter")) {
+		f_cnt_all = ProjectSettings::get_singleton()->get("internationalization/locale/country_filter");
+		prev = f_cnt_all;
+	}
+
+	int l_idx = f_cnt_all.find(cnt);
+
+	if (checked) {
+		if (l_idx == -1) {
+			f_cnt_all.append(cnt);
+		}
+	} else {
+		if (l_idx != -1) {
+			f_cnt_all.remove_at(l_idx);
+		}
+	}
+
+	f_cnt_all.sort();
+
+	undo_redo->create_action(TTR("Changed Locale Country Filter"));
+	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/country_filter", f_cnt_all);
+	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/country_filter", prev);
+	undo_redo->commit_action();
+}
+
+void EditorLocaleDialog::_filter_mode_changed(int p_mode) {
+	int f_mode = filter_mode->get_selected_id();
+	Variant prev;
+
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/locale_filter_mode")) {
+		prev = ProjectSettings::get_singleton()->get("internationalization/locale/locale_filter_mode");
+	}
+
+	undo_redo->create_action(TTR("Changed Locale Filter Mode"));
+	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter_mode", f_mode);
+	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter_mode", prev);
+	undo_redo->commit_action();
+
+	_update_tree();
+}
+
+void EditorLocaleDialog::_edit_filters(bool p_checked) {
+	_update_tree();
+}
+
+void EditorLocaleDialog::_update_tree() {
+	updating_lists = true;
+
+	int filter = SHOW_ALL_LOCALES;
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/locale_filter_mode")) {
+		filter = ProjectSettings::get_singleton()->get("internationalization/locale/locale_filter_mode");
+	}
+	Array f_lang_all;
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/language_filter")) {
+		f_lang_all = ProjectSettings::get_singleton()->get("internationalization/locale/language_filter");
+	}
+	Array f_cnt_all;
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/country_filter")) {
+		f_cnt_all = ProjectSettings::get_singleton()->get("internationalization/locale/country_filter");
+	}
+	Array f_script_all;
+	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/script_filter")) {
+		f_script_all = ProjectSettings::get_singleton()->get("internationalization/locale/script_filter");
+	}
+	bool is_edit_mode = edit_filters->is_pressed();
+
+	filter_mode->select(filter);
+
+	// Hide text advanced edit and disable OK button if in filter edit mode.
+	advanced->set_visible(!is_edit_mode);
+	hb_locale->set_visible(!is_edit_mode && advanced->is_pressed());
+	vb_script_list->set_visible(advanced->is_pressed());
+	get_ok_button()->set_disabled(is_edit_mode);
+
+	// Update language list.
+	lang_list->clear();
+	TreeItem *l_root = lang_list->create_item(nullptr);
+	lang_list->set_hide_root(true);
+
+	Vector<String> languages = TranslationServer::get_singleton()->get_all_languages();
+	for (const String &E : languages) {
+		if (is_edit_mode || (filter == SHOW_ALL_LOCALES) || f_lang_all.has(E) || f_lang_all.is_empty()) {
+			const String &lang = TranslationServer::get_singleton()->get_language_name(E);
+			TreeItem *t = lang_list->create_item(l_root);
+			if (is_edit_mode) {
+				t->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
+				t->set_editable(0, true);
+				t->set_checked(0, f_lang_all.has(E));
+			} else if (lang_code->get_text() == E) {
+				t->select(0);
+			}
+			t->set_text(0, vformat("%s [%s]", lang, E));
+			t->set_metadata(0, E);
+		}
+	}
+
+	// Update script list.
+	script_list->clear();
+	TreeItem *s_root = script_list->create_item(nullptr);
+	script_list->set_hide_root(true);
+
+	if (!is_edit_mode) {
+		TreeItem *t = script_list->create_item(s_root);
+		t->set_text(0, "[Default]");
+		t->set_metadata(0, "");
+	}
+
+	Vector<String> scripts = TranslationServer::get_singleton()->get_all_scripts();
+	for (const String &E : scripts) {
+		if (is_edit_mode || (filter == SHOW_ALL_LOCALES) || f_script_all.has(E) || f_script_all.is_empty()) {
+			const String &script = TranslationServer::get_singleton()->get_script_name(E);
+			TreeItem *t = script_list->create_item(s_root);
+			if (is_edit_mode) {
+				t->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
+				t->set_editable(0, true);
+				t->set_checked(0, f_script_all.has(E));
+			} else if (script_code->get_text() == E) {
+				t->select(0);
+			}
+			t->set_text(0, vformat("%s [%s]", script, E));
+			t->set_metadata(0, E);
+		}
+	}
+
+	// Update country list.
+	cnt_list->clear();
+	TreeItem *c_root = cnt_list->create_item(nullptr);
+	cnt_list->set_hide_root(true);
+
+	if (!is_edit_mode) {
+		TreeItem *t = cnt_list->create_item(c_root);
+		t->set_text(0, "[Default]");
+		t->set_metadata(0, "");
+	}
+
+	Vector<String> countries = TranslationServer::get_singleton()->get_all_countries();
+	for (const String &E : countries) {
+		if (is_edit_mode || (filter == SHOW_ALL_LOCALES) || f_cnt_all.has(E) || f_cnt_all.is_empty()) {
+			const String &cnt = TranslationServer::get_singleton()->get_country_name(E);
+			TreeItem *t = cnt_list->create_item(c_root);
+			if (is_edit_mode) {
+				t->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
+				t->set_editable(0, true);
+				t->set_checked(0, f_cnt_all.has(E));
+			} else if (country_code->get_text() == E) {
+				t->select(0);
+			}
+			t->set_text(0, vformat("%s [%s]", cnt, E));
+			t->set_metadata(0, E);
+		}
+	}
+	updating_lists = false;
+}
+
+void EditorLocaleDialog::set_locale(const String &p_locale) {
+	const String &locale = TranslationServer::get_singleton()->standardize_locale(p_locale);
+	if (locale.is_empty()) {
+		locale_set = false;
+
+		lang_code->set_text("");
+		script_code->set_text("");
+		country_code->set_text("");
+		variant_code->set_text("");
+	} else {
+		locale_set = true;
+
+		Vector<String> locale_elements = p_locale.split("_");
+		lang_code->set_text(locale_elements[0]);
+		if (locale_elements.size() >= 2) {
+			if (locale_elements[1].length() == 4 && is_upper_case(locale_elements[1][0]) && is_lower_case(locale_elements[1][1]) && is_lower_case(locale_elements[1][2]) && is_lower_case(locale_elements[1][3])) {
+				script_code->set_text(locale_elements[1]);
+				advanced->set_pressed(true);
+			}
+			if (locale_elements[1].length() == 2 && is_upper_case(locale_elements[1][0]) && is_upper_case(locale_elements[1][1])) {
+				country_code->set_text(locale_elements[1]);
+			}
+		}
+		if (locale_elements.size() >= 3) {
+			if (locale_elements[2].length() == 2 && is_upper_case(locale_elements[2][0]) && is_upper_case(locale_elements[2][1])) {
+				country_code->set_text(locale_elements[2]);
+			} else {
+				variant_code->set_text(locale_elements[2].to_lower());
+				advanced->set_pressed(true);
+			}
+		}
+		if (locale_elements.size() >= 4) {
+			variant_code->set_text(locale_elements[3].to_lower());
+			advanced->set_pressed(true);
+		}
+	}
+}
+
+void EditorLocaleDialog::popup_locale_dialog() {
+	popup_centered_clamped(Size2(1050, 700) * EDSCALE, 0.8);
+}
+
+EditorLocaleDialog::EditorLocaleDialog() {
+	undo_redo = EditorNode::get_undo_redo();
+
+	set_title(TTR("Select a Locale"));
+
+	VBoxContainer *vb = memnew(VBoxContainer);
+	{
+		HBoxContainer *hb_filter = memnew(HBoxContainer);
+		{
+			filter_mode = memnew(OptionButton);
+			filter_mode->add_item(TTR("Show All Locales"), SHOW_ALL_LOCALES);
+			filter_mode->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+			filter_mode->add_item(TTR("Show Selected Locales Only"), SHOW_ONLY_SELECTED_LOCALES);
+			filter_mode->select(0);
+			filter_mode->connect("item_selected", callable_mp(this, &EditorLocaleDialog::_filter_mode_changed));
+			hb_filter->add_child(filter_mode);
+		}
+		{
+			edit_filters = memnew(CheckButton);
+			edit_filters->set_text("Edit Filters");
+			edit_filters->set_toggle_mode(true);
+			edit_filters->set_pressed(false);
+			edit_filters->connect("toggled", callable_mp(this, &EditorLocaleDialog::_edit_filters));
+			hb_filter->add_child(edit_filters);
+		}
+		{
+			advanced = memnew(CheckButton);
+			advanced->set_text("Advanced");
+			advanced->set_toggle_mode(true);
+			advanced->set_pressed(false);
+			advanced->connect("toggled", callable_mp(this, &EditorLocaleDialog::_toggle_advanced));
+			hb_filter->add_child(advanced);
+		}
+		vb->add_child(hb_filter);
+	}
+	{
+		HBoxContainer *hb_lists = memnew(HBoxContainer);
+		hb_lists->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+		{
+			VBoxContainer *vb_lang_list = memnew(VBoxContainer);
+			vb_lang_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+			{
+				Label *lang_lbl = memnew(Label);
+				lang_lbl->set_text(TTR("Language:"));
+				vb_lang_list->add_child(lang_lbl);
+			}
+			{
+				lang_list = memnew(Tree);
+				lang_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+				lang_list->connect("cell_selected", callable_mp(this, &EditorLocaleDialog::_item_selected));
+				lang_list->set_columns(1);
+				lang_list->connect("item_edited", callable_mp(this, &EditorLocaleDialog::_filter_lang_option_changed));
+				vb_lang_list->add_child(lang_list);
+			}
+			hb_lists->add_child(vb_lang_list);
+		}
+		{
+			vb_script_list = memnew(VBoxContainer);
+			vb_script_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+			{
+				Label *script_lbl = memnew(Label);
+				script_lbl->set_text(TTR("Script:"));
+				vb_script_list->add_child(script_lbl);
+			}
+			{
+				script_list = memnew(Tree);
+				script_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+				script_list->connect("cell_selected", callable_mp(this, &EditorLocaleDialog::_item_selected));
+				script_list->set_columns(1);
+				script_list->connect("item_edited", callable_mp(this, &EditorLocaleDialog::_filter_script_option_changed));
+				vb_script_list->add_child(script_list);
+			}
+			hb_lists->add_child(vb_script_list);
+		}
+		{
+			VBoxContainer *vb_cnt_list = memnew(VBoxContainer);
+			vb_cnt_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+			{
+				Label *cnt_lbl = memnew(Label);
+				cnt_lbl->set_text(TTR("Country:"));
+				vb_cnt_list->add_child(cnt_lbl);
+			}
+			{
+				cnt_list = memnew(Tree);
+				cnt_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+				cnt_list->connect("cell_selected", callable_mp(this, &EditorLocaleDialog::_item_selected));
+				cnt_list->set_columns(1);
+				cnt_list->connect("item_edited", callable_mp(this, &EditorLocaleDialog::_filter_cnt_option_changed));
+				vb_cnt_list->add_child(cnt_list);
+			}
+			hb_lists->add_child(vb_cnt_list);
+		}
+		vb->add_child(hb_lists);
+	}
+	{
+		hb_locale = memnew(HBoxContainer);
+		hb_locale->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+		{
+			{
+				VBoxContainer *vb_language = memnew(VBoxContainer);
+				vb_language->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+				{
+					Label *language_lbl = memnew(Label);
+					language_lbl->set_text(TTR("Language"));
+					vb_language->add_child(language_lbl);
+				}
+				{
+					lang_code = memnew(LineEdit);
+					lang_code->set_max_length(3);
+					lang_code->set_tooltip("Language");
+					vb_language->add_child(lang_code);
+				}
+				hb_locale->add_child(vb_language);
+			}
+			{
+				VBoxContainer *vb_script = memnew(VBoxContainer);
+				vb_script->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+				{
+					Label *script_lbl = memnew(Label);
+					script_lbl->set_text(TTR("Script"));
+					vb_script->add_child(script_lbl);
+				}
+				{
+					script_code = memnew(LineEdit);
+					script_code->set_max_length(4);
+					script_code->set_tooltip("Script");
+					vb_script->add_child(script_code);
+				}
+				hb_locale->add_child(vb_script);
+			}
+			{
+				VBoxContainer *vb_country = memnew(VBoxContainer);
+				vb_country->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+				{
+					Label *country_lbl = memnew(Label);
+					country_lbl->set_text(TTR("Country"));
+					vb_country->add_child(country_lbl);
+				}
+				{
+					country_code = memnew(LineEdit);
+					country_code->set_max_length(2);
+					country_code->set_tooltip("Country");
+					vb_country->add_child(country_code);
+				}
+				hb_locale->add_child(vb_country);
+			}
+			{
+				VBoxContainer *vb_variant = memnew(VBoxContainer);
+				vb_variant->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+				{
+					Label *variant_lbl = memnew(Label);
+					variant_lbl->set_text(TTR("Variant"));
+					vb_variant->add_child(variant_lbl);
+				}
+				{
+					variant_code = memnew(LineEdit);
+					variant_code->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+					variant_code->set_placeholder("Variant");
+					variant_code->set_tooltip("Variant");
+					vb_variant->add_child(variant_code);
+				}
+				hb_locale->add_child(vb_variant);
+			}
+		}
+		vb->add_child(hb_locale);
+	}
+	add_child(vb);
+	_update_tree();
+
+	get_ok_button()->set_text(TTR("Select"));
+}

+ 93 - 0
editor/editor_locale_dialog.h

@@ -0,0 +1,93 @@
+/*************************************************************************/
+/*  editor_locale_dialog.h                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef EDITOR_LOCALE_DIALOG_H
+#define EDITOR_LOCALE_DIALOG_H
+
+#include "core/string/translation.h"
+#include "scene/gui/dialogs.h"
+
+class Button;
+class HBoxContainer;
+class VBoxContainer;
+class LineEdit;
+class Tree;
+class OptionButton;
+class UndoRedo;
+
+class EditorLocaleDialog : public ConfirmationDialog {
+	GDCLASS(EditorLocaleDialog, ConfirmationDialog);
+
+	enum LocaleFilter {
+		SHOW_ALL_LOCALES,
+		SHOW_ONLY_SELECTED_LOCALES,
+	};
+
+	HBoxContainer *hb_locale = nullptr;
+	VBoxContainer *vb_script_list = nullptr;
+	OptionButton *filter_mode = nullptr;
+	Button *edit_filters = nullptr;
+	Button *advanced = nullptr;
+	LineEdit *lang_code = nullptr;
+	LineEdit *script_code = nullptr;
+	LineEdit *country_code = nullptr;
+	LineEdit *variant_code = nullptr;
+	Tree *lang_list = nullptr;
+	Tree *script_list = nullptr;
+	Tree *cnt_list = nullptr;
+
+	UndoRedo *undo_redo = nullptr;
+
+	bool locale_set = false;
+	bool updating_lists = false;
+
+protected:
+	static void _bind_methods();
+	virtual void _post_popup() override;
+	virtual void ok_pressed() override;
+
+	void _item_selected();
+	void _filter_lang_option_changed();
+	void _filter_script_option_changed();
+	void _filter_cnt_option_changed();
+	void _filter_mode_changed(int p_mode);
+	void _edit_filters(bool p_checked);
+	void _toggle_advanced(bool p_checked);
+
+	void _update_tree();
+
+public:
+	EditorLocaleDialog();
+
+	void set_locale(const String &p_locale);
+	void popup_locale_dialog();
+};
+
+#endif // EDITOR_LOCALE_DIALOG_H

+ 62 - 0
editor/editor_properties.cpp

@@ -343,6 +343,64 @@ EditorPropertyTextEnum::EditorPropertyTextEnum() {
 	add_focusable(cancel_button);
 }
 
+//////////////////// LOCALE ////////////////////////
+
+void EditorPropertyLocale::_locale_selected(const String &p_locale) {
+	emit_changed(get_edited_property(), p_locale);
+	update_property();
+}
+
+void EditorPropertyLocale::_locale_pressed() {
+	if (!dialog) {
+		dialog = memnew(EditorLocaleDialog);
+		dialog->connect("locale_selected", callable_mp(this, &EditorPropertyLocale::_locale_selected));
+		add_child(dialog);
+	}
+
+	String locale_code = get_edited_object()->get(get_edited_property());
+	dialog->set_locale(locale_code);
+	dialog->popup_locale_dialog();
+}
+
+void EditorPropertyLocale::update_property() {
+	String locale_code = get_edited_object()->get(get_edited_property());
+	locale->set_text(locale_code);
+	locale->set_tooltip(locale_code);
+}
+
+void EditorPropertyLocale::setup(const String &p_hint_text) {
+}
+
+void EditorPropertyLocale::_notification(int p_what) {
+	if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) {
+		locale_edit->set_icon(get_theme_icon(SNAME("Translation"), SNAME("EditorIcons")));
+	}
+}
+
+void EditorPropertyLocale::_locale_focus_exited() {
+	_locale_selected(locale->get_text());
+}
+
+void EditorPropertyLocale::_bind_methods() {
+}
+
+EditorPropertyLocale::EditorPropertyLocale() {
+	HBoxContainer *locale_hb = memnew(HBoxContainer);
+	add_child(locale_hb);
+	locale = memnew(LineEdit);
+	locale_hb->add_child(locale);
+	locale->connect("text_submitted", callable_mp(this, &EditorPropertyLocale::_locale_selected));
+	locale->connect("focus_exited", callable_mp(this, &EditorPropertyLocale::_locale_focus_exited));
+	locale->set_h_size_flags(SIZE_EXPAND_FILL);
+
+	locale_edit = memnew(Button);
+	locale_edit->set_clip_text(true);
+	locale_hb->add_child(locale_edit);
+	add_focusable(locale);
+	dialog = nullptr;
+	locale_edit->connect("pressed", callable_mp(this, &EditorPropertyLocale::_locale_pressed));
+}
+
 ///////////////////// PATH /////////////////////////
 
 void EditorPropertyPath::_set_read_only(bool p_read_only) {
@@ -3379,6 +3437,10 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
 				EditorPropertyClassName *editor = memnew(EditorPropertyClassName);
 				editor->setup("Object", p_hint_text);
 				return editor;
+			} else if (p_hint == PROPERTY_HINT_LOCALE_ID) {
+				EditorPropertyLocale *editor = memnew(EditorPropertyLocale);
+				editor->setup(p_hint_text);
+				return editor;
 			} else if (p_hint == PROPERTY_HINT_DIR || p_hint == PROPERTY_HINT_FILE || p_hint == PROPERTY_HINT_SAVE_FILE || p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE) {
 				Vector<String> extensions = p_hint_text.split(",");
 				bool global = p_hint == PROPERTY_HINT_GLOBAL_DIR || p_hint == PROPERTY_HINT_GLOBAL_FILE;

+ 21 - 0
editor/editor_properties.h

@@ -33,6 +33,7 @@
 
 #include "editor/create_dialog.h"
 #include "editor/editor_inspector.h"
+#include "editor/editor_locale_dialog.h"
 #include "editor/editor_resource_picker.h"
 #include "editor/editor_spin_slider.h"
 #include "editor/property_selector.h"
@@ -153,6 +154,26 @@ public:
 	EditorPropertyPath();
 };
 
+class EditorPropertyLocale : public EditorProperty {
+	GDCLASS(EditorPropertyLocale, EditorProperty);
+	EditorLocaleDialog *dialog;
+	LineEdit *locale;
+	Button *locale_edit;
+
+	void _locale_selected(const String &p_locale);
+	void _locale_pressed();
+	void _locale_focus_exited();
+
+protected:
+	static void _bind_methods();
+	void _notification(int p_what);
+
+public:
+	void setup(const String &p_hit_string);
+	virtual void update_property() override;
+	EditorPropertyLocale();
+};
+
 class EditorPropertyClassName : public EditorProperty {
 	GDCLASS(EditorPropertyClassName, EditorProperty);
 

+ 8 - 8
editor/editor_settings.cpp

@@ -339,7 +339,6 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 	{
 		String lang_hint = "en";
 		String host_lang = OS::get_singleton()->get_locale();
-		host_lang = TranslationServer::standardize_locale(host_lang);
 
 		// Skip locales if Text server lack required features.
 		Vector<String> locales_to_skip;
@@ -365,6 +364,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 		}
 
 		String best;
+		int best_score = 0;
 		for (const String &locale : get_editor_locales()) {
 			// Skip locales which we can't render properly (see above comment).
 			// Test against language code without regional variants (e.g. ur_PK).
@@ -376,16 +376,16 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 			lang_hint += ",";
 			lang_hint += locale;
 
-			if (host_lang == locale) {
-				best = locale;
-			}
-
-			if (best.is_empty() && host_lang.begins_with(locale)) {
+			int score = TranslationServer::get_singleton()->compare_locales(host_lang, locale);
+			if (score >= best_score) {
 				best = locale;
+				best_score = score;
+				if (score == 10) {
+					break; // Exact match, skip the rest.
+				}
 			}
 		}
-
-		if (best.is_empty()) {
+		if (best_score == 0) {
 			best = "en";
 		}
 

+ 10 - 415
editor/import/dynamicfont_import_settings.cpp

@@ -441,398 +441,6 @@ void DynamicFontImportSettings::_add_glyph_range_item(int32_t p_start, int32_t p
 	}
 }
 
-/*************************************************************************/
-/* Languages and scripts                                                 */
-/*************************************************************************/
-
-struct CodeInfo {
-	String name;
-	String code;
-};
-
-static CodeInfo langs[] = {
-	{ U"Custom", U"xx" },
-	{ U"-", U"-" },
-	{ U"Abkhazian", U"ab" },
-	{ U"Afar", U"aa" },
-	{ U"Afrikaans", U"af" },
-	{ U"Akan", U"ak" },
-	{ U"Albanian", U"sq" },
-	{ U"Amharic", U"am" },
-	{ U"Arabic", U"ar" },
-	{ U"Aragonese", U"an" },
-	{ U"Armenian", U"hy" },
-	{ U"Assamese", U"as" },
-	{ U"Avaric", U"av" },
-	{ U"Avestan", U"ae" },
-	{ U"Aymara", U"ay" },
-	{ U"Azerbaijani", U"az" },
-	{ U"Bambara", U"bm" },
-	{ U"Bashkir", U"ba" },
-	{ U"Basque", U"eu" },
-	{ U"Belarusian", U"be" },
-	{ U"Bengali", U"bn" },
-	{ U"Bihari", U"bh" },
-	{ U"Bislama", U"bi" },
-	{ U"Bosnian", U"bs" },
-	{ U"Breton", U"br" },
-	{ U"Bulgarian", U"bg" },
-	{ U"Burmese", U"my" },
-	{ U"Catalan", U"ca" },
-	{ U"Chamorro", U"ch" },
-	{ U"Chechen", U"ce" },
-	{ U"Chichewa", U"ny" },
-	{ U"Chinese", U"zh" },
-	{ U"Chuvash", U"cv" },
-	{ U"Cornish", U"kw" },
-	{ U"Corsican", U"co" },
-	{ U"Cree", U"cr" },
-	{ U"Croatian", U"hr" },
-	{ U"Czech", U"cs" },
-	{ U"Danish", U"da" },
-	{ U"Divehi", U"dv" },
-	{ U"Dutch", U"nl" },
-	{ U"Dzongkha", U"dz" },
-	{ U"English", U"en" },
-	{ U"Esperanto", U"eo" },
-	{ U"Estonian", U"et" },
-	{ U"Ewe", U"ee" },
-	{ U"Faroese", U"fo" },
-	{ U"Fijian", U"fj" },
-	{ U"Finnish", U"fi" },
-	{ U"French", U"fr" },
-	{ U"Fulah", U"ff" },
-	{ U"Galician", U"gl" },
-	{ U"Georgian", U"ka" },
-	{ U"German", U"de" },
-	{ U"Greek", U"el" },
-	{ U"Guarani", U"gn" },
-	{ U"Gujarati", U"gu" },
-	{ U"Haitian", U"ht" },
-	{ U"Hausa", U"ha" },
-	{ U"Hebrew", U"he" },
-	{ U"Herero", U"hz" },
-	{ U"Hindi", U"hi" },
-	{ U"Hiri Motu", U"ho" },
-	{ U"Hungarian", U"hu" },
-	{ U"Interlingua", U"ia" },
-	{ U"Indonesian", U"id" },
-	{ U"Interlingue", U"ie" },
-	{ U"Irish", U"ga" },
-	{ U"Igbo", U"ig" },
-	{ U"Inupiaq", U"ik" },
-	{ U"Ido", U"io" },
-	{ U"Icelandic", U"is" },
-	{ U"Italian", U"it" },
-	{ U"Inuktitut", U"iu" },
-	{ U"Japanese", U"ja" },
-	{ U"Javanese", U"jv" },
-	{ U"Kalaallisut", U"kl" },
-	{ U"Kannada", U"kn" },
-	{ U"Kanuri", U"kr" },
-	{ U"Kashmiri", U"ks" },
-	{ U"Kazakh", U"kk" },
-	{ U"Central Khmer", U"km" },
-	{ U"Kikuyu", U"ki" },
-	{ U"Kinyarwanda", U"rw" },
-	{ U"Kirghiz", U"ky" },
-	{ U"Komi", U"kv" },
-	{ U"Kongo", U"kg" },
-	{ U"Korean", U"ko" },
-	{ U"Kurdish", U"ku" },
-	{ U"Kuanyama", U"kj" },
-	{ U"Latin", U"la" },
-	{ U"Luxembourgish", U"lb" },
-	{ U"Ganda", U"lg" },
-	{ U"Limburgan", U"li" },
-	{ U"Lingala", U"ln" },
-	{ U"Lao", U"lo" },
-	{ U"Lithuanian", U"lt" },
-	{ U"Luba-Katanga", U"lu" },
-	{ U"Latvian", U"lv" },
-	{ U"Man", U"gv" },
-	{ U"Macedonian", U"mk" },
-	{ U"Malagasy", U"mg" },
-	{ U"Malay", U"ms" },
-	{ U"Malayalam", U"ml" },
-	{ U"Maltese", U"mt" },
-	{ U"Maori", U"mi" },
-	{ U"Marathi", U"mr" },
-	{ U"Marshallese", U"mh" },
-	{ U"Mongolian", U"mn" },
-	{ U"Nauru", U"na" },
-	{ U"Navajo", U"nv" },
-	{ U"North Ndebele", U"nd" },
-	{ U"Nepali", U"ne" },
-	{ U"Ndonga", U"ng" },
-	{ U"Norwegian Bokmål", U"nb" },
-	{ U"Norwegian Nynorsk", U"nn" },
-	{ U"Norwegian", U"no" },
-	{ U"Sichuan Yi, Nuosu", U"ii" },
-	{ U"South Ndebele", U"nr" },
-	{ U"Occitan", U"oc" },
-	{ U"Ojibwa", U"oj" },
-	{ U"Church Slavic", U"cu" },
-	{ U"Oromo", U"om" },
-	{ U"Oriya", U"or" },
-	{ U"Ossetian", U"os" },
-	{ U"Punjabi", U"pa" },
-	{ U"Pali", U"pi" },
-	{ U"Persian", U"fa" },
-	{ U"Polish", U"pl" },
-	{ U"Pashto", U"ps" },
-	{ U"Portuguese", U"pt" },
-	{ U"Quechua", U"qu" },
-	{ U"Romansh", U"rm" },
-	{ U"Rundi", U"rn" },
-	{ U"Romanian", U"ro" },
-	{ U"Russian", U"ru" },
-	{ U"Sanskrit", U"sa" },
-	{ U"Sardinian", U"sc" },
-	{ U"Sindhi", U"sd" },
-	{ U"Northern Sami", U"se" },
-	{ U"Samoan", U"sm" },
-	{ U"Sango", U"sg" },
-	{ U"Serbian", U"sr" },
-	{ U"Gaelic", U"gd" },
-	{ U"Shona", U"sn" },
-	{ U"Sinhala", U"si" },
-	{ U"Slovak", U"sk" },
-	{ U"Slovenian", U"sl" },
-	{ U"Somali", U"so" },
-	{ U"Southern Sotho", U"st" },
-	{ U"Spanish", U"es" },
-	{ U"Sundanese", U"su" },
-	{ U"Swahili", U"sw" },
-	{ U"Swati", U"ss" },
-	{ U"Swedish", U"sv" },
-	{ U"Tamil", U"ta" },
-	{ U"Telugu", U"te" },
-	{ U"Tajik", U"tg" },
-	{ U"Thai", U"th" },
-	{ U"Tigrinya", U"ti" },
-	{ U"Tibetan", U"bo" },
-	{ U"Turkmen", U"tk" },
-	{ U"Tagalog", U"tl" },
-	{ U"Tswana", U"tn" },
-	{ U"Tonga", U"to" },
-	{ U"Turkish", U"tr" },
-	{ U"Tsonga", U"ts" },
-	{ U"Tatar", U"tt" },
-	{ U"Twi", U"tw" },
-	{ U"Tahitian", U"ty" },
-	{ U"Uighur", U"ug" },
-	{ U"Ukrainian", U"uk" },
-	{ U"Urdu", U"ur" },
-	{ U"Uzbek", U"uz" },
-	{ U"Venda", U"ve" },
-	{ U"Vietnamese", U"vi" },
-	{ U"Volapük", U"vo" },
-	{ U"Walloon", U"wa" },
-	{ U"Welsh", U"cy" },
-	{ U"Wolof", U"wo" },
-	{ U"Western Frisian", U"fy" },
-	{ U"Xhosa", U"xh" },
-	{ U"Yiddish", U"yi" },
-	{ U"Yoruba", U"yo" },
-	{ U"Zhuang", U"za" },
-	{ U"Zulu", U"zu" },
-	{ String(), String() }
-};
-
-static CodeInfo scripts[] = {
-	{ U"Custom", U"Qaaa" },
-	{ U"-", U"-" },
-	{ U"Adlam", U"Adlm" },
-	{ U"Afaka", U"Afak" },
-	{ U"Caucasian Albanian", U"Aghb" },
-	{ U"Ahom", U"Ahom" },
-	{ U"Arabic", U"Arab" },
-	{ U"Imperial Aramaic", U"Armi" },
-	{ U"Armenian", U"Armn" },
-	{ U"Avestan", U"Avst" },
-	{ U"Balinese", U"Bali" },
-	{ U"Bamum", U"Bamu" },
-	{ U"Bassa Vah", U"Bass" },
-	{ U"Batak", U"Batk" },
-	{ U"Bengali", U"Beng" },
-	{ U"Bhaiksuki", U"Bhks" },
-	{ U"Blissymbols", U"Blis" },
-	{ U"Bopomofo", U"Bopo" },
-	{ U"Brahmi", U"Brah" },
-	{ U"Braille", U"Brai" },
-	{ U"Buginese", U"Bugi" },
-	{ U"Buhid", U"Buhd" },
-	{ U"Chakma", U"Cakm" },
-	{ U"Unified Canadian Aboriginal", U"Cans" },
-	{ U"Carian", U"Cari" },
-	{ U"Cham", U"Cham" },
-	{ U"Cherokee", U"Cher" },
-	{ U"Chorasmian", U"Chrs" },
-	{ U"Cirth", U"Cirt" },
-	{ U"Coptic", U"Copt" },
-	{ U"Cypro-Minoan", U"Cpmn" },
-	{ U"Cypriot", U"Cprt" },
-	{ U"Cyrillic", U"Cyrl" },
-	{ U"Devanagari", U"Deva" },
-	{ U"Dives Akuru", U"Diak" },
-	{ U"Dogra", U"Dogr" },
-	{ U"Deseret", U"Dsrt" },
-	{ U"Duployan", U"Dupl" },
-	{ U"Egyptian demotic", U"Egyd" },
-	{ U"Egyptian hieratic", U"Egyh" },
-	{ U"Egyptian hieroglyphs", U"Egyp" },
-	{ U"Elbasan", U"Elba" },
-	{ U"Elymaic", U"Elym" },
-	{ U"Ethiopic", U"Ethi" },
-	{ U"Khutsuri", U"Geok" },
-	{ U"Georgian", U"Geor" },
-	{ U"Glagolitic", U"Glag" },
-	{ U"Gunjala Gondi", U"Gong" },
-	{ U"Masaram Gondi", U"Gonm" },
-	{ U"Gothic", U"Goth" },
-	{ U"Grantha", U"Gran" },
-	{ U"Greek", U"Grek" },
-	{ U"Gujarati", U"Gujr" },
-	{ U"Gurmukhi", U"Guru" },
-	{ U"Hangul", U"Hang" },
-	{ U"Han", U"Hani" },
-	{ U"Hanunoo", U"Hano" },
-	{ U"Hatran", U"Hatr" },
-	{ U"Hebrew", U"Hebr" },
-	{ U"Hiragana", U"Hira" },
-	{ U"Anatolian Hieroglyphs", U"Hluw" },
-	{ U"Pahawh Hmong", U"Hmng" },
-	{ U"Nyiakeng Puachue Hmong", U"Hmnp" },
-	{ U"Old Hungarian", U"Hung" },
-	{ U"Indus", U"Inds" },
-	{ U"Old Italic", U"Ital" },
-	{ U"Javanese", U"Java" },
-	{ U"Jurchen", U"Jurc" },
-	{ U"Kayah Li", U"Kali" },
-	{ U"Katakana", U"Kana" },
-	{ U"Kharoshthi", U"Khar" },
-	{ U"Khmer", U"Khmr" },
-	{ U"Khojki", U"Khoj" },
-	{ U"Khitan large script", U"Kitl" },
-	{ U"Khitan small script", U"Kits" },
-	{ U"Kannada", U"Knda" },
-	{ U"Kpelle", U"Kpel" },
-	{ U"Kaithi", U"Kthi" },
-	{ U"Tai Tham", U"Lana" },
-	{ U"Lao", U"Laoo" },
-	{ U"Latin", U"Latn" },
-	{ U"Leke", U"Leke" },
-	{ U"Lepcha", U"Lepc" },
-	{ U"Limbu", U"Limb" },
-	{ U"Linear A", U"Lina" },
-	{ U"Linear B", U"Linb" },
-	{ U"Lisu", U"Lisu" },
-	{ U"Loma", U"Loma" },
-	{ U"Lycian", U"Lyci" },
-	{ U"Lydian", U"Lydi" },
-	{ U"Mahajani", U"Mahj" },
-	{ U"Makasar", U"Maka" },
-	{ U"Mandaic", U"Mand" },
-	{ U"Manichaean", U"Mani" },
-	{ U"Marchen", U"Marc" },
-	{ U"Mayan Hieroglyphs", U"Maya" },
-	{ U"Medefaidrin", U"Medf" },
-	{ U"Mende Kikakui", U"Mend" },
-	{ U"Meroitic Cursive", U"Merc" },
-	{ U"Meroitic Hieroglyphs", U"Mero" },
-	{ U"Malayalam", U"Mlym" },
-	{ U"Modi", U"Modi" },
-	{ U"Mongolian", U"Mong" },
-	{ U"Moon", U"Moon" },
-	{ U"Mro", U"Mroo" },
-	{ U"Meitei Mayek", U"Mtei" },
-	{ U"Multani", U"Mult" },
-	{ U"Myanmar (Burmese)", U"Mymr" },
-	{ U"Nandinagari", U"Nand" },
-	{ U"Old North Arabian", U"Narb" },
-	{ U"Nabataean", U"Nbat" },
-	{ U"Newa", U"Newa" },
-	{ U"Naxi Dongba", U"Nkdb" },
-	{ U"Nakhi Geba", U"Nkgb" },
-	{ U"N’Ko", U"Nkoo" },
-	{ U"Nüshu", U"Nshu" },
-	{ U"Ogham", U"Ogam" },
-	{ U"Ol Chiki", U"Olck" },
-	{ U"Old Turkic", U"Orkh" },
-	{ U"Oriya", U"Orya" },
-	{ U"Osage", U"Osge" },
-	{ U"Osmanya", U"Osma" },
-	{ U"Old Uyghur", U"Ougr" },
-	{ U"Palmyrene", U"Palm" },
-	{ U"Pau Cin Hau", U"Pauc" },
-	{ U"Proto-Cuneiform", U"Pcun" },
-	{ U"Proto-Elamite", U"Pelm" },
-	{ U"Old Permic", U"Perm" },
-	{ U"Phags-pa", U"Phag" },
-	{ U"Inscriptional Pahlavi", U"Phli" },
-	{ U"Psalter Pahlavi", U"Phlp" },
-	{ U"Book Pahlavi", U"Phlv" },
-	{ U"Phoenician", U"Phnx" },
-	{ U"Klingon", U"Piqd" },
-	{ U"Miao", U"Plrd" },
-	{ U"Inscriptional Parthian", U"Prti" },
-	{ U"Proto-Sinaitic", U"Psin" },
-	{ U"Ranjana", U"Ranj" },
-	{ U"Rejang", U"Rjng" },
-	{ U"Hanifi Rohingya", U"Rohg" },
-	{ U"Rongorongo", U"Roro" },
-	{ U"Runic", U"Runr" },
-	{ U"Samaritan", U"Samr" },
-	{ U"Sarati", U"Sara" },
-	{ U"Old South Arabian", U"Sarb" },
-	{ U"Saurashtra", U"Saur" },
-	{ U"SignWriting", U"Sgnw" },
-	{ U"Shavian", U"Shaw" },
-	{ U"Sharada", U"Shrd" },
-	{ U"Shuishu", U"Shui" },
-	{ U"Siddham", U"Sidd" },
-	{ U"Khudawadi", U"Sind" },
-	{ U"Sinhala", U"Sinh" },
-	{ U"Sogdian", U"Sogd" },
-	{ U"Old Sogdian", U"Sogo" },
-	{ U"Sora Sompeng", U"Sora" },
-	{ U"Soyombo", U"Soyo" },
-	{ U"Sundanese", U"Sund" },
-	{ U"Syloti Nagri", U"Sylo" },
-	{ U"Syriac", U"Syrc" },
-	{ U"Tagbanwa", U"Tagb" },
-	{ U"Takri", U"Takr" },
-	{ U"Tai Le", U"Tale" },
-	{ U"New Tai Lue", U"Talu" },
-	{ U"Tamil", U"Taml" },
-	{ U"Tangut", U"Tang" },
-	{ U"Tai Viet", U"Tavt" },
-	{ U"Telugu", U"Telu" },
-	{ U"Tengwar", U"Teng" },
-	{ U"Tifinagh", U"Tfng" },
-	{ U"Tagalog", U"Tglg" },
-	{ U"Thaana", U"Thaa" },
-	{ U"Thai", U"Thai" },
-	{ U"Tibetan", U"Tibt" },
-	{ U"Tirhuta", U"Tirh" },
-	{ U"Tangsa", U"Tnsa" },
-	{ U"Toto", U"Toto" },
-	{ U"Ugaritic", U"Ugar" },
-	{ U"Vai", U"Vaii" },
-	{ U"Visible Speech", U"Visp" },
-	{ U"Vithkuqi", U"Vith" },
-	{ U"Warang Citi", U"Wara" },
-	{ U"Wancho", U"Wcho" },
-	{ U"Woleai", U"Wole" },
-	{ U"Old Persian", U"Xpeo" },
-	{ U"Cuneiform", U"Xsux" },
-	{ U"Yezidi", U"Yezi" },
-	{ U"Yi", U"Yiii" },
-	{ U"Zanabazar Square", U"Zanb" },
-	{ String(), String() }
-};
-
 /*************************************************************************/
 /* Page 1 callbacks: Rendering Options                                   */
 /*************************************************************************/
@@ -1159,19 +767,17 @@ void DynamicFontImportSettings::_range_update(int32_t p_start, int32_t p_end) {
 /*************************************************************************/
 
 void DynamicFontImportSettings::_lang_add() {
-	menu_langs->set_position(lang_list->get_screen_position() + lang_list->get_local_mouse_position());
-	menu_langs->reset_size();
-	menu_langs->popup();
+	locale_select->popup_locale_dialog();
 }
 
-void DynamicFontImportSettings::_lang_add_item(int p_option) {
+void DynamicFontImportSettings::_lang_add_item(const String &p_locale) {
 	TreeItem *lang_item = lang_list->create_item(lang_list_root);
 	ERR_FAIL_NULL(lang_item);
 
 	lang_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	lang_item->set_editable(0, true);
 	lang_item->set_checked(0, false);
-	lang_item->set_text(1, langs[p_option].code);
+	lang_item->set_text(1, p_locale);
 	lang_item->set_editable(1, true);
 	lang_item->add_button(2, lang_list->get_theme_icon("Remove", "EditorIcons"), BUTTON_REMOVE_VAR, false, TTR("Remove"));
 	lang_item->set_button_color(2, 0, Color(1, 1, 1, 0.75));
@@ -1230,7 +836,7 @@ void DynamicFontImportSettings::_script_add_item(int p_option) {
 	script_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 	script_item->set_editable(0, true);
 	script_item->set_checked(0, false);
-	script_item->set_text(1, scripts[p_option].code);
+	script_item->set_text(1, script_codes[p_option]);
 	script_item->set_editable(1, true);
 	script_item->add_button(2, lang_list->get_theme_icon("Remove", "EditorIcons"), BUTTON_REMOVE_VAR, false, TTR("Remove"));
 	script_item->set_button_color(2, 0, Color(1, 1, 1, 0.75));
@@ -1688,26 +1294,15 @@ DynamicFontImportSettings::DynamicFontImportSettings() {
 
 	// Popup menus
 
-	menu_langs = memnew(PopupMenu);
-	menu_langs->set_name("Language");
-	for (int i = 0; !langs[i].name.is_empty(); i++) {
-		if (langs[i].name == "-") {
-			menu_langs->add_separator();
-		} else {
-			menu_langs->add_item(langs[i].name + " (" + langs[i].code + ")", i);
-		}
-	}
-	add_child(menu_langs);
-	menu_langs->connect("id_pressed", callable_mp(this, &DynamicFontImportSettings::_lang_add_item));
+	locale_select = memnew(EditorLocaleDialog);
+	locale_select->connect("locale_selected", callable_mp(this, &DynamicFontImportSettings::_lang_add_item));
+	add_child(locale_select);
 
 	menu_scripts = memnew(PopupMenu);
 	menu_scripts->set_name("Script");
-	for (int i = 0; !scripts[i].name.is_empty(); i++) {
-		if (scripts[i].name == "-") {
-			menu_scripts->add_separator();
-		} else {
-			menu_scripts->add_item(scripts[i].name + " (" + scripts[i].code + ")", i);
-		}
+	script_codes = TranslationServer::get_singleton()->get_all_scripts();
+	for (int i = 0; i < script_codes.size(); i++) {
+		menu_scripts->add_item(TranslationServer::get_singleton()->get_script_name(script_codes[i]) + " (" + script_codes[i] + ")", i);
 	}
 	add_child(menu_scripts);
 	menu_scripts->connect("id_pressed", callable_mp(this, &DynamicFontImportSettings::_script_add_item));

+ 5 - 2
editor/import/dynamicfont_import_settings.h

@@ -33,6 +33,7 @@
 
 #include "editor/editor_file_dialog.h"
 #include "editor/editor_inspector.h"
+#include "editor/editor_locale_dialog.h"
 
 #include "editor/import/resource_importer_dynamicfont.h"
 
@@ -67,6 +68,9 @@ class DynamicFontImportSettings : public ConfirmationDialog {
 	List<ResourceImporter::ImportOption> options_variations;
 	List<ResourceImporter::ImportOption> options_general;
 
+	EditorLocaleDialog *locale_select;
+	Vector<String> script_codes;
+
 	// Root layout
 	Label *label_warn = nullptr;
 	TabContainer *main_pages = nullptr;
@@ -122,7 +126,6 @@ class DynamicFontImportSettings : public ConfirmationDialog {
 	Button *add_script = nullptr;
 	Button *add_ot = nullptr;
 
-	PopupMenu *menu_langs = nullptr;
 	PopupMenu *menu_scripts = nullptr;
 	PopupMenu *menu_ot = nullptr;
 	PopupMenu *menu_ot_ss = nullptr;
@@ -142,7 +145,7 @@ class DynamicFontImportSettings : public ConfirmationDialog {
 	Label *label_ot = nullptr;
 
 	void _lang_add();
-	void _lang_add_item(int p_option);
+	void _lang_add_item(const String &p_locale);
 	void _lang_remove(Object *p_item, int p_column, int p_id);
 
 	void _script_add();

+ 1 - 2
editor/import/resource_importer_csv_translation.cpp

@@ -99,8 +99,7 @@ Error ResourceImporterCSVTranslation::import(const String &p_source_file, const
 	Vector<Ref<Translation>> translations;
 
 	for (int i = 1; i < line.size(); i++) {
-		String locale = line[i];
-		ERR_FAIL_COND_V_MSG(!TranslationServer::is_locale_valid(locale), ERR_PARSE_ERROR, "Error importing CSV translation: '" + locale + "' is not a valid locale.");
+		String locale = TranslationServer::get_singleton()->standardize_locale(line[i]);
 
 		locales.push_back(locale);
 		Ref<Translation> translation;

+ 31 - 220
editor/localization_editor.cpp

@@ -32,6 +32,7 @@
 
 #include "core/string/translation.h"
 #include "editor_node.h"
+#include "editor_scale.h"
 #include "editor_translation_parser.h"
 #include "pot_generator.h"
 #include "scene/gui/control.h"
@@ -175,10 +176,27 @@ void LocalizationEditor::_translation_res_select() {
 	if (updating_translations) {
 		return;
 	}
-
 	call_deferred(SNAME("update_translations"));
 }
 
+void LocalizationEditor::_translation_res_option_popup(bool p_arrow_clicked) {
+	TreeItem *ed = translation_remap_options->get_edited();
+	ERR_FAIL_COND(!ed);
+
+	locale_select->set_locale(ed->get_tooltip(1));
+	locale_select->popup_locale_dialog();
+}
+
+void LocalizationEditor::_translation_res_option_selected(const String &p_locale) {
+	TreeItem *ed = translation_remap_options->get_edited();
+	ERR_FAIL_COND(!ed);
+
+	ed->set_text(1, TranslationServer::get_singleton()->get_locale_name(p_locale));
+	ed->set_tooltip(1, p_locale);
+
+	LocalizationEditor::_translation_res_option_changed();
+}
+
 void LocalizationEditor::_translation_res_option_changed() {
 	if (updating_translations) {
 		return;
@@ -198,20 +216,11 @@ void LocalizationEditor::_translation_res_option_changed() {
 	String key = k->get_metadata(0);
 	int idx = ed->get_metadata(0);
 	String path = ed->get_metadata(1);
-	int which = ed->get_range(1);
-
-	Vector<String> langs = TranslationServer::get_all_locales();
-
-	ERR_FAIL_INDEX(which, langs.size());
+	String locale = ed->get_tooltip(1);
 
 	ERR_FAIL_COND(!remaps.has(key));
 	PackedStringArray r = remaps[key];
-	ERR_FAIL_INDEX(idx, r.size());
-	if (translation_locales_idxs_remap.size() > which) {
-		r.set(idx, path + ":" + langs[translation_locales_idxs_remap[which]]);
-	} else {
-		r.set(idx, path + ":" + langs[which]);
-	}
+	r.set(idx, path + ":" + locale);
 	remaps[key] = r;
 
 	updating_translations = true;
@@ -289,86 +298,6 @@ void LocalizationEditor::_translation_res_option_delete(Object *p_item, int p_co
 	undo_redo->commit_action();
 }
 
-void LocalizationEditor::_translation_filter_option_changed() {
-	int sel_id = translation_locale_filter_mode->get_selected_id();
-	TreeItem *t = translation_filter->get_edited();
-	String locale = t->get_tooltip(0);
-	bool checked = t->is_checked(0);
-
-	Variant prev;
-	Array f_locales_all;
-
-	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/locale_filter")) {
-		f_locales_all = ProjectSettings::get_singleton()->get("internationalization/locale/locale_filter");
-		prev = f_locales_all;
-
-		if (f_locales_all.size() != 2) {
-			f_locales_all.clear();
-			f_locales_all.append(sel_id);
-			f_locales_all.append(Array());
-		}
-	} else {
-		f_locales_all.append(sel_id);
-		f_locales_all.append(Array());
-	}
-
-	Array f_locales = f_locales_all[1];
-	int l_idx = f_locales.find(locale);
-
-	if (checked) {
-		if (l_idx == -1) {
-			f_locales.append(locale);
-		}
-	} else {
-		if (l_idx != -1) {
-			f_locales.remove_at(l_idx);
-		}
-	}
-
-	f_locales.sort();
-
-	undo_redo->create_action(TTR("Changed Locale Filter"));
-	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter", f_locales_all);
-	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter", prev);
-	undo_redo->add_do_method(this, "update_translations");
-	undo_redo->add_undo_method(this, "update_translations");
-	undo_redo->add_do_method(this, "emit_signal", localization_changed);
-	undo_redo->add_undo_method(this, "emit_signal", localization_changed);
-	undo_redo->commit_action();
-}
-
-void LocalizationEditor::_translation_filter_mode_changed(int p_mode) {
-	int sel_id = translation_locale_filter_mode->get_selected_id();
-
-	Variant prev;
-	Array f_locales_all;
-
-	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/locale_filter")) {
-		f_locales_all = ProjectSettings::get_singleton()->get("internationalization/locale/locale_filter");
-		prev = f_locales_all;
-
-		if (f_locales_all.size() != 2) {
-			f_locales_all.clear();
-			f_locales_all.append(sel_id);
-			f_locales_all.append(Array());
-		} else {
-			f_locales_all[0] = sel_id;
-		}
-	} else {
-		f_locales_all.append(sel_id);
-		f_locales_all.append(Array());
-	}
-
-	undo_redo->create_action(TTR("Changed Locale Filter Mode"));
-	undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter", f_locales_all);
-	undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/locale_filter", prev);
-	undo_redo->add_do_method(this, "update_translations");
-	undo_redo->add_undo_method(this, "update_translations");
-	undo_redo->add_do_method(this, "emit_signal", localization_changed);
-	undo_redo->add_undo_method(this, "emit_signal", localization_changed);
-	undo_redo->commit_action();
-}
-
 void LocalizationEditor::_pot_add(const PackedStringArray &p_paths) {
 	PackedStringArray pot_translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations_pot_files");
 	for (int i = 0; i < p_paths.size(); i++) {
@@ -452,64 +381,6 @@ void LocalizationEditor::update_translations() {
 		}
 	}
 
-	Vector<String> langs = TranslationServer::get_all_locales();
-	Vector<String> names = TranslationServer::get_all_locale_names();
-
-	// Update filter tab
-	Array l_filter_all;
-
-	bool is_arr_empty = true;
-	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/locale_filter")) {
-		l_filter_all = ProjectSettings::get_singleton()->get("internationalization/locale/locale_filter");
-
-		if (l_filter_all.size() == 2) {
-			translation_locale_filter_mode->select(l_filter_all[0]);
-			is_arr_empty = false;
-		}
-	}
-	if (is_arr_empty) {
-		l_filter_all.append(0);
-		l_filter_all.append(Array());
-		translation_locale_filter_mode->select(0);
-	}
-
-	int filter_mode = l_filter_all[0];
-	Array l_filter = l_filter_all[1];
-
-	int s = names.size();
-	bool is_short_list_when_show_all_selected = filter_mode == SHOW_ALL_LOCALES && translation_filter_treeitems.size() < s;
-	bool is_full_list_when_show_only_selected = filter_mode == SHOW_ONLY_SELECTED_LOCALES && translation_filter_treeitems.size() == s;
-	bool should_recreate_locales_list = is_short_list_when_show_all_selected || is_full_list_when_show_only_selected;
-
-	if (!translation_locales_list_created || should_recreate_locales_list) {
-		translation_locales_list_created = true;
-		translation_filter->clear();
-		root = translation_filter->create_item(nullptr);
-		translation_filter->set_hide_root(true);
-		translation_filter_treeitems.clear();
-		for (int i = 0; i < s; i++) {
-			String n = names[i];
-			String l = langs[i];
-			bool is_checked = l_filter.has(l);
-			if (filter_mode == SHOW_ONLY_SELECTED_LOCALES && !is_checked) {
-				continue;
-			}
-
-			TreeItem *t = translation_filter->create_item(root);
-			t->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
-			t->set_text(0, vformat("[%s] %s", l, n));
-			t->set_editable(0, true);
-			t->set_tooltip(0, l);
-			t->set_checked(0, is_checked);
-			translation_filter_treeitems.push_back(t);
-		}
-	} else {
-		for (int i = 0; i < translation_filter_treeitems.size(); i++) {
-			TreeItem *t = translation_filter_treeitems[i];
-			t->set_checked(0, l_filter.has(t->get_tooltip(0)));
-		}
-	}
-
 	// Update translation remaps.
 	String remap_selected;
 	if (translation_remap->get_selected()) {
@@ -524,32 +395,6 @@ void LocalizationEditor::update_translations() {
 	translation_remap_options->set_hide_root(true);
 	translation_res_option_add_button->set_disabled(true);
 
-	translation_locales_idxs_remap.clear();
-	translation_locales_idxs_remap.resize(l_filter.size());
-	int fl_idx_count = translation_locales_idxs_remap.size();
-
-	String langnames = "";
-	int l_idx = 0;
-	for (int i = 0; i < names.size(); i++) {
-		if (filter_mode == SHOW_ONLY_SELECTED_LOCALES && fl_idx_count != 0) {
-			if (l_filter.size() > 0) {
-				if (l_filter.find(langs[i]) != -1) {
-					if (langnames.length() > 0) {
-						langnames += ",";
-					}
-					langnames += vformat("[%s] %s", langs[i], names[i]);
-					translation_locales_idxs_remap.write[l_idx] = i;
-					l_idx++;
-				}
-			}
-		} else {
-			if (i > 0) {
-				langnames += ",";
-			}
-			langnames += vformat("[%s] %s", langs[i], names[i]);
-		}
-	}
-
 	if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
 		Dictionary remaps = ProjectSettings::get_singleton()->get("internationalization/locale/translation_remaps");
 		List<Variant> rk;
@@ -584,21 +429,11 @@ void LocalizationEditor::update_translations() {
 					t2->set_tooltip(0, path);
 					t2->set_metadata(0, j);
 					t2->add_button(0, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")), 0, false, TTR("Remove"));
-					t2->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);
-					t2->set_text(1, langnames);
+					t2->set_cell_mode(1, TreeItem::CELL_MODE_CUSTOM);
+					t2->set_text(1, TranslationServer::get_singleton()->get_locale_name(locale));
 					t2->set_editable(1, true);
 					t2->set_metadata(1, path);
-					int idx = langs.find(locale);
-					if (idx < 0) {
-						idx = 0;
-					}
-
-					int f_idx = translation_locales_idxs_remap.find(idx);
-					if (f_idx != -1 && fl_idx_count > 0 && filter_mode == SHOW_ONLY_SELECTED_LOCALES) {
-						t2->set_range(1, f_idx);
-					} else {
-						t2->set_range(1, idx);
-					}
+					t2->set_tooltip(1, locale);
 				}
 			}
 		}
@@ -637,9 +472,6 @@ LocalizationEditor::LocalizationEditor() {
 	updating_translations = false;
 	localization_changed = "localization_changed";
 
-	translation_locales_idxs_remap = Vector<int>();
-	translation_locales_list_created = false;
-
 	TabContainer *translations = memnew(TabContainer);
 	translations->set_tab_alignment(TabContainer::ALIGNMENT_LEFT);
 	translations->set_v_size_flags(Control::SIZE_EXPAND_FILL);
@@ -669,6 +501,10 @@ LocalizationEditor::LocalizationEditor() {
 		translation_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 		tmc->add_child(translation_list);
 
+		locale_select = memnew(EditorLocaleDialog);
+		locale_select->connect("locale_selected", callable_mp(this, &LocalizationEditor::_translation_res_option_selected));
+		add_child(locale_select);
+
 		translation_file_open = memnew(EditorFileDialog);
 		translation_file_open->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
 		translation_file_open->connect("files_selected", callable_mp(this, &LocalizationEditor::_translation_add));
@@ -731,10 +567,11 @@ LocalizationEditor::LocalizationEditor() {
 		translation_remap_options->set_column_expand(0, true);
 		translation_remap_options->set_column_clip_content(0, true);
 		translation_remap_options->set_column_expand(1, false);
-		translation_remap_options->set_column_clip_content(1, true);
-		translation_remap_options->set_column_custom_minimum_width(1, 200);
+		translation_remap_options->set_column_clip_content(1, false);
+		translation_remap_options->set_column_custom_minimum_width(1, 250);
 		translation_remap_options->connect("item_edited", callable_mp(this, &LocalizationEditor::_translation_res_option_changed));
 		translation_remap_options->connect("button_pressed", callable_mp(this, &LocalizationEditor::_translation_res_option_delete));
+		translation_remap_options->connect("custom_popup_edited", callable_mp(this, &LocalizationEditor::_translation_res_option_popup));
 		tmc->add_child(translation_remap_options);
 
 		translation_res_option_file_open_dialog = memnew(EditorFileDialog);
@@ -743,32 +580,6 @@ LocalizationEditor::LocalizationEditor() {
 		add_child(translation_res_option_file_open_dialog);
 	}
 
-	{
-		VBoxContainer *tvb = memnew(VBoxContainer);
-		tvb->set_name(TTR("Locales Filter"));
-		translations->add_child(tvb);
-
-		VBoxContainer *tmc = memnew(VBoxContainer);
-		tmc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-		tvb->add_child(tmc);
-
-		translation_locale_filter_mode = memnew(OptionButton);
-		translation_locale_filter_mode->add_item(TTR("Show All Locales"), SHOW_ALL_LOCALES);
-		translation_locale_filter_mode->add_item(TTR("Show Selected Locales Only"), SHOW_ONLY_SELECTED_LOCALES);
-		translation_locale_filter_mode->select(0);
-		translation_locale_filter_mode->connect("item_selected", callable_mp(this, &LocalizationEditor::_translation_filter_mode_changed));
-		tmc->add_margin_child(TTR("Filter mode:"), translation_locale_filter_mode);
-
-		Label *l = memnew(Label(TTR("Locales:")));
-		l->set_theme_type_variation("HeaderSmall");
-		tmc->add_child(l);
-		translation_filter = memnew(Tree);
-		translation_filter->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-		translation_filter->set_columns(1);
-		translation_filter->connect("item_edited", callable_mp(this, &LocalizationEditor::_translation_filter_option_changed));
-		tmc->add_child(translation_filter);
-	}
-
 	{
 		VBoxContainer *tvb = memnew(VBoxContainer);
 		tvb->set_name(TTR("POT Generation"));

+ 4 - 13
editor/localization_editor.h

@@ -33,18 +33,15 @@
 
 #include "core/object/undo_redo.h"
 #include "editor_file_dialog.h"
+#include "editor_locale_dialog.h"
 #include "scene/gui/tree.h"
 
 class LocalizationEditor : public VBoxContainer {
 	GDCLASS(LocalizationEditor, VBoxContainer);
 
-	enum LocaleFilter {
-		SHOW_ALL_LOCALES,
-		SHOW_ONLY_SELECTED_LOCALES,
-	};
-
 	Tree *translation_list;
 
+	EditorLocaleDialog *locale_select;
 	EditorFileDialog *translation_file_open;
 
 	Button *translation_res_option_add_button;
@@ -52,11 +49,6 @@ class LocalizationEditor : public VBoxContainer {
 	EditorFileDialog *translation_res_option_file_open_dialog;
 	Tree *translation_remap;
 	Tree *translation_remap_options;
-	Tree *translation_filter;
-	bool translation_locales_list_created;
-	OptionButton *translation_locale_filter_mode;
-	Vector<TreeItem *> translation_filter_treeitems;
-	Vector<int> translation_locales_idxs_remap;
 
 	Tree *translation_pot_list;
 	EditorFileDialog *pot_file_open_dialog;
@@ -78,9 +70,8 @@ class LocalizationEditor : public VBoxContainer {
 	void _translation_res_option_add(const PackedStringArray &p_paths);
 	void _translation_res_option_changed();
 	void _translation_res_option_delete(Object *p_item, int p_column, int p_button);
-
-	void _translation_filter_option_changed();
-	void _translation_filter_mode_changed(int p_mode);
+	void _translation_res_option_popup(bool p_arrow_clicked);
+	void _translation_res_option_selected(const String &p_locale);
 
 	void _pot_add(const PackedStringArray &p_paths);
 	void _pot_delete(Object *p_item, int p_column, int p_button);

+ 22 - 3
editor/property_editor.cpp

@@ -506,12 +506,16 @@ bool CustomPropertyEditor::edit(Object *p_owner, const String &p_name, Variant::
 
 		} break;
 		case Variant::STRING: {
-			if (hint == PROPERTY_HINT_FILE || hint == PROPERTY_HINT_GLOBAL_FILE) {
+			if (hint == PROPERTY_HINT_LOCALE_ID) {
+				List<String> names;
+				names.push_back(TTR("Locale..."));
+				names.push_back(TTR("Clear"));
+				config_action_buttons(names);
+			} else if (hint == PROPERTY_HINT_FILE || hint == PROPERTY_HINT_GLOBAL_FILE) {
 				List<String> names;
 				names.push_back(TTR("File..."));
 				names.push_back(TTR("Clear"));
 				config_action_buttons(names);
-
 			} else if (hint == PROPERTY_HINT_DIR || hint == PROPERTY_HINT_GLOBAL_DIR) {
 				List<String> names;
 				names.push_back(TTR("Dir..."));
@@ -1034,6 +1038,14 @@ void CustomPropertyEditor::_file_selected(String p_file) {
 	}
 }
 
+void CustomPropertyEditor::_locale_selected(String p_locale) {
+	if (type == Variant::STRING && hint == PROPERTY_HINT_LOCALE_ID) {
+		v = p_locale;
+		emit_signal(SNAME("variant_changed"));
+		hide();
+	}
+}
+
 void CustomPropertyEditor::_type_create_selected(int p_idx) {
 	if (type == Variant::INT || type == Variant::FLOAT) {
 		float newval = 0;
@@ -1177,7 +1189,8 @@ void CustomPropertyEditor::_action_pressed(int p_which) {
 		case Variant::STRING: {
 			if (hint == PROPERTY_HINT_MULTILINE_TEXT) {
 				hide();
-
+			} else if (hint == PROPERTY_HINT_LOCALE_ID) {
+				locale->popup_locale_dialog();
 			} else if (hint == PROPERTY_HINT_FILE || hint == PROPERTY_HINT_GLOBAL_FILE) {
 				if (p_which == 0) {
 					if (hint == PROPERTY_HINT_FILE) {
@@ -1821,6 +1834,12 @@ CustomPropertyEditor::CustomPropertyEditor() {
 	file->connect("file_selected", callable_mp(this, &CustomPropertyEditor::_file_selected));
 	file->connect("dir_selected", callable_mp(this, &CustomPropertyEditor::_file_selected));
 
+	locale = memnew(EditorLocaleDialog);
+	value_vbox->add_child(locale);
+	locale->hide();
+
+	locale->connect("locale_selected", callable_mp(this, &CustomPropertyEditor::_locale_selected));
+
 	error = memnew(ConfirmationDialog);
 	error->set_title(TTR("Error!"));
 	value_vbox->add_child(error);

+ 3 - 0
editor/property_editor.h

@@ -32,6 +32,7 @@
 #define PROPERTY_EDITOR_H
 
 #include "editor/editor_file_dialog.h"
+#include "editor/editor_locale_dialog.h"
 #include "editor/scene_tree_editor.h"
 #include "scene/gui/button.h"
 #include "scene/gui/check_box.h"
@@ -97,6 +98,7 @@ class CustomPropertyEditor : public PopupPanel {
 	PopupMenu *menu;
 	SceneTreeDialog *scene_tree;
 	EditorFileDialog *file;
+	EditorLocaleDialog *locale;
 	ConfirmationDialog *error;
 	String name;
 	Variant::Type type;
@@ -136,6 +138,7 @@ class CustomPropertyEditor : public PopupPanel {
 
 	void _text_edit_changed();
 	void _file_selected(String p_file);
+	void _locale_selected(String p_locale);
 	void _modified(String p_string);
 
 	real_t _parse_real_expression(String text);

+ 1 - 0
modules/gdnative/include/nativescript/godot_nativescript.h

@@ -79,6 +79,7 @@ typedef enum {
 	GODOT_PROPERTY_HINT_PROPERTY_OF_BASE_TYPE, ///< a property of a base type
 	GODOT_PROPERTY_HINT_PROPERTY_OF_INSTANCE, ///< a property of an instance
 	GODOT_PROPERTY_HINT_PROPERTY_OF_SCRIPT, ///< a property of a script & base
+	GODOT_PROPERTY_HINT_LOCALE_ID,
 	GODOT_PROPERTY_HINT_MAX,
 } godot_nativescript_property_hint;
 

+ 1 - 1
scene/gui/button.cpp

@@ -569,7 +569,7 @@ void Button::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_button_icon", "get_button_icon");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_text"), "set_clip_text", "get_clip_text");

+ 1 - 1
scene/gui/graph_node.cpp

@@ -1022,7 +1022,7 @@ void GraphNode::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "title"), "set_title", "get_title");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position_offset"), "set_position_offset", "get_position_offset");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_close"), "set_show_close_button", "is_close_button_visible");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resizable"), "set_resizable", "is_resizable");

+ 1 - 1
scene/gui/label.cpp

@@ -916,7 +916,7 @@ void Label::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text");
 	ADD_GROUP("Locale", "");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "horizontal_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_horizontal_alignment", "get_horizontal_alignment");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "vertical_alignment", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_vertical_alignment", "get_vertical_alignment");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Off,Arbitrary,Word,Word (Smart)"), "set_autowrap_mode", "get_autowrap_mode");

+ 1 - 1
scene/gui/line_edit.cpp

@@ -2344,7 +2344,7 @@ void LineEdit::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_right_icon", "get_right_icon");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars");
 	ADD_GROUP("Structured Text", "structured_text_");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override");

+ 1 - 1
scene/gui/link_button.cpp

@@ -308,7 +308,7 @@ void LinkButton::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "underline", PROPERTY_HINT_ENUM, "Always,On Hover,Never"), "set_underline_mode", "get_underline_mode");
 	ADD_GROUP("Structured Text", "structured_text_");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override");

+ 1 - 1
scene/gui/rich_text_label.cpp

@@ -4207,7 +4207,7 @@ void RichTextLabel::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "RichTextEffect"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects");
 
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 
 	ADD_GROUP("Structured Text", "structured_text_");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override");

+ 1 - 1
scene/gui/text_edit.cpp

@@ -5171,7 +5171,7 @@ void TextEdit::_bind_methods() {
 	/* Inspector */
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
-	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language");
 
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled");

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff