Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,26 @@

package com.spotify.i18n.locales.common.impl;

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.MUTUALLY_INTELLIGIBLE;
import static com.spotify.i18n.locales.common.model.LocaleAffinity.SAME;

import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.ibm.icu.util.LocaleMatcher;
import com.ibm.icu.util.ULocale;
import com.spotify.i18n.locales.common.LocalesResolver;
import com.spotify.i18n.locales.common.model.LocaleAffinity;
import com.spotify.i18n.locales.common.model.ResolvedLocale;
import com.spotify.i18n.locales.common.model.SupportedLocale;
import com.spotify.i18n.locales.utils.acceptlanguage.AcceptLanguageUtils;
import com.spotify.i18n.locales.utils.available.AvailableLocalesUtils;
import com.spotify.i18n.locales.utils.hierarchy.LocalesHierarchyUtils;
import com.spotify.i18n.locales.utils.languagetag.LanguageTagUtils;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -68,19 +76,39 @@ public abstract class LocalesResolverBaseImpl implements LocalesResolver {
.map(ULocale::getLanguage)
.collect(Collectors.toSet());

/** Default required locale affinity for resolved locale */
public static final LocaleAffinity DEFAULT_REQUIRED_LOCALE_AFFINITY = MUTUALLY_INTELLIGIBLE;

/** Wildcard character for language ranges */
private static final String LANGUAGE_RANGE_WILDCARD = "*";

/** Parseable code for undefined value in a language tag */
private static final String ULOCALE_UNDEFINED_CODE = "Und";

/**
* Affinity to distance threshold for {@link LocaleMatcher.Builder#setMaxDistance(Locale, Locale)}
*/
private static final Map<LocaleAffinity, Integer> AFFINITY_TO_DISTANCE_THRESHOLD =
Map.of(
SAME, 6,
MUTUALLY_INTELLIGIBLE, 10,
HIGH, 20,
LOW, 100);

/**
* Returns the set of {@link SupportedLocale} against which locale resolution will be performed.
*
* @return The set of {@link SupportedLocale}s
*/
public abstract Set<SupportedLocale> supportedLocales();

/**
* Returns the required {@link LocaleAffinity} for the resolved locale
*
* @return The required {@link LocaleAffinity}
*/
abstract LocaleAffinity minimumLocaleAffinity();

/**
* Returns the default {@link ResolvedLocale} that will be used as final fallback for a given
* Accept-Language value when no match can be found, or when the resolution fails for some reason.
Expand Down Expand Up @@ -442,6 +470,7 @@ private ULocale getLocaleForFormatting(
private LocaleMatcher getLocaleMatcher(final Set<ULocale> supportedLocales) {
return LocaleMatcher.builder()
.setSupportedULocales(supportedLocales)
.internalSetThresholdDistance(AFFINITY_TO_DISTANCE_THRESHOLD.get(minimumLocaleAffinity()))
.setNoDefaultLocale()
.build();
}
Expand All @@ -459,7 +488,9 @@ public static Builder builder() {
/** A builder for a {@link LocalesResolverBaseImpl}. */
@AutoValue.Builder
public abstract static class Builder {
Builder() {} // package private constructor
Builder() { // package private constructor
minimumLocaleAffinity(DEFAULT_REQUIRED_LOCALE_AFFINITY);
}

/**
* Configures the default {@link ResolvedLocale} that will be returned if locale resolution
Expand All @@ -479,11 +510,26 @@ public abstract static class Builder {
*/
public abstract Builder supportedLocales(final Set<SupportedLocale> supportedLocales);

/**
* Configures the set of {@link LocaleAffinity} based on which locale resolution will be
* performed.
*
* <p>Default value: {@link LocaleAffinity#MUTUALLY_INTELLIGIBLE}
*
* @param minimumLocaleAffinity
* @return The {@link Builder}
*/
abstract Builder minimumLocaleAffinity(final LocaleAffinity minimumLocaleAffinity);

abstract LocalesResolverBaseImpl autoBuild();

/** Builds a {@link LocalesResolver} out of this builder. */
public final LocalesResolver build() {
return autoBuild();
LocalesResolverBaseImpl built = autoBuild();
Preconditions.checkState(
built.minimumLocaleAffinity() != LocaleAffinity.NONE,
"Required locale affinity for resolved locale cannot be NONE.");
return built;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ public static Stream<Arguments> whenCalculating_returnsExpectedAffinity() {
Arguments.of("zh-Hant", "zh", NONE),
Arguments.of("zh-MK", "zh-CN", SAME),
Arguments.of("zh-FR", "zh-CN", SAME),
Arguments.of("zh-TW", "zh-US", SAME));
Arguments.of("zh-TW", "zh-US", SAME),

// Malay
Arguments.of("ms", "id", HIGH));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.spotify.i18n.locales.common.impl;

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.MUTUALLY_INTELLIGIBLE;
import static com.spotify.i18n.locales.common.model.LocaleAffinity.NONE;
Expand Down Expand Up @@ -50,7 +51,10 @@ class LocaleAffinityCalculatorBaseImplTest {
public static final LocaleAffinityCalculator CALCULATOR_AGAINST_TEST_SET_OF_LOCALES =
LocaleAffinityCalculatorBaseImpl.builder()
.againstLocales(
Set.of("ar", "bs-Cyrl", "es", "fr", "ja", "pt", "sr-Latn", "zh-Hant").stream()
Set.of(
"ar", "bs-Cyrl", "es", "fr", "id", "ja", "nb", "pt", "ru", "sr-Latn",
"zh-Hant")
.stream()
.map(ULocale::forLanguageTag)
.collect(Collectors.toSet()))
.build();
Expand Down Expand Up @@ -186,6 +190,9 @@ public static Stream<Arguments> whenCalculating_returnsExpectedAffinity() {
// Basque should be matched, since we support Spanish
Arguments.of("eu", LOW),

// Danish
Arguments.of("da", HIGH),

// French
Arguments.of("fr", SAME),
Arguments.of("fr-BE", SAME),
Expand All @@ -206,6 +213,17 @@ public static Stream<Arguments> whenCalculating_returnsExpectedAffinity() {
Arguments.of("sr-Latn", SAME),
Arguments.of("sr-Cyrl-ME", SAME),

// Kazakh should be matched with Russian
Arguments.of("kk", NONE),
Arguments.of("kk-KZ", NONE),

// Malay
Arguments.of("ms", HIGH),

// Mongolian should be matched with Russian
Arguments.of("mn", NONE),
Arguments.of("mn-MN", NONE),

// Portuguese
Arguments.of("pt", SAME),
Arguments.of("pt-BR", SAME),
Expand Down
Loading
Loading