forked from Helium314/HeliBoard
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLocaleUtils.kt
206 lines (189 loc) · 9.9 KB
/
LocaleUtils.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*
* Copyright (C) 2011 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin.common
import android.content.Context
import android.content.res.Resources
import helium314.keyboard.compat.locale
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import java.util.Locale
/**
* A class to help with handling Locales in string form.
*
*
* This file has the same meaning and features (and shares all of its code) with the one with the
* same name in Latin IME. They need to be kept synchronized; for any update/bugfix to
* this file, consider also updating/fixing the version in Latin IME.
*/
object LocaleUtils {
// Locale match level constants.
// A higher level of match is guaranteed to have a higher numerical value.
// Some room is left within constants to add match cases that may arise necessary
// in the future, for example differentiating between the case where the countries
// are both present and different, and the case where one of the locales does not
// specify the countries. This difference is not needed now.
// Nothing matches.
private const val LOCALE_NO_MATCH = 0
// The language (and maybe more) matches, but the script is different
private const val LOCALE_MATCH_SCRIPT_DIFFER = 1
// The languages matches, but the country are different. Or, the reference locale requires a
// country and the tested locale does not have one.
private const val LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3
// The languages and country match, but the variants are different. Or, the reference locale
// requires a variant and the tested locale does not have one.
private const val LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6
// The required locale is null or empty so it will accept anything, and the tested locale
// is non-null and non-empty.
private const val LOCALE_ANY_MATCH = 10
// The language matches, and the tested locale specifies a country but the reference locale
// does not require one.
private const val LOCALE_LANGUAGE_MATCH = 15
// The language and the country match, and the tested locale specifies a variant but the
// reference locale does not require one.
private const val LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20
// The compared locales are fully identical. This is the best match level.
private const val LOCALE_FULL_MATCH = 30
const val LOCALE_GOOD_MATCH = LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
/**
* Return how well a tested locale matches a reference locale.
*
*
* This will check the tested locale against the reference locale and return a measure of how
* a well it matches the reference. The general idea is that the tested locale has to match
* every specified part of the required locale. A full match occur when they are equal, a
* partial match when the tested locale agrees with the reference locale but is more specific,
* and a difference when the tested locale does not comply with all requirements from the
* reference locale.
* In more detail, if the reference locale specifies at least a language and the testedLocale
* does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
* reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
* if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
* tested locale agree on the language, but not on the country,
* LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
* and LOCALE_LANGUAGE_MATCH otherwise.
* If they agree on both the language and the country, but not on the variant,
* LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
* specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
* LOCALE_FULL_MATCH is returned.
* Examples:
* en <=> en_US => LOCALE_LANGUAGE_MATCH
* en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
* en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
* en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
* sp_US <=> en_US => LOCALE_NO_MATCH
* de <=> de => LOCALE_FULL_MATCH
* en_US <=> en_US => LOCALE_FULL_MATCH
* "" <=> en_US => LOCALE_ANY_MATCH
*
* @param reference the reference locale to test against.
* @param tested the locale to test.
* @return a constant that measures how well the tested locale matches the reference locale.
*/
fun getMatchLevel(reference: Locale, tested: Locale): Int {
if (reference == tested) return LOCALE_FULL_MATCH
if (reference.toString().isEmpty()) return LOCALE_ANY_MATCH
if (reference.language != tested.language) return LOCALE_NO_MATCH
// language matches
if (reference.script() != tested.script()) {
return LOCALE_MATCH_SCRIPT_DIFFER
}
// script matches
if (reference.country != tested.country) {
return if (reference.country.isEmpty()) LOCALE_LANGUAGE_MATCH
else LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
}
// country matches
return if (reference.variant == tested.variant) LOCALE_FULL_MATCH
else if (reference.variant.isEmpty()) LOCALE_LANGUAGE_AND_COUNTRY_MATCH
else LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
}
@JvmStatic
fun <T> getBestMatch(locale: Locale, collection: Collection<T>, toLocale: (T) -> Locale): T? {
var best: T? = null
var bestLevel = 0
collection.forEach {
val level = getMatchLevel(locale, toLocale(it))
if (level > bestLevel && level >= LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER) {
bestLevel = level
best = it
}
}
return best
}
private val sLocaleCache = HashMap<String, Locale>()
/**
* Creates a locale from a string specification or language tag.
* Ideally this works as reverse of Locale.toString and Locale.toLanguageTag
* If a localeString contains "-" it is always interpreted as language tag.
* localeString is a string specification of a locale, in a format of "ll_cc_variant" where
* "ll" is a language code, "cc" is a country code.
* The script may also be part of the locale string, e.g. "ll_cc_#script"
* Converts "ZZ" regions that used to signal latin script into actual latin script.
* "cc" / region should be uppercase and language should be lowercase, this is automatically converted
*/
@JvmStatic
fun String.constructLocale(): Locale {
synchronized(sLocaleCache) {
sLocaleCache[this]?.let { return it }
if (contains("-")) {
// looks like it's actually a language tag, and not a locale string
val locale = Locale.forLanguageTag(this)
sLocaleCache[this] = locale
return locale
}
val elements = split("_", limit = 3)
val language = elements[0].lowercase()
val region = elements.getOrNull(1)?.uppercase()
val locale = if (elements.size == 1) {
Locale(language) // "zz" works both in constructor and forLanguageTag
} else if (elements.size == 2) {
if (region == "ZZ") Locale.forLanguageTag(elements[0] + "-Latn")
else Locale(language, region!!)
} else if (language == SubtypeLocaleUtils.NO_LANGUAGE) { // localeParams.length == 3
Locale.Builder().setLanguage(language).setVariant(elements[2]).setScript("Latn").build()
} else if (elements[2].startsWith("#")) {
// best guess: elements[2] is a script, e.g. sr-Latn locale to string is sr__#Latn
Locale.Builder().setLanguage(language).setRegion(region).setScript(elements[2].substringAfter("#")).build()
} else {
Locale(language, region!!, elements[2])
}
sLocaleCache[this] = locale
return locale
}
}
@JvmStatic
fun isRtlLanguage(locale: Locale): Boolean {
val displayName = locale.getDisplayName(locale)
if (displayName.isEmpty()) return false
return when (Character.getDirectionality(displayName.codePointAt(0))) {
Character.DIRECTIONALITY_RIGHT_TO_LEFT, Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC -> true
else -> false
}
}
fun Locale.localizedDisplayName(context: Context) =
getLocaleDisplayNameInLocale(this, context.resources, context.resources.configuration.locale())
@JvmStatic
fun getLocaleDisplayNameInLocale(locale: Locale, resources: Resources, displayLocale: Locale): String {
val languageTag = locale.toLanguageTag()
if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE) return resources.getString(R.string.subtype_no_language)
if (locale.script() != locale.language.constructLocale().script() || locale.language == "mns" || locale.language == "xdq" || locale.language=="dru") {
val resId = resources.getIdentifier(
"subtype_${languageTag.replace("-", "_")}",
"string",
BuildConfig.APPLICATION_ID // replaces context.packageName, see https://stackoverflow.com/a/24525379
)
if (resId != 0) return resources.getString(resId)
}
val localeDisplayName = locale.getDisplayName(displayLocale)
return if (localeDisplayName == languageTag) {
locale.getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
} else {
localeDisplayName
}
}
}