diff --git a/README.md b/README.md index a810e5b..1f4e085 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,9 @@ We define the affinity between two locales using a `LocaleAffinity` enum value: - `LOW`: Locales are somewhat related, meaning they either have low similarities from a linguistic perspective or co-exist in given geopolitical or cultural contexts. - `HIGH`: Locales are quite related, meaning they have similarities from a linguistic perspective. -- `SAME_OR_MUTUALLY_INTELLIGIBLE`: Locales either identify the same language, or languages that are - similar to a point where a person should understand both if they understand one of them. +- `MUTUALLY_INTELLIGIBLE`: Locales identify languages that are similar to a point where a person + should understand both if they understand one of them. +- `SAME`: Locales identify the same language We offer two separate logics, each dedicated to separate use-cases: diff --git a/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/AffinityCalculationExampleMain.java b/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/AffinityCalculationExampleMain.java index 44a5eb6..f394608 100644 --- a/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/AffinityCalculationExampleMain.java +++ b/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/AffinityCalculationExampleMain.java @@ -22,7 +22,7 @@ import static com.spotify.i18n.locales.common.model.LocaleAffinity.LOW; import static com.spotify.i18n.locales.common.model.LocaleAffinity.NONE; -import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE; +import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME; import com.ibm.icu.util.ULocale; import com.spotify.i18n.locales.common.LocaleAffinityCalculator; @@ -120,18 +120,18 @@ private static Map getLanguageTagToExpectedAffinityMap() map.put("en-US", NONE); // Spanish in Europe should ok - map.put("es-419", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("es-GB", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("es-US", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("es-419", SAME); + map.put("es-GB", SAME); + map.put("es-US", SAME); // Basque should be matched, since we support Spanish map.put("eu", LOW); // French - map.put("fr", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("fr-BE", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("fr-CA", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("fr-FR", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("fr", SAME); + map.put("fr-BE", SAME); + map.put("fr-CA", SAME); + map.put("fr-FR", SAME); // Galician should be matched, since we support Spanish map.put("gl", LOW); @@ -140,23 +140,23 @@ private static Map getLanguageTagToExpectedAffinityMap() map.put("hi", NONE); // Croatian should be nicely matched with Bosnian - map.put("hr-HR", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("hr-HR", SAME); // Serbian Cyrillic should be matched, although only Latin script is supported - map.put("sr", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("sr-Latn", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("sr-Cyrl-ME", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("sr", SAME); + map.put("sr-Latn", SAME); + map.put("sr-Cyrl-ME", SAME); // Portuguese - map.put("pt", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("pt-BR", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("pt-SE", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("pt-US", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("pt", SAME); + map.put("pt-BR", SAME); + map.put("pt-SE", SAME); + map.put("pt-US", SAME); // Only Traditional Chinese should be matched, not Simplified map.put("zh-CN", NONE); - map.put("zh-TW", SAME_OR_MUTUALLY_INTELLIGIBLE); - map.put("zh-HK", SAME_OR_MUTUALLY_INTELLIGIBLE); + map.put("zh-TW", SAME); + map.put("zh-HK", SAME); return map; } @@ -168,13 +168,9 @@ public static void main(String[] args) { System.out.println("========================================"); System.out.println( String.format( - "Example 1: List of language tags with calculated affinity = %s", - SAME_OR_MUTUALLY_INTELLIGIBLE.name())); + "Example 1: List of language tags with calculated affinity = %s", SAME.name())); getLanguageTagToExpectedAffinityMap().keySet().stream() - .filter( - languageTag -> - affinityCalculator.calculate(languageTag).affinity() - == SAME_OR_MUTUALLY_INTELLIGIBLE) + .filter(languageTag -> affinityCalculator.calculate(languageTag).affinity() == SAME) .forEach(System.out::println); // Example 2: Check that calculated affinity for each language tag matches the expected value. diff --git a/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/ReferenceLocalesBasedJoinExampleMain.java b/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/ReferenceLocalesBasedJoinExampleMain.java index 9e1ba29..61c1697 100644 --- a/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/ReferenceLocalesBasedJoinExampleMain.java +++ b/examples/locales-affinity-examples/src/main/java/com/spotify/i18n/locales/affinity/examples/ReferenceLocalesBasedJoinExampleMain.java @@ -45,17 +45,15 @@ public class ReferenceLocalesBasedJoinExampleMain { *

