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 65e190c..95703f1 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 @@ -21,6 +21,7 @@ package com.spotify.i18n.locales.common.impl; import static com.spotify.i18n.locales.utils.hierarchy.LocalesHierarchyUtils.isRootLocale; +import static com.spotify.i18n.locales.utils.hierarchy.LocalesHierarchyUtils.isSameLocale; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; @@ -33,8 +34,10 @@ import com.spotify.i18n.locales.common.LocaleAffinityCalculator; import com.spotify.i18n.locales.common.model.LocaleAffinity; import com.spotify.i18n.locales.common.model.LocaleAffinityResult; +import com.spotify.i18n.locales.utils.language.LanguageUtils; import com.spotify.i18n.locales.utils.languagetag.LanguageTagUtils; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Optional; import java.util.Set; /** @@ -97,12 +100,36 @@ private LocaleAffinity getAffinity(@Nullable final String languageTag) { if (againstLocales().isEmpty()) { return LocaleAffinity.NONE; } else { - int bestDistance = getBestDistance(languageTag); - int correspondingScore = convertDistanceToAffinityScore(bestDistance); - return convertScoreToLocaleAffinity(correspondingScore); + // 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_INTERCHANGEABLE; + } else { + return calculateScoreBasedAffinity(languageTag); + } } } + private boolean hasSameSpokenLanguageAffinity(final String languageTag) { + return LanguageUtils.getSpokenLanguageLocale(languageTag) + .map( + spokenLanguageLocale -> + againstLocales().stream() + .map(ULocale::toLanguageTag) + .map(LanguageUtils::getSpokenLanguageLocale) + .flatMap(Optional::stream) + .anyMatch( + againstSpokenLocale -> + isSameLocale(spokenLanguageLocale, againstSpokenLocale))) + .orElse(false); + } + + private LocaleAffinity calculateScoreBasedAffinity(String languageTag) { + int bestDistance = getBestDistance(languageTag); + int correspondingScore = convertDistanceToAffinityScore(bestDistance); + return convertScoreToLocaleAffinity(correspondingScore); + } + private int getBestDistance(@Nullable final String languageTag) { return LanguageTagUtils.parse(languageTag) .map(LocaleAffinityCalculatorBaseImpl::getMaximizedLanguageScriptRegion) 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 6a5acb2..1eebd64 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 @@ -34,6 +34,7 @@ import com.spotify.i18n.locales.common.model.LocaleAffinity; import com.spotify.i18n.locales.common.model.RelatedReferenceLocale; import com.spotify.i18n.locales.utils.available.AvailableLocalesUtils; +import com.spotify.i18n.locales.utils.language.LanguageUtils; import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -69,7 +70,8 @@ public void validateLocaleAffinityScoreRanges(final ULocale input) { ULocale referenceLanguageScriptOnly = getLocaleWithLanguageAndScriptOnly(referenceLocale); - if (isSameLocale(inputLanguageScriptOnly, referenceLanguageScriptOnly)) { + if (isSameLocale(inputLanguageScriptOnly, referenceLanguageScriptOnly) + || isSameSpokenLanguage(inputLanguageScriptOnly, referenceLanguageScriptOnly)) { assertEquals( SAME_OR_INTERCHANGEABLE, affinity, @@ -101,6 +103,12 @@ public void validateLocaleAffinityScoreRanges(final ULocale input) { } } + private boolean isSameSpokenLanguage( + ULocale inputLanguageScriptOnly, ULocale referenceLanguageScriptOnly) { + return LanguageUtils.getSpokenLanguageLocale(inputLanguageScriptOnly.toLanguageTag()) + .equals(LanguageUtils.getSpokenLanguageLocale(referenceLanguageScriptOnly.toLanguageTag())); + } + private static ULocale getLocaleWithLanguageAndScriptOnly(ULocale input) { return new Builder() .setLocale(ULocale.addLikelySubtags(input)) @@ -116,10 +124,13 @@ private boolean areKnownInterchangeableLocales(ULocale inputLS, ULocale referenc switch (input) { // Bosnian and Croatian case "bs-Latn": + return reference.equals("hr-Latn"); + // Bosnian and Croatian + case "bs-Cyrl": return reference.equals("hr-Latn"); // Croatian and Bosnian case "hr-Latn": - return reference.equals("bs-Latn"); + return reference.equals("bs-Latn") || reference.equals("bs-Cyrl"); // German and Luxembourgish or Swiss German case "de-Latn": return reference.equals("lb-Latn") || reference.equals("gsw-Latn"); @@ -469,7 +480,7 @@ private static List norwegian() { // Norwegian rrl("no", SAME_OR_INTERCHANGEABLE), // Nynorsk - rrl("nn", LOW)); + rrl("nn", SAME_OR_INTERCHANGEABLE)); } private static List serbian() {