com.squarespace.cldr.LanguageMatcher Maven / Gradle / Ivy
package com.squarespace.cldr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Implements enhanced language matching:
* http://www.unicode.org/reports/tr35/tr35.html#EnhancedLanguageMatching
*
* Given a list of application supported locales, and a list of a user's
* desired locales (sorted in order of preference descending), return the
* application locale that best matches the user's request.
*/
public class LanguageMatcher {
private static final Pattern COMMA_SPACE = Pattern.compile("[,\\s]+");
private static final CLDR CLDR_INSTANCE = CLDR.get();
private static final Map PARADIGM_LOCALES = buildParadigmLocales();
private static final DistanceTable DISTANCE_TABLE = DistanceTable.get();
private final Map> exactMatch = new HashMap<>();
private final List supportedLocales;
public LanguageMatcher(String supportedLocales) {
this.supportedLocales = parse(supportedLocales);
init();
}
public LanguageMatcher(List supportedLocales) {
this.supportedLocales = parse(supportedLocales);
init();
}
private void init() {
if (supportedLocales.isEmpty()) {
throw new IllegalArgumentException("You must provide at least one supported locale.");
}
Match first = supportedLocales.get(0);
this.supportedLocales.sort((a, b) -> {
// Make sure we keep the first locale in position, since it also serves as the default.
if (a.equals(first)) {
return -1;
}
if (b.equals(first)) {
return 1;
}
// Sort all paradigm locales to the front.
Integer pa = PARADIGM_LOCALES.get(a.locale);
Integer pb = PARADIGM_LOCALES.get(b.locale);
if (pa != null) {
return pb == null ? -1 : Integer.compare(pa, pb);
} else if (pb != null) {
return 1;
}
// Leave all other locales in their relative positions.
return 0;
});
for (Match match : supportedLocales) {
List bundles = exactMatch.get(match.locale);
if (bundles == null) {
bundles = new ArrayList<>();
exactMatch.put(match.locale, bundles);
}
bundles.add(match.bundleId);
}
}
public String match(String desiredRaw) {
return matchImpl(parse(desiredRaw));
}
public String match(List desired) {
return matchImpl(parse(desired));
}
private String matchImpl(List desiredLocales) {
int bestDistance = 100;
Match bestMatch = null;
for (Match desired : desiredLocales) {
List exact = exactMatch.get(desired.locale);
if (exact != null) {
return exact.get(0);
}
for (Match supported : supportedLocales) {
int distance = DISTANCE_TABLE.distance(desired.locale, supported.locale);
if (distance < bestDistance) {
bestDistance = distance;
bestMatch = supported;
}
}
}
return bestMatch == null ? supportedLocales.get(0).bundleId : bestMatch.bundleId;
}
private static List parse(String locales) {
return convert(Arrays.stream(COMMA_SPACE.split(locales)));
}
private static List parse(List locales) {
Stream stream = locales.stream()
.flatMap(COMMA_SPACE::splitAsStream);
return convert(stream);
}
private static List convert(Stream stream) {
return stream.map(s -> s.trim())
.filter(s -> !s.isEmpty())
.map(s -> new Match(s, CLDR_INSTANCE.resolve(s)))
.collect(Collectors.toList());
}
private static Map buildParadigmLocales() {
Map res = new HashMap<>();
int size = _LanguageRules.PARADIGM_LOCALES.size();
for (int i = 0; i < size; i++) {
CLDR.Locale locale = CLDR_INSTANCE.resolve(_LanguageRules.PARADIGM_LOCALES.get(i));
res.put(locale, i);
}
return res;
}
private static class Match {
private final String bundleId;
private final CLDR.Locale locale;
public Match(String bundleId, CLDR.Locale locale) {
this.bundleId = bundleId;
this.locale = locale;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("Match(id=").append(bundleId).append(", max=").append(locale).append(')');
return buf.toString();
}
}
}