Possible joins in the execution output are: * *

* * @param args diff --git a/locales-common/src/main/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImpl.java b/locales-common/src/main/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImpl.java index 690b099..1f432a2 100644 --- a/locales-common/src/main/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImpl.java +++ b/locales-common/src/main/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImpl.java @@ -75,7 +75,7 @@ public abstract class LocaleAffinityCalculatorBaseImpl implements LocaleAffinity private static final double DISTANCE_THRESHOLD = 224.0; // Score to affinity thresholds - private static final int SCORE_THRESHOLD_SAME_OR_MUTUALLY_INTELLIGIBLE = 65; + private static final int SCORE_THRESHOLD_MUTUALLY_INTELLIGIBLE = 65; private static final int SCORE_THRESHOLD_HIGH = 30; private static final int SCORE_THRESHOLD_LOW = 0; @@ -103,14 +103,14 @@ private LocaleAffinity getAffinity(@Nullable final String languageTag) { // We attempt to match based on corresponding spoken language first, and make use of the // score-based affinity calculation as fallback. if (hasSameSpokenLanguageAffinity(languageTag)) { - return LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE; + return LocaleAffinity.SAME; } else { return calculateScoreBasedAffinity(languageTag); } } } - private boolean hasSameSpokenLanguageAffinity(final String languageTag) { + private boolean hasSameSpokenLanguageAffinity(@Nullable final String languageTag) { return LanguageUtils.getSpokenLanguageLocale(languageTag) .map( spokenLanguageLocale -> @@ -155,8 +155,8 @@ private int convertDistanceToAffinityScore(final int distance) { } private LocaleAffinity convertScoreToLocaleAffinity(final int score) { - if (score > SCORE_THRESHOLD_SAME_OR_MUTUALLY_INTELLIGIBLE) { - return LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE; + if (score > SCORE_THRESHOLD_MUTUALLY_INTELLIGIBLE) { + return LocaleAffinity.MUTUALLY_INTELLIGIBLE; } else if (score > SCORE_THRESHOLD_HIGH) { return LocaleAffinity.HIGH; } else if (score > SCORE_THRESHOLD_LOW) { diff --git a/locales-common/src/main/java/com/spotify/i18n/locales/common/model/LocaleAffinity.java b/locales-common/src/main/java/com/spotify/i18n/locales/common/model/LocaleAffinity.java index 822a90e..973af50 100644 --- a/locales-common/src/main/java/com/spotify/i18n/locales/common/model/LocaleAffinity.java +++ b/locales-common/src/main/java/com/spotify/i18n/locales/common/model/LocaleAffinity.java @@ -40,8 +40,11 @@ public enum LocaleAffinity { HIGH, /** - * Locales either identify the same language, or languages that are similar to a point where a - * person should understand both if they understand one of them. + * Locales identify languages that are similar to a point where a person should understand both if + * they understand one of them. */ - SAME_OR_MUTUALLY_INTELLIGIBLE + MUTUALLY_INTELLIGIBLE, + + /** Locales identify the same language. */ + SAME } diff --git a/locales-common/src/test/java/com/spotify/i18n/locales/common/LocaleAffinityHelpersFactoryTest.java b/locales-common/src/test/java/com/spotify/i18n/locales/common/LocaleAffinityHelpersFactoryTest.java index 2f1fa8a..91af1b1 100644 --- a/locales-common/src/test/java/com/spotify/i18n/locales/common/LocaleAffinityHelpersFactoryTest.java +++ b/locales-common/src/test/java/com/spotify/i18n/locales/common/LocaleAffinityHelpersFactoryTest.java @@ -188,15 +188,15 @@ void whenBuildingRelatedReferenceLocalesCalculator_returnsExpectedCalculator() { whenJoiningDatasetsUsingReferenceLocalesCalculator_joinsBasedOnExpectedRelatedReferenceLocale() { return Stream.of( // Chinese (Hong-Kong), Chinese (Traditional) -> Chinese (Taiwan) - Arguments.of("zh-HK", "zh-Hant", "zh-TW", LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("zh-HK", "zh-Hant", "zh-TW", LocaleAffinity.SAME), // Chinese (Hong-Kong), Cantonese (Hong-Kong) -> Cantonese Arguments.of("zh-HK", "yue-HK", "yue", LocaleAffinity.HIGH), // Dutch (Belgium), Dutch (Netherlands) -> Dutch - Arguments.of("nl-BE", "nl-NL", "nl", LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("nl-BE", "nl-NL", "nl", LocaleAffinity.SAME), // French (Switzerland), French (Canada) -> French - Arguments.of("fr-CH", "fr-CA", "fr-CA", LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE)); + Arguments.of("fr-CH", "fr-CA", "fr-CA", LocaleAffinity.SAME)); } } diff --git a/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImplTest.java b/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImplTest.java index aa86e26..42e0cd3 100644 --- a/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImplTest.java +++ b/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/LocaleAffinityCalculatorBaseImplTest.java @@ -21,8 +21,9 @@ package com.spotify.i18n.locales.common.impl; import static com.spotify.i18n.locales.common.model.LocaleAffinity.LOW; +import static com.spotify.i18n.locales.common.model.LocaleAffinity.MUTUALLY_INTELLIGIBLE; import static com.spotify.i18n.locales.common.model.LocaleAffinity.NONE; -import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE; +import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.*; @@ -112,18 +113,18 @@ public static Stream whenCalculating_returnsExpectedAffinity() { Arguments.of("en-US", NONE), // Spanish in Europe should be ranked higher - Arguments.of("es-419", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("es-GB", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("es-US", SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("es-419", SAME), + Arguments.of("es-GB", SAME), + Arguments.of("es-US", SAME), // Basque should be matched, since we support Spanish Arguments.of("eu", LOW), // French - Arguments.of("fr", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("fr-BE", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("fr-CA", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("fr-FR", SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("fr", SAME), + Arguments.of("fr-BE", SAME), + Arguments.of("fr-CA", SAME), + Arguments.of("fr-FR", SAME), // Galician should be matched, since we support Spanish Arguments.of("gl", LOW), @@ -132,23 +133,23 @@ public static Stream whenCalculating_returnsExpectedAffinity() { Arguments.of("hi", NONE), // Croatian should be nicely matched with Bosnian - Arguments.of("hr-HR", SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("hr-HR", MUTUALLY_INTELLIGIBLE), // Serbian Cyrillic should be matched, although only Latin script is supported - Arguments.of("sr", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("sr-Latn", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("sr-Cyrl-ME", SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("sr", SAME), + Arguments.of("sr-Latn", SAME), + Arguments.of("sr-Cyrl-ME", SAME), // Portuguese - Arguments.of("pt", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("pt-BR", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("pt-SE", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("pt-US", SAME_OR_MUTUALLY_INTELLIGIBLE), + Arguments.of("pt", SAME), + Arguments.of("pt-BR", SAME), + Arguments.of("pt-SE", SAME), + Arguments.of("pt-US", SAME), // Only Traditional Chinese should be matched, not Simplified Arguments.of("zh-CN", NONE), - Arguments.of("zh-TW", SAME_OR_MUTUALLY_INTELLIGIBLE), - Arguments.of("zh-HK", SAME_OR_MUTUALLY_INTELLIGIBLE)); + Arguments.of("zh-TW", SAME), + Arguments.of("zh-HK", SAME)); } @Test diff --git a/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/ReferenceLocalesCalculatorBaseImplTest.java b/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/ReferenceLocalesCalculatorBaseImplTest.java index b81fa49..bd12a58 100644 --- a/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/ReferenceLocalesCalculatorBaseImplTest.java +++ b/locales-common/src/test/java/com/spotify/i18n/locales/common/impl/ReferenceLocalesCalculatorBaseImplTest.java @@ -22,7 +22,8 @@ import static com.spotify.i18n.locales.common.model.LocaleAffinity.HIGH; import static com.spotify.i18n.locales.common.model.LocaleAffinity.LOW; -import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME_OR_MUTUALLY_INTELLIGIBLE; +import static com.spotify.i18n.locales.common.model.LocaleAffinity.MUTUALLY_INTELLIGIBLE; +import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME; import static com.spotify.i18n.locales.utils.hierarchy.LocalesHierarchyUtils.isSameLocale; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -73,7 +74,7 @@ public void validateLocaleAffinityScoreRanges(final ULocale input) { if (isSameLocale(inputLanguageScriptOnly, referenceLanguageScriptOnly) || isSameSpokenLanguage(inputLanguageScriptOnly, referenceLanguageScriptOnly)) { assertEquals( - SAME_OR_MUTUALLY_INTELLIGIBLE, + SAME, affinity, String.format( "Reference locale [%s] for input locale [%s] share the same language & script [%s], Yet, affinity is %s.", @@ -81,20 +82,26 @@ public void validateLocaleAffinityScoreRanges(final ULocale input) { input.toLanguageTag(), inputLanguageScriptOnly.toLanguageTag(), affinity)); - } else if (areKnownInterchangeableLocales( + } else if (areKnownMutuallyIntelligibleLocales( inputLanguageScriptOnly, referenceLanguageScriptOnly)) { assertEquals( - SAME_OR_MUTUALLY_INTELLIGIBLE, + MUTUALLY_INTELLIGIBLE, affinity, String.format( - "Reference locale [%s] for input locale [%s] are known interchangeable locales, Yet, affinity is %s.", + "Reference locale [%s] for input locale [%s] are known mutually intelligible locales, Yet, affinity is %s.", referenceLocale.toLanguageTag(), input.toLanguageTag(), inputLanguageScriptOnly.toLanguageTag(), affinity)); } else { assertNotEquals( - SAME_OR_MUTUALLY_INTELLIGIBLE, + SAME, + affinity, + String.format( + "Reference locale [%s] for input locale [%s] do not share the same language & script. Yet, affinity is %s.", + referenceLocale.toLanguageTag(), input.toLanguageTag(), affinity)); + assertNotEquals( + MUTUALLY_INTELLIGIBLE, affinity, String.format( "Reference locale [%s] for input locale [%s] do not share the same language & script. Yet, affinity is %s.", @@ -117,7 +124,7 @@ private static ULocale getLocaleWithLanguageAndScriptOnly(ULocale input) { .build(); } - private boolean areKnownInterchangeableLocales(ULocale inputLS, ULocale referenceLS) { + private boolean areKnownMutuallyIntelligibleLocales(ULocale inputLS, ULocale referenceLS) { String input = inputLS.toLanguageTag(); String reference = referenceLS.toLanguageTag(); @@ -218,10 +225,10 @@ private static RelatedReferenceLocale rrl( private static List chineseTraditional() { return List.of( // Traditional Chinese - rrl("zh-HK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("zh-MO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("zh-TW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("zh-Hant-MY", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("zh-HK", SAME), + rrl("zh-MO", SAME), + rrl("zh-TW", SAME), + rrl("zh-Hant-MY", SAME), // Cantonese rrl("yue", HIGH), rrl("yue-MO", HIGH), @@ -231,8 +238,8 @@ private static List chineseTraditional() { private static List danish() { return List.of( // Danish - rrl("da", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("da-GL", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("da", SAME), + rrl("da-GL", SAME), // Bokmål rrl("nb", HIGH), rrl("nb-SJ", HIGH), @@ -251,124 +258,124 @@ private static List english() { // Welsh rrl("cy", LOW), // English - rrl("en", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-001", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-150", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-AU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BB", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-BZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CX", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CY", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-CZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-DE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-DG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-DK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-DM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ER", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ES", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-FI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-FJ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-FR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-FK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-FM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GB", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GD", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-GY", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-HK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-HU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ID", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IL", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-IT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-JE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-JM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-KE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-KI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-KN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-KY", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-LC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-LR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-LS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MP", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MV", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-MY", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NL", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-NZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PL", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-PW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-RO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-RW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SB", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SD", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SL", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SX", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-SZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TO", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TV", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-TZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-UG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-UM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-VC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-VG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-VI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-VU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-WS", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ZA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ZM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("en-ZW", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("en", SAME), + rrl("en-001", SAME), + rrl("en-150", SAME), + rrl("en-AE", SAME), + rrl("en-AG", SAME), + rrl("en-AI", SAME), + rrl("en-AS", SAME), + rrl("en-AT", SAME), + rrl("en-AU", SAME), + rrl("en-BB", SAME), + rrl("en-BE", SAME), + rrl("en-BI", SAME), + rrl("en-BM", SAME), + rrl("en-BS", SAME), + rrl("en-BW", SAME), + rrl("en-BZ", SAME), + rrl("en-CA", SAME), + rrl("en-CC", SAME), + rrl("en-CH", SAME), + rrl("en-CK", SAME), + rrl("en-CM", SAME), + rrl("en-CX", SAME), + rrl("en-CY", SAME), + rrl("en-CZ", SAME), + rrl("en-DE", SAME), + rrl("en-DG", SAME), + rrl("en-DK", SAME), + rrl("en-DM", SAME), + rrl("en-ER", SAME), + rrl("en-ES", SAME), + rrl("en-FI", SAME), + rrl("en-FJ", SAME), + rrl("en-FR", SAME), + rrl("en-FK", SAME), + rrl("en-FM", SAME), + rrl("en-GB", SAME), + rrl("en-GS", SAME), + rrl("en-GD", SAME), + rrl("en-GG", SAME), + rrl("en-GH", SAME), + rrl("en-GI", SAME), + rrl("en-GM", SAME), + rrl("en-GU", SAME), + rrl("en-GY", SAME), + rrl("en-HK", SAME), + rrl("en-HU", SAME), + rrl("en-ID", SAME), + rrl("en-IE", SAME), + rrl("en-IL", SAME), + rrl("en-IM", SAME), + rrl("en-IN", SAME), + rrl("en-IO", SAME), + rrl("en-IT", SAME), + rrl("en-JE", SAME), + rrl("en-JM", SAME), + rrl("en-KE", SAME), + rrl("en-KI", SAME), + rrl("en-KN", SAME), + rrl("en-KY", SAME), + rrl("en-LC", SAME), + rrl("en-LR", SAME), + rrl("en-LS", SAME), + rrl("en-MG", SAME), + rrl("en-MH", SAME), + rrl("en-MO", SAME), + rrl("en-MP", SAME), + rrl("en-MS", SAME), + rrl("en-MT", SAME), + rrl("en-MU", SAME), + rrl("en-MV", SAME), + rrl("en-MW", SAME), + rrl("en-MY", SAME), + rrl("en-NA", SAME), + rrl("en-NF", SAME), + rrl("en-NG", SAME), + rrl("en-NL", SAME), + rrl("en-NO", SAME), + rrl("en-NR", SAME), + rrl("en-NU", SAME), + rrl("en-NZ", SAME), + rrl("en-PG", SAME), + rrl("en-PH", SAME), + rrl("en-PK", SAME), + rrl("en-PL", SAME), + rrl("en-PN", SAME), + rrl("en-PR", SAME), + rrl("en-PT", SAME), + rrl("en-PW", SAME), + rrl("en-RO", SAME), + rrl("en-RW", SAME), + rrl("en-SB", SAME), + rrl("en-SC", SAME), + rrl("en-SD", SAME), + rrl("en-SE", SAME), + rrl("en-SG", SAME), + rrl("en-SH", SAME), + rrl("en-SI", SAME), + rrl("en-SK", SAME), + rrl("en-SL", SAME), + rrl("en-SS", SAME), + rrl("en-SX", SAME), + rrl("en-SZ", SAME), + rrl("en-TC", SAME), + rrl("en-TK", SAME), + rrl("en-TO", SAME), + rrl("en-TT", SAME), + rrl("en-TV", SAME), + rrl("en-TZ", SAME), + rrl("en-UG", SAME), + rrl("en-UM", SAME), + rrl("en-VC", SAME), + rrl("en-VG", SAME), + rrl("en-VI", SAME), + rrl("en-VU", SAME), + rrl("en-WS", SAME), + rrl("en-ZA", SAME), + rrl("en-ZM", SAME), + rrl("en-ZW", SAME), // Irish rrl("ga", LOW), rrl("ga-GB", LOW), @@ -390,52 +397,52 @@ private static List french() { // Breton rrl("br", LOW), // French - rrl("fr", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-BE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-BF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-BI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-BJ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-BL", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CD", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-CM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-DJ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-DZ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-GA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-GF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-GN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-GP", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-GQ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-HT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-KM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-LU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-ML", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MQ", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-MU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-NC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-NE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-PF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-PM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-RE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-RW", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-SC", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-SN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-SY", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-TD", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-TG", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-TN", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-VU", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-WF", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("fr-YT", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("fr", SAME), + rrl("fr-BE", SAME), + rrl("fr-BF", SAME), + rrl("fr-BI", SAME), + rrl("fr-BJ", SAME), + rrl("fr-BL", SAME), + rrl("fr-CA", SAME), + rrl("fr-CD", SAME), + rrl("fr-CF", SAME), + rrl("fr-CG", SAME), + rrl("fr-CH", SAME), + rrl("fr-CI", SAME), + rrl("fr-CM", SAME), + rrl("fr-DJ", SAME), + rrl("fr-DZ", SAME), + rrl("fr-GA", SAME), + rrl("fr-GF", SAME), + rrl("fr-GN", SAME), + rrl("fr-GP", SAME), + rrl("fr-GQ", SAME), + rrl("fr-HT", SAME), + rrl("fr-KM", SAME), + rrl("fr-LU", SAME), + rrl("fr-MA", SAME), + rrl("fr-MC", SAME), + rrl("fr-MF", SAME), + rrl("fr-MG", SAME), + rrl("fr-ML", SAME), + rrl("fr-MQ", SAME), + rrl("fr-MR", SAME), + rrl("fr-MU", SAME), + rrl("fr-NC", SAME), + rrl("fr-NE", SAME), + rrl("fr-PF", SAME), + rrl("fr-PM", SAME), + rrl("fr-RE", SAME), + rrl("fr-RW", SAME), + rrl("fr-SC", SAME), + rrl("fr-SN", SAME), + rrl("fr-SY", SAME), + rrl("fr-TD", SAME), + rrl("fr-TG", SAME), + rrl("fr-TN", SAME), + rrl("fr-VU", SAME), + rrl("fr-WF", SAME), + rrl("fr-YT", SAME), // Occitan rrl("oc", LOW), rrl("oc-ES", LOW)); @@ -444,29 +451,25 @@ private static List french() { private static List german() { return List.of( // German - rrl("de", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-AT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-BE", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-CH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-IT", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-LI", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("de-LU", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("de", SAME), + rrl("de-AT", SAME), + rrl("de-BE", SAME), + rrl("de-CH", SAME), + rrl("de-IT", SAME), + rrl("de-LI", SAME), + rrl("de-LU", SAME), // Swiss German - rrl("gsw", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("gsw-FR", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("gsw-LI", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("gsw", MUTUALLY_INTELLIGIBLE), + rrl("gsw-FR", MUTUALLY_INTELLIGIBLE), + rrl("gsw-LI", MUTUALLY_INTELLIGIBLE), // Luxembourgian - rrl("lb", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("lb", MUTUALLY_INTELLIGIBLE), // Romansh rrl("rm", LOW)); } private static List italian() { - return List.of( - rrl("it", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("it-CH", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("it-SM", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("it-VA", SAME_OR_MUTUALLY_INTELLIGIBLE)); + return List.of(rrl("it", SAME), rrl("it-CH", SAME), rrl("it-SM", SAME), rrl("it-VA", SAME)); } private static List norwegian() { @@ -475,30 +478,27 @@ private static List norwegian() { rrl("da", HIGH), rrl("da-GL", HIGH), // Bokmål - rrl("nb", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("nb-SJ", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("nb", SAME), + rrl("nb-SJ", SAME), // Norwegian - rrl("no", SAME_OR_MUTUALLY_INTELLIGIBLE), + rrl("no", SAME), // Nynorsk - rrl("nn", SAME_OR_MUTUALLY_INTELLIGIBLE)); + rrl("nn", SAME)); } private static List serbian() { return List.of( - rrl("sr", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-BA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-Cyrl-ME", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-Latn", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-Latn-BA", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-Latn-XK", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-ME", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sr-XK", SAME_OR_MUTUALLY_INTELLIGIBLE)); + rrl("sr", SAME), + rrl("sr-BA", SAME), + rrl("sr-Cyrl-ME", SAME), + rrl("sr-Latn", SAME), + rrl("sr-Latn-BA", SAME), + rrl("sr-Latn-XK", SAME), + rrl("sr-ME", SAME), + rrl("sr-XK", SAME)); } private static List swedish() { - return List.of( - rrl("sv", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sv-AX", SAME_OR_MUTUALLY_INTELLIGIBLE), - rrl("sv-FI", SAME_OR_MUTUALLY_INTELLIGIBLE)); + return List.of(rrl("sv", SAME), rrl("sv-AX", SAME), rrl("sv-FI", SAME)); } } diff --git a/locales-utils/src/main/java/com/spotify/i18n/locales/utils/language/LanguageUtils.java b/locales-utils/src/main/java/com/spotify/i18n/locales/utils/language/LanguageUtils.java index 033abbd..e8a72f6 100644 --- a/locales-utils/src/main/java/com/spotify/i18n/locales/utils/language/LanguageUtils.java +++ b/locales-utils/src/main/java/com/spotify/i18n/locales/utils/language/LanguageUtils.java @@ -45,6 +45,9 @@ public class LanguageUtils { .setNoDefaultLocale() .build(); + // Locale for Croatian (necessary to go around a bug in icu4j) + public static final ULocale CROATIAN = ULocale.forLanguageTag("hr"); + /** * Returns the optional {@link ULocale} identifying the written language associated with the given * language tag. The returned locale will consist of a language code at the minimum, but will @@ -55,7 +58,22 @@ public class LanguageUtils { * @return the optional locale identifying the written language */ public static Optional getWrittenLanguageLocale(final String languageTag) { - return LanguageTagUtils.parse(languageTag).map(WRITTEN_LANGUAGE_LOCALE_MATCHER::getBestMatch); + return LanguageTagUtils.parse(languageTag) + .map(LanguageUtils::getWrittenLanguageLocaleForLocale); + } + + private static ULocale getWrittenLanguageLocaleForLocale(final ULocale locale) { + // Croatian is Bosnia is matched with Bosnian (Latin script). This is likely a bug in icu4j. We + // created a workaround to ensure that we return Croatian when encountering this locale. + if (isCroatianBosnia(locale)) { + return CROATIAN; + } else { + return WRITTEN_LANGUAGE_LOCALE_MATCHER.getBestMatch(locale); + } + } + + private static boolean isCroatianBosnia(ULocale locale) { + return locale.getLanguage().equals("hr") && locale.getCountry().equals("BA"); } /** @@ -69,8 +87,7 @@ public static Optional getWrittenLanguageLocale(final String languageTa * @return the optional locale identifying the written language */ public static Optional getSpokenLanguageLocale(final String languageTag) { - return LanguageTagUtils.parse(languageTag) - .map(WRITTEN_LANGUAGE_LOCALE_MATCHER::getBestMatch) + return getWrittenLanguageLocale(languageTag) .map(LanguageUtils::getCorrespondingSpokenLanguageLocale); } diff --git a/locales-utils/src/test/java/com/spotify/i18n/locales/utils/language/LanguageUtilsTest.java b/locales-utils/src/test/java/com/spotify/i18n/locales/utils/language/LanguageUtilsTest.java index a471ba4..4ab5e53 100644 --- a/locales-utils/src/test/java/com/spotify/i18n/locales/utils/language/LanguageUtilsTest.java +++ b/locales-utils/src/test/java/com/spotify/i18n/locales/utils/language/LanguageUtilsTest.java @@ -142,4 +142,65 @@ public void confirmLogicAccountsForAllHighestAncestorLocalesWithScript() { .filter(locale -> !fallbackIsNotSpokenLanguage.contains(locale.toLanguageTag())) .count()); } + + @Test + public void validateBosnianCroatian() { + ULocale bosnian = ULocale.forLanguageTag("bs"); + ULocale bosnianLatin = ULocale.forLanguageTag("bs-Latn"); + ULocale bosnianCyrillic = ULocale.forLanguageTag("bs-Cyrl"); + ULocale croatian = ULocale.forLanguageTag("hr"); + + // Bosnian written + Set.of("bs", "bs-Latn", "bs-Latn-BA") + .forEach( + languageTag -> + assertEquals( + bosnianLatin, + LanguageUtils.getWrittenLanguageLocale(languageTag).get(), + String.format( + "Spoken language for language tag %s should be %s", + languageTag, bosnianLatin.toLanguageTag()))); + Set.of("bs-Cyrl", "bs-Cyrl-BA") + .forEach( + languageTag -> + assertEquals( + bosnianCyrillic, + LanguageUtils.getWrittenLanguageLocale(languageTag).get(), + String.format( + "Spoken language for language tag %s should be %s", + languageTag, bosnianCyrillic.toLanguageTag()))); + + // Bosnian spoken + Set.of("bs", "bs-Cyrl", "bs-Cyrl-BA", "bs-Latn", "bs-Latn-BA") + .forEach( + languageTag -> + assertEquals( + bosnian, + LanguageUtils.getSpokenLanguageLocale(languageTag).get(), + String.format( + "Spoken language for language tag %s should be %s", + languageTag, bosnian.toLanguageTag()))); + + // Croatian written + Set.of("hr", "hr-BA", "hr-HR") + .forEach( + languageTag -> + assertEquals( + croatian, + LanguageUtils.getWrittenLanguageLocale(languageTag).get(), + String.format( + "Written language for language tag %s should be %s", + languageTag, croatian.toLanguageTag()))); + + // Croatian spoken + Set.of("hr", "hr-BA", "hr-HR") + .forEach( + languageTag -> + assertEquals( + croatian, + LanguageUtils.getSpokenLanguageLocale(languageTag).get(), + String.format( + "Spoken language for language tag %s should be %s", + languageTag, croatian.toLanguageTag()))); + } }