package com.oracle.truffle.js.runtime.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;
import com.ibm.icu.text.CaseMap;
import com.ibm.icu.text.CaseMap.Lower;
import com.ibm.icu.text.CaseMap.Upper;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Region;
import com.ibm.icu.util.ULocale;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.profiles.BranchProfile;
import com.oracle.truffle.js.runtime.Errors;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.builtins.JSOrdinary;
import com.oracle.truffle.js.runtime.objects.JSObject;
public final class IntlUtil {
private IntlUtil() {
}
public static final String _2_DIGIT = "2-digit";
public static final String ACCENT = "accent";
public static final String ACCOUNTING = "accounting";
public static final String ALWAYS = "always";
public static final String AUTO = "auto";
public static final String BEST_FIT = "best fit";
public static final String BASE = "base";
public static final String BASE_NAME = "baseName";
public static final String BASIC = "basic";
public static final String BREAK_TYPE = "breakType";
public static final String CALENDAR = "calendar";
public static final String CARDINAL = "cardinal";
public static final String CASE = "case";
public static final String CASE_FIRST = "caseFirst";
public static final String CODE = "code";
public static final String COLLATION = "collation";
public static final String COMPACT = "compact";
public static final String COMPACT_DISPLAY = "compactDisplay";
public static final String CONJUNCTION = "conjunction";
public static final String CURRENCY = "currency";
public static final String CURRENCY_DISPLAY = "currencyDisplay";
public static final String CURRENCY_SIGN = "currencySign";
public static final String DATE_STYLE = "dateStyle";
public static final String DAY = "day";
public static final String DEFAULT = "default";
public static final String DECIMAL = "decimal";
public static final String DISJUNCTION = "disjunction";
public static final String ELEMENT = "element";
public static final String ENGINEERING = "engineering";
public static final String ERA = "era";
public static final String EXCEPT_ZERO = "exceptZero";
public static final String FALLBACK = "fallback";
public static final String FALSE = "false";
public static final String FORMAT_MATCHER = "formatMatcher";
public static final String FULL = "full";
public static final String GRANULARITY = "granularity";
public static final String GRAPHEME = "grapheme";
public static final String H11 = "h11";
public static final String H12 = "h12";
public static final String H23 = "h23";
public static final String H24 = "h24";
public static final String HOUR = "hour";
public static final String HOUR_CYCLE = "hourCycle";
public static final String HOUR12 = "hour12";
public static final String INDEX = "index";
public static final String IGNORE_PUNCTUATION = "ignorePunctuation";
public static final String LANGUAGE = "language";
public static final String LITERAL = "literal";
public static final String LOCALE = "locale";
public static final String LOCALE_MATCHER = "localeMatcher";
public static final String LONG = "long";
public static final String LOOKUP = "lookup";
public static final String LOOSE = "loose";
public static final String LOWER = "lower";
public static final String MAXIMUM_FRACTION_DIGITS = "maximumFractionDigits";
public static final String MAXIMUM_SIGNIFICANT_DIGITS = "maximumSignificantDigits";
public static final String MEDIUM = "medium";
public static final String MINIMUM_FRACTION_DIGITS = "minimumFractionDigits";
public static final String MINIMUM_INTEGER_DIGITS = "minimumIntegerDigits";
public static final String MINIMUM_SIGNIFICANT_DIGITS = "minimumSignificantDigits";
public static final String MINUTE = "minute";
public static final String MONTH = "month";
public static final String NAME = "name";
public static final String NARROW = "narrow";
public static final String NARROW_SYMBOL = "narrowSymbol";
public static final String NEVER = "never";
public static final String NONE = "none";
public static final String NORMAL = "normal";
public static final String NOTATION = "notation";
public static final String NUMERIC = "numeric";
public static final String NUMBERING_SYSTEM = "numberingSystem";
public static final String OR = "or";
public static final String OR_NARROW = "or-narrow";
public static final String OR_SHORT = "or-short";
public static final String ORDINAL = "ordinal";
public static final String PERCENT = "percent";
public static final String REGION = "region";
public static final String SCIENTIFIC = "scientific";
public static final String SCRIPT = "script";
public static final String SEARCH = "search";
public static final String SEP = "sep";
public static final String SECOND = "second";
public static final String SEGMENT = "segment";
public static final String SENTENCE = "sentence";
public static final String SENSITIVITY = "sensitivity";
public static final String SHORT = "short";
public static final String SIGN_DISPLAY = "signDisplay";
public static final String SORT = "sort";
public static final String STANDARD = "standard";
public static final String STANDARD_NARROW = "standard-narrow";
public static final String STANDARD_SHORT = "standard-short";
public static final String STRICT = "strict";
public static final String STYLE = "style";
public static final String SYMBOL = "symbol";
public static final String TERM = "term";
public static final String TIME_STYLE = "timeStyle";
public static final String TIME_ZONE = "timeZone";
public static final String TIME_ZONE_NAME = "timeZoneName";
public static final String TYPE = "type";
public static final String UND = "und";
public static final String UNIT = "unit";
public static final String UNIT_DISPLAY = "unitDisplay";
public static final String UNIT_NARROW = "unit-narrow";
public static final String UNIT_SHORT = "unit-short";
public static final String UPPER = "upper";
public static final String USAGE = "usage";
public static final String USE_GROUPING = "useGrouping";
public static final String VALUE = "value";
public static final String VARIANT = "variant";
public static final String WORD = "word";
public static final String WEEKDAY = "weekday";
public static final String YEAR = "year";
public static final String YEAR_NAME = "yearName";
private static final Set<String> SANCTIONED_SIMPLE_UNIT_IDENTIFIERS = new HashSet<>(Arrays.asList(new String[]{
"acre",
"bit",
"byte",
"celsius",
"centimeter",
"day",
"degree",
"fahrenheit",
"fluid-ounce",
"foot",
"gallon",
"gigabit",
"gigabyte",
"gram",
"hectare",
"hour",
"inch",
"kilobit",
"kilobyte",
"kilogram",
"kilometer",
"liter",
"megabit",
"megabyte",
"meter",
"mile",
"mile-scandinavian",
"milliliter",
"millimeter",
"millisecond",
"minute",
"month",
"ounce",
"percent",
"petabyte",
"pound",
"second",
"stone",
"terabit",
"terabyte",
"week",
"yard",
"year"
}));
public static Locale selectedLocale(JSContext ctx, String[] locales) {
return lookupMatcher(ctx, locales);
}
public static Locale bestAvailableLocale(JSContext context, Locale locale) {
Locale candidate = locale;
while (true) {
if (isAvailableLocale(context, candidate)) {
return candidate;
}
String candidateLanguageTag = candidate.toLanguageTag();
int pos = candidateLanguageTag.lastIndexOf('-');
if (pos == -1) {
return null;
} else {
if (pos >= 2 && candidateLanguageTag.charAt(pos - 2) == '-') {
pos -= 2;
}
candidateLanguageTag = candidateLanguageTag.substring(0, pos);
candidate = Locale.forLanguageTag(candidateLanguageTag);
}
}
}
@TruffleBoundary
public static Locale lookupMatcher(JSContext ctx, String[] requestedLocales) {
for (String locale : requestedLocales) {
Locale requestedLocale = Locale.forLanguageTag(locale);
Locale noExtensionsLocale = requestedLocale.stripExtensions();
Locale availableLocale = bestAvailableLocale(ctx, noExtensionsLocale);
if (availableLocale != null) {
String unicodeExtension = requestedLocale.getExtension('u');
if (unicodeExtension != null) {
availableLocale = new Locale.Builder().setLocale(availableLocale).setExtension('u', unicodeExtension).build();
}
return availableLocale;
}
}
return ctx.getLocale();
}
@TruffleBoundary
public static List<Object> supportedLocales(JSContext ctx, String[] requestedLocales, @SuppressWarnings("unused") String matcher) {
List<Object> result = new ArrayList<>();
for (String locale : requestedLocales) {
Locale noExtensionsLocale = Locale.forLanguageTag(locale).stripExtensions();
Locale availableLocale = bestAvailableLocale(ctx, noExtensionsLocale);
if (availableLocale != null) {
result.add(locale);
}
}
return result;
}
private static boolean isAvailableLocale(JSContext ctx, Locale locale) {
return getAvailableLocales().contains(locale) || ctx.getLocale().equals(locale);
}
private static final LazyValue<Set<Locale>> AVAILABLE_LOCALES = new LazyValue<>(IntlUtil::initAvailableLocales);
private static Set<Locale> getAvailableLocales() {
return AVAILABLE_LOCALES.get();
}
private static Set<Locale> initAvailableLocales() {
Set<Locale> result = new HashSet<>();
try {
for (ULocale ul : ULocale.getAvailableLocales()) {
result.add(ul.toLocale());
if (!ul.getScript().isEmpty()) {
result.add(new Locale(ul.getLanguage(), ul.getCountry()));
}
}
} catch (MissingResourceException e) {
throw Errors.createICU4JDataError(e);
}
return result;
}
public static boolean isValidNumberingSystem(String numberingSystem) {
return Arrays.asList(NumberingSystem.getAvailableNames()).contains(numberingSystem);
}
public static String defaultNumberingSystemName(JSContext context, Locale locale) {
if (context.isOptionV8CompatibilityMode() && "ar".equals(locale.toLanguageTag())) {
return "latn";
}
return NumberingSystem.getInstance(locale).getName();
}
public static void validateUnicodeLocaleIdentifierType(String type, BranchProfile errorBranch) {
if (!UTS35Validator.isStructurallyValidType(type)) {
errorBranch.enter();
throw Errors.createRangeErrorFormat("Invalid option: %s", null, type);
}
}
@TruffleBoundary
public static String normalizeUnicodeLocaleIdentifierType(String type) {
return type.toLowerCase();
}
@TruffleBoundary
public static Locale withoutUnicodeExtension(Locale originalLocale, String key) {
if (!originalLocale.getUnicodeLocaleKeys().contains(key)) {
return originalLocale;
} else {
String value = originalLocale.getUnicodeLocaleType(key);
String originalTag = originalLocale.toLanguageTag();
String toRemove = "-u-" + key + "-" + value;
String strippedTag = originalTag.replace(toRemove, "");
return new Locale(strippedTag);
}
}
public static boolean isWellFormedCurrencyCode(String currency) {
return currency.length() == 3 && UTS35Validator.isAlpha(currency.charAt(0)) && UTS35Validator.isAlpha(currency.charAt(1)) && UTS35Validator.isAlpha(currency.charAt(2));
}
public static void ensureIsWellFormedCurrencyCode(String currency) {
if (!isWellFormedCurrencyCode(currency)) {
throw Errors.createRangeErrorCurrencyNotWellFormed(currency);
}
}
public static void ensureIsStructurallyValidLanguageSubtag(String region) {
if (!UTS35Validator.isStructurallyValidLanguageSubtag(region)) {
throw Errors.createRangeErrorInvalidLanguage(region);
}
}
public static void ensureIsStructurallyValidRegionSubtag(String region) {
if (!UTS35Validator.isStructurallyValidRegionSubtag(region)) {
throw Errors.createRangeErrorInvalidRegion(region);
}
}
public static void ensureIsStructurallyValidScriptSubtag(String script) {
if (!UTS35Validator.isStructurallyValidScriptSubtag(script)) {
throw Errors.createRangeErrorInvalidScript(script);
}
}
@TruffleBoundary
public static void ensureIsStructurallyValidLanguageTag(String languageTag) {
try {
new Locale.Builder().setLanguageTag(languageTag).build();
} catch (IllformedLocaleException e) {
throw Errors.createRangeError(e.getMessage());
}
}
@TruffleBoundary
public static String validateAndCanonicalizeLanguageTag(String languageTag) {
try {
if (!UTS35Validator.isWellFormedUnicodeBCP47LocaleIdentifier(languageTag)) {
throw Errors.createRangeErrorFormat("Language tag is not well-formed: %s", null, languageTag);
}
Locale.Builder builder = new Locale.Builder().setLanguageTag(languageTag);
Locale locale = builder.build();
String language = locale.getLanguage();
if (!language.isEmpty()) {
String canonicalLanguage;
if ("mo".equals(language)) {
canonicalLanguage = "ro";
} else if ("cmn".equals(language)) {
canonicalLanguage = "zh";
} else {
if ("cnr".equals(language)) {
builder.setRegion("ME");
} else if ("sh".equals(language) && locale.getScript().isEmpty()) {
builder.setScript("Latn");
}
canonicalLanguage = ULocale.createCanonical(language).getLanguage();
}
builder.setLanguage(canonicalLanguage);
}
String region = locale.getCountry();
if (!region.isEmpty()) {
try {
Region icuRegion = Region.getInstance(region);
if (icuRegion.getType() == Region.RegionType.DEPRECATED) {
ULocale baseLocale = new ULocale.Builder().setLanguage(language).setScript(locale.getScript()).build();
String preferredRegionName = ULocale.addLikelySubtags(baseLocale).getCountry();
Region preferredRegion = Region.getInstance(preferredRegionName);
List<Region> replacements = icuRegion.getPreferredValues();
if (replacements.contains(preferredRegion)) {
icuRegion = preferredRegion;
} else {
icuRegion = replacements.get(0);
}
}
String canonicalRegion = icuRegion.toString();
builder.setRegion(canonicalRegion);
} catch (IllegalArgumentException iaex) {
}
}
String variant = locale.getVariant();
if (!variant.isEmpty()) {
String[] variants = variant.toLowerCase().split("[_-]");
for (int i = 0; i < variants.length; i++) {
if ("heploc".equals(variants[i])) {
variants[i] = "alalc97";
} else if ("arevela".equals(variants[i])) {
variants[i] = "";
} else if ("arevmda".equals(variants[i])) {
variants[i] = "";
if ("hy".equals(locale.getLanguage())) {
builder.setLanguage("hyw");
}
}
}
if (new HashSet<>(Arrays.asList(variants)).size() != variants.length) {
throw Errors.createRangeErrorFormat("Language tag with duplicate variants: %s", null, languageTag);
}
Arrays.sort(variants);
StringBuilder sb = new StringBuilder(variants[0]);
for (int i = 1; i < variants.length; i++) {
sb.append('-').append(variants[i]);
}
builder.setVariant(sb.toString());
}
Set<Character> extensions = locale.getExtensionKeys();
if (!extensions.isEmpty()) {
String tag = languageTag.toLowerCase();
int privateExtIdx = extensions.contains('x') ? tag.indexOf("-x-") : tag.length();
for (Character ext : extensions) {
if (ext != 'x') {
String extDelimiter = "-" + ext + "-";
int idx = tag.indexOf(extDelimiter);
int idx2 = tag.indexOf(extDelimiter, idx + 1);
if (idx2 != -1 && idx2 < privateExtIdx) {
throw Errors.createRangeErrorFormat("Language tag with duplicate singletons: %s", null, languageTag);
}
}
}
for (String key : locale.getUnicodeLocaleKeys()) {
String type = locale.getUnicodeLocaleType(key);
if ("true".equals(type)) {
type = "";
} else if ("yes".equals(type)) {
if ("kb".equals(key) || "kc".equals(key) || "kh".equals(key) || "kk".equals(key) || "kn".equals(key)) {
type = "";
}
} else if ("ca".equals(key)) {
type = normalizeCAType(type);
} else if ("ks".equals(key)) {
type = normalizeKSType(type);
} else if ("ms".equals(key)) {
type = normalizeMSType(type);
} else if ("rg".equals(key) || "sd".equals(key)) {
type = normalizeRGType(type);
} else if ("tz".equals(key)) {
type = normalizeTZType(type);
}
builder.setUnicodeLocaleKeyword(key, type);
}
String transformedExt = locale.getExtension('t');
if (transformedExt != null) {
builder.setExtension('t', normalizeTransformedExtension(transformedExt));
}
}
return maybeAppendMissingLanguageSubTag(builder.build().toLanguageTag());
} catch (IllformedLocaleException e) {
throw Errors.createRangeError(e.getMessage());
}
}
public static String normalizeCAType(String type) {
if ("gregorian".equals(type)) {
return "gregory";
} else if ("ethiopic-amete-alem".equals(type)) {
return "ethioaa";
} else if ("islamicc".equals(type)) {
return "islamic-civil";
}
return type;
}
private static String normalizeKSType(String type) {
if ("primary".equals(type)) {
return "level1";
} else if ("tertiary".equals(type)) {
return "level3";
}
return type;
}
private static String normalizeMSType(String type) {
if ("imperial".equals(type)) {
return "uksystem";
}
return type;
}
private static String normalizeRGType(String type) {
if ("cn11".equals(type)) {
return "cnbj";
} else if ("cz10a".equals(type)) {
return "cz110";
} else if ("fra".equals(type) || "frg".equals(type)) {
return "frges";
} else if ("lud".equals(type)) {
return "lucl";
} else if ("no23".equals(type)) {
return "no50";
}
return type;
}
private static String normalizeTZType(String type) {
if ("cnckg".equals(type)) {
return "cnsha";
} else if ("eire".equals(type)) {
return "iedub";
} else if ("est".equals(type)) {
return "utcw05";
} else if ("gmt0".equals(type)) {
return "gmt";
} else if ("uct".equals(type)) {
return "utc";
} else if ("zulu".equals(type)) {
return "utc";
}
return type;
}
private static String normalizeTransformedExtension(String extension) {
String tlang = null;
Map<String, String> fields = new TreeMap<>();
boolean seenDash = true;
String lastKey = null;
int lastValueStart = -1;
for (int i = 0; i < extension.length() - 1; i++) {
if (seenDash && UTS35Validator.isAlpha(extension.charAt(i)) && UTS35Validator.isDigit(extension.charAt(i + 1)) && (i + 2 == extension.length() || extension.charAt(i + 2) == '-')) {
if (lastKey == null) {
tlang = extension.substring(0, Math.max(0, i - 1));
} else {
fields.put(lastKey, extension.substring(lastValueStart, i - 1));
}
lastKey = extension.substring(i, i + 2);
lastValueStart = i + 3;
}
seenDash = (extension.charAt(i) == '-');
}
if (tlang == null) {
tlang = extension;
}
if (lastKey != null) {
fields.put(lastKey, extension.substring(lastValueStart));
}
StringBuilder normalized = new StringBuilder();
if (!tlang.isEmpty()) {
tlang = validateAndCanonicalizeLanguageTag(tlang);
normalized.append(tlang);
}
for (Map.Entry<String, String> entry : fields.entrySet()) {
if (normalized.length() != 0) {
normalized.append('-');
}
String value = entry.getValue();
if ("names".equalsIgnoreCase(value)) {
value = "prprname";
}
normalized.append(entry.getKey()).append('-').append(value);
}
return normalized.toString();
}
public static String maybeAppendMissingLanguageSubTag(String tag) {
return tag.startsWith("x-") ? ("und-" + tag) : tag;
}
@TruffleBoundary
public static String toUpperCase(String in) {
StringBuilder result = new StringBuilder(in.length());
for (int i = 0; i < in.length(); i++) {
int c = in.codePointAt(i);
if (c >= 'a' && c <= 'z') {
result.append((char) (c - 32));
} else {
result.append((char) c);
}
}
return result.toString();
}
@TruffleBoundary
public static String toLowerCase(JSContext ctx, String s, String[] locales) {
Locale strippedLocale = selectedLocaleStripped(ctx, locales);
StringBuilder result = new StringBuilder();
Lower tr = CaseMap.toLower();
tr.apply(strippedLocale, s, result, null);
return result.toString();
}
@TruffleBoundary
public static String toUpperCase(JSContext ctx, String s, String[] locales) {
Locale strippedLocale = selectedLocaleStripped(ctx, locales);
StringBuilder result = new StringBuilder();
Upper tr = CaseMap.toUpper();
tr.apply(strippedLocale, s, result, null);
return result.toString();
}
@TruffleBoundary
public static Locale selectedLocaleStripped(JSContext ctx, String[] locales) {
return IntlUtil.selectedLocale(ctx, locales).stripExtensions();
}
public static DynamicObject makePart(JSContext context, String type, String value) {
return makePart(context, type, value, null);
}
public static DynamicObject makePart(JSContext context, String type, String value, String unit) {
DynamicObject p = JSOrdinary.create(context);
JSObject.set(p, TYPE, type);
JSObject.set(p, VALUE, value);
if (unit != null) {
JSObject.set(p, UNIT, unit);
}
return p;
}
@TruffleBoundary
private static boolean isSanctionedSimpleUnitIdentifier(String unitIdentifier) {
return SANCTIONED_SIMPLE_UNIT_IDENTIFIERS.contains(unitIdentifier);
}
@TruffleBoundary
private static boolean isWellFormedUnitIdentifier(String unitIdentifier) {
if (isSanctionedSimpleUnitIdentifier(unitIdentifier)) {
return true;
}
String per = "-per-";
int index = unitIdentifier.indexOf(per);
if (index == -1) {
return false;
}
String numerator = unitIdentifier.substring(0, index);
String denominator = unitIdentifier.substring(index + per.length());
return isSanctionedSimpleUnitIdentifier(numerator) && isSanctionedSimpleUnitIdentifier(denominator);
}
public static void ensureIsWellFormedUnitIdentifier(String unitIdentifier) {
if (!isWellFormedUnitIdentifier(unitIdentifier)) {
throw Errors.createRangeErrorInvalidUnitIdentifier(unitIdentifier);
}
}
}