Path: blob/aarch64-shenandoah-jdk8u272-b10/jdk/make/src/classes/build/tools/cldrconverter/CLDRConverter.java
32287 views
/*1* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package build.tools.cldrconverter;2627import build.tools.cldrconverter.BundleGenerator.BundleType;28import java.io.File;29import java.nio.file.DirectoryStream;30import java.nio.file.FileSystems;31import java.nio.file.Files;32import java.nio.file.Path;33import java.util.*;34import javax.xml.parsers.SAXParser;35import javax.xml.parsers.SAXParserFactory;36import org.xml.sax.SAXNotRecognizedException;37import org.xml.sax.SAXNotSupportedException;383940/**41* Converts locale data from "Locale Data Markup Language" format to42* JRE resource bundle format. LDML is the format used by the Common43* Locale Data Repository maintained by the Unicode Consortium.44*/45public class CLDRConverter {4647static final String LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldml.dtd";48static final String SPPL_LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldmlSupplemental.dtd";4950private static String CLDR_BASE = "../CLDR/21.0.1/";51static String LOCAL_LDML_DTD;52static String LOCAL_SPPL_LDML_DTD;53private static String SOURCE_FILE_DIR;54private static String SPPL_SOURCE_FILE;55private static String NUMBERING_SOURCE_FILE;56private static String METAZONES_SOURCE_FILE;57static String DESTINATION_DIR = "build/gensrc";5859static final String LOCALE_NAME_PREFIX = "locale.displayname.";60static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol.";61static final String CURRENCY_NAME_PREFIX = "currency.displayname.";62static final String CALENDAR_NAME_PREFIX = "calendarname.";63static final String TIMEZONE_ID_PREFIX = "timezone.id.";64static final String ZONE_NAME_PREFIX = "timezone.displayname.";65static final String METAZONE_ID_PREFIX = "metazone.id.";6667private static SupplementDataParseHandler handlerSuppl;68static NumberingSystemsParseHandler handlerNumbering;69static MetaZonesParseHandler handlerMetaZones;70private static BundleGenerator bundleGenerator;7172static enum DraftType {73UNCONFIRMED,74PROVISIONAL,75CONTRIBUTED,76APPROVED;7778private static final Map<String, DraftType> map = new HashMap<>();79static {80for (DraftType dt : values()) {81map.put(dt.getKeyword(), dt);82}83}84static private DraftType defaultType = CONTRIBUTED;8586private final String keyword;8788private DraftType() {89keyword = this.name().toLowerCase(Locale.ROOT);9091}9293static DraftType forKeyword(String keyword) {94return map.get(keyword);95}9697static DraftType getDefault() {98return defaultType;99}100101static void setDefault(String keyword) {102defaultType = Objects.requireNonNull(forKeyword(keyword));103}104105String getKeyword() {106return keyword;107}108}109110static boolean USE_UTF8 = false;111private static boolean verbose;112113private CLDRConverter() {114// no instantiation115}116117@SuppressWarnings("AssignmentToForLoopParameter")118public static void main(String[] args) throws Exception {119if (args.length != 0) {120String currentArg = null;121try {122for (int i = 0; i < args.length; i++) {123currentArg = args[i];124switch (currentArg) {125case "-draft":126String draftDataType = args[++i];127try {128DraftType.setDefault(draftDataType);129} catch (NullPointerException e) {130severe("Error: incorrect draft value: %s%n", draftDataType);131System.exit(1);132}133info("Using the specified data type: %s%n", draftDataType);134break;135136case "-base":137// base directory for input files138CLDR_BASE = args[++i];139if (!CLDR_BASE.endsWith("/")) {140CLDR_BASE += "/";141}142break;143144case "-o":145// output directory146DESTINATION_DIR = args[++i];147break;148149case "-utf8":150USE_UTF8 = true;151break;152153case "-verbose":154verbose = true;155break;156157case "-help":158usage();159System.exit(0);160break;161162default:163throw new RuntimeException();164}165}166} catch (RuntimeException e) {167severe("unknown or imcomplete arg(s): " + currentArg);168usage();169System.exit(1);170}171}172173// Set up path names174LOCAL_LDML_DTD = CLDR_BASE + "common/dtd/ldml.dtd";175LOCAL_SPPL_LDML_DTD = CLDR_BASE + "common/dtd/ldmlSupplemental.dtd";176SOURCE_FILE_DIR = CLDR_BASE + "common/main";177SPPL_SOURCE_FILE = CLDR_BASE + "common/supplemental/supplementalData.xml";178NUMBERING_SOURCE_FILE = CLDR_BASE + "common/supplemental/numberingSystems.xml";179METAZONES_SOURCE_FILE = CLDR_BASE + "common/supplemental/metaZones.xml";180181bundleGenerator = new ResourceBundleGenerator();182183List<Bundle> bundles = readBundleList();184convertBundles(bundles);185}186187private static void usage() {188errout("Usage: java CLDRConverter [options]%n"189+ "\t-help output this usage message and exit%n"190+ "\t-verbose output information%n"191+ "\t-draft [approved | provisional | unconfirmed]%n"192+ "\t\t draft level for using data (default: approved)%n"193+ "\t-base dir base directory for CLDR input files%n"194+ "\t-o dir output directory (defaut: ./build/gensrc)%n"195+ "\t-utf8 use UTF-8 rather than \\uxxxx (for debug)%n");196}197198static void info(String fmt, Object... args) {199if (verbose) {200System.out.printf(fmt, args);201}202}203204static void info(String msg) {205if (verbose) {206System.out.println(msg);207}208}209210static void warning(String fmt, Object... args) {211System.err.print("Warning: ");212System.err.printf(fmt, args);213}214215static void warning(String msg) {216System.err.print("Warning: ");217errout(msg);218}219220static void severe(String fmt, Object... args) {221System.err.print("Error: ");222System.err.printf(fmt, args);223}224225static void severe(String msg) {226System.err.print("Error: ");227errout(msg);228}229230private static void errout(String msg) {231if (msg.contains("%n")) {232System.err.printf(msg);233} else {234System.err.println(msg);235}236}237238/**239* Configure the parser to allow access to DTDs on the file system.240*/241private static void enableFileAccess(SAXParser parser) throws SAXNotSupportedException {242try {243parser.setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "file");244} catch (SAXNotRecognizedException ignore) {245// property requires >= JAXP 1.5246}247}248249private static List<Bundle> readBundleList() throws Exception {250ResourceBundle.Control defCon = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);251List<Bundle> retList = new ArrayList<>();252Path path = FileSystems.getDefault().getPath(SOURCE_FILE_DIR);253try (DirectoryStream<Path> dirStr = Files.newDirectoryStream(path)) {254for (Path entry : dirStr) {255String fileName = entry.getFileName().toString();256if (fileName.endsWith(".xml")) {257String id = fileName.substring(0, fileName.indexOf('.'));258Locale cldrLoc = Locale.forLanguageTag(toLanguageTag(id));259List<Locale> candList = defCon.getCandidateLocales("", cldrLoc);260StringBuilder sb = new StringBuilder();261for (Locale loc : candList) {262if (!loc.equals(Locale.ROOT)) {263sb.append(toLocaleName(loc.toLanguageTag()));264sb.append(",");265}266}267if (sb.indexOf("root") == -1) {268sb.append("root");269}270Bundle b = new Bundle(id, sb.toString(), null, null);271// Insert the bundle for en at the top so that it will get272// processed first.273if ("en".equals(id)) {274retList.add(0, b);275} else {276retList.add(b);277}278}279}280}281return retList;282}283284private static Map<String, Map<String, Object>> cldrBundles = new HashMap<>();285286static Map<String, Object> getCLDRBundle(String id) throws Exception {287Map<String, Object> bundle = cldrBundles.get(id);288if (bundle != null) {289return bundle;290}291SAXParserFactory factory = SAXParserFactory.newInstance();292factory.setValidating(true);293SAXParser parser = factory.newSAXParser();294enableFileAccess(parser);295LDMLParseHandler handler = new LDMLParseHandler(id);296File file = new File(SOURCE_FILE_DIR + File.separator + id + ".xml");297if (!file.exists()) {298// Skip if the file doesn't exist.299return Collections.emptyMap();300}301302info("..... main directory .....");303info("Reading file " + file);304parser.parse(file, handler);305306bundle = handler.getData();307cldrBundles.put(id, bundle);308String country = getCountryCode(id);309if (country != null) {310bundle = handlerSuppl.getData(country);311if (bundle != null) {312//merge two maps into one map313Map<String, Object> temp = cldrBundles.remove(id);314bundle.putAll(temp);315cldrBundles.put(id, bundle);316}317}318return bundle;319}320321private static void convertBundles(List<Bundle> bundles) throws Exception {322// Parse SupplementalData file and store the information in the HashMap323// Calendar information such as firstDay and minDay are stored in324// supplementalData.xml as of CLDR1.4. Individual territory is listed325// with its ISO 3166 country code while default is listed using UNM49326// region and composition numerical code (001 for World.)327SAXParserFactory factorySuppl = SAXParserFactory.newInstance();328factorySuppl.setValidating(true);329SAXParser parserSuppl = factorySuppl.newSAXParser();330enableFileAccess(parserSuppl);331handlerSuppl = new SupplementDataParseHandler();332File fileSupply = new File(SPPL_SOURCE_FILE);333parserSuppl.parse(fileSupply, handlerSuppl);334335// Parse numberingSystems to get digit zero character information.336SAXParserFactory numberingParser = SAXParserFactory.newInstance();337numberingParser.setValidating(true);338SAXParser parserNumbering = numberingParser.newSAXParser();339enableFileAccess(parserNumbering);340handlerNumbering = new NumberingSystemsParseHandler();341File fileNumbering = new File(NUMBERING_SOURCE_FILE);342parserNumbering.parse(fileNumbering, handlerNumbering);343344// Parse metaZones to create mappings between Olson tzids and CLDR meta zone names345SAXParserFactory metazonesParser = SAXParserFactory.newInstance();346metazonesParser.setValidating(true);347SAXParser parserMetaZones = metazonesParser.newSAXParser();348enableFileAccess(parserMetaZones);349handlerMetaZones = new MetaZonesParseHandler();350File fileMetaZones = new File(METAZONES_SOURCE_FILE);351parserNumbering.parse(fileMetaZones, handlerMetaZones);352353// For generating information on supported locales.354Map<String, SortedSet<String>> metaInfo = new HashMap<>();355metaInfo.put("LocaleNames", new TreeSet<String>());356metaInfo.put("CurrencyNames", new TreeSet<String>());357metaInfo.put("TimeZoneNames", new TreeSet<String>());358metaInfo.put("CalendarData", new TreeSet<String>());359metaInfo.put("FormatData", new TreeSet<String>());360361for (Bundle bundle : bundles) {362// Get the target map, which contains all the data that should be363// visible for the bundle's locale364365Map<String, Object> targetMap = bundle.getTargetMap();366367EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes();368369// Fill in any missing resources in the base bundle from en and en-US data.370// This is because CLDR root.xml is supposed to be language neutral and doesn't371// provide some resource data. Currently, the runtime assumes that there are all372// resources though the parent resource bundle chain.373if (bundle.isRoot()) {374Map<String, Object> enData = new HashMap<>();375// Create a superset of en-US and en bundles data in order to376// fill in any missing resources in the base bundle.377enData.putAll(Bundle.getBundle("en").getTargetMap());378enData.putAll(Bundle.getBundle("en_US").getTargetMap());379for (String key : enData.keySet()) {380if (!targetMap.containsKey(key)) {381targetMap.put(key, enData.get(key));382}383}384// Add DateTimePatternChars because CLDR no longer supports localized patterns.385targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ");386}387388// Now the map contains just the entries that need to be in the resources bundles.389// Go ahead and generate them.390if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) {391Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID());392if (!localeNamesMap.isEmpty() || bundle.isRoot()) {393metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID()));394bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, BundleType.OPEN);395}396}397if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) {398Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies());399if (!currencyNamesMap.isEmpty() || bundle.isRoot()) {400metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID()));401bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, BundleType.OPEN);402}403}404if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) {405Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID());406if (!zoneNamesMap.isEmpty() || bundle.isRoot()) {407metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID()));408bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getID(), true, zoneNamesMap, BundleType.TIMEZONE);409}410}411if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) {412Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID());413if (!calendarDataMap.isEmpty() || bundle.isRoot()) {414metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID()));415bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, BundleType.PLAIN);416}417}418if (bundleTypes.contains(Bundle.Type.FORMATDATA)) {419Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID());420// LocaleData.getAvailableLocales depends on having FormatData bundles around421if (!formatDataMap.isEmpty() || bundle.isRoot()) {422metaInfo.get("FormatData").add(toLanguageTag(bundle.getID()));423bundleGenerator.generateBundle("text", "FormatData", bundle.getID(), true, formatDataMap, BundleType.PLAIN);424}425}426427// For testing428SortedSet<String> allLocales = new TreeSet<>();429allLocales.addAll(metaInfo.get("CurrencyNames"));430allLocales.addAll(metaInfo.get("LocaleNames"));431allLocales.addAll(metaInfo.get("CalendarData"));432allLocales.addAll(metaInfo.get("FormatData"));433metaInfo.put("All", allLocales);434}435436bundleGenerator.generateMetaInfo(metaInfo);437}438439/*440* Returns the language portion of the given id.441* If id is "root", "" is returned.442*/443static String getLanguageCode(String id) {444int index = id.indexOf('_');445String lang = null;446if (index != -1) {447lang = id.substring(0, index);448} else {449lang = "root".equals(id) ? "" : id;450}451return lang;452}453454/**455* Examine if the id includes the country (territory) code. If it does, it returns456* the country code.457* Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".458*/459private static String getCountryCode(String id) {460//Truncate a variant code with '@' if there is any461//(eg. de_DE@collation=phonebook,currency=DOM)462if (id.indexOf('@') != -1) {463id = id.substring(0, id.indexOf('@'));464}465String[] tokens = id.split("_");466for (int index = 1; index < tokens.length; ++index) {467if (tokens[index].length() == 2468&& Character.isLetter(tokens[index].charAt(0))469&& Character.isLetter(tokens[index].charAt(1))) {470return tokens[index];471}472}473return null;474}475476private static class KeyComparator implements Comparator<String> {477static KeyComparator INSTANCE = new KeyComparator();478479private KeyComparator() {480}481482@Override483public int compare(String o1, String o2) {484int len1 = o1.length();485int len2 = o2.length();486if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) {487// Shorter string comes first unless either starts with a digit.488if (len1 < len2) {489return -1;490}491if (len1 > len2) {492return 1;493}494}495return o1.compareTo(o2);496}497498private boolean isDigit(char c) {499return c >= '0' && c <= '9';500}501}502503private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) {504Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE);505for (String key : map.keySet()) {506if (key.startsWith(LOCALE_NAME_PREFIX)) {507localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key));508}509}510return localeNames;511}512513@SuppressWarnings("AssignmentToForLoopParameter")514private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names)515throws Exception {516Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE);517for (String key : map.keySet()) {518if (key.startsWith(CURRENCY_NAME_PREFIX)) {519currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key));520} else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) {521currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key));522}523}524return currencyNames;525}526527private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) {528Map<String, Object> names = new HashMap<>();529for (String tzid : handlerMetaZones.keySet()) {530String tzKey = TIMEZONE_ID_PREFIX + tzid;531Object data = map.get(tzKey);532if (data instanceof String[]) {533names.put(tzid, data);534} else {535String meta = handlerMetaZones.get(tzid);536if (meta != null) {537String metaKey = METAZONE_ID_PREFIX + meta;538data = map.get(metaKey);539if (data instanceof String[]) {540// Keep the metazone prefix here.541names.put(metaKey, data);542names.put(tzid, meta);543}544}545}546}547return names;548}549550private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) {551Map<String, Object> calendarData = new LinkedHashMap<>();552copyIfPresent(map, "firstDayOfWeek", calendarData);553copyIfPresent(map, "minimalDaysInFirstWeek", calendarData);554return calendarData;555}556557static final String[] FORMAT_DATA_ELEMENTS = {558"MonthNames",559"standalone.MonthNames",560"MonthAbbreviations",561"standalone.MonthAbbreviations",562"MonthNarrows",563"standalone.MonthNarrows",564"DayNames",565"standalone.DayNames",566"DayAbbreviations",567"standalone.DayAbbreviations",568"DayNarrows",569"standalone.DayNarrows",570"QuarterNames",571"standalone.QuarterNames",572"QuarterAbbreviations",573"standalone.QuarterAbbreviations",574"QuarterNarrows",575"standalone.QuarterNarrows",576"AmPmMarkers",577"narrow.AmPmMarkers",578"long.Eras",579"Eras",580"narrow.Eras",581"field.era",582"field.year",583"field.month",584"field.week",585"field.weekday",586"field.dayperiod",587"field.hour",588"field.minute",589"field.second",590"field.zone",591"TimePatterns",592"DatePatterns",593"DateTimePatterns",594"DateTimePatternChars"595};596597private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) {598Map<String, Object> formatData = new LinkedHashMap<>();599for (CalendarType calendarType : CalendarType.values()) {600String prefix = calendarType.keyElementName();601for (String element : FORMAT_DATA_ELEMENTS) {602String key = prefix + element;603copyIfPresent(map, "java.time." + key, formatData);604copyIfPresent(map, key, formatData);605}606}607// Workaround for islamic-umalqura name support (JDK-8015986)608switch (id) {609case "ar":610map.put(CLDRConverter.CALENDAR_NAME_PREFIX611+ CalendarType.ISLAMIC_UMALQURA.lname(),612// derived from CLDR 24 draft613"\u0627\u0644\u062a\u0642\u0648\u064a\u0645 "614+"\u0627\u0644\u0625\u0633\u0644\u0627\u0645\u064a "615+"[\u0623\u0645 \u0627\u0644\u0642\u0631\u0649]");616break;617case "en":618map.put(CLDRConverter.CALENDAR_NAME_PREFIX619+ CalendarType.ISLAMIC_UMALQURA.lname(),620// derived from CLDR 24 draft621"Islamic Calendar [Umm al-Qura]");622break;623}624// Copy available calendar names625for (String key : map.keySet()) {626if (key.startsWith(CLDRConverter.CALENDAR_NAME_PREFIX)) {627String type = key.substring(CLDRConverter.CALENDAR_NAME_PREFIX.length());628for (CalendarType calendarType : CalendarType.values()) {629if (type.equals(calendarType.lname())) {630Object value = map.get(key);631formatData.put(key, value);632String ukey = CLDRConverter.CALENDAR_NAME_PREFIX + calendarType.uname();633if (!key.equals(ukey)) {634formatData.put(ukey, value);635}636}637}638}639}640641copyIfPresent(map, "DefaultNumberingSystem", formatData);642643@SuppressWarnings("unchecked")644List<String> numberingScripts = (List<String>) map.remove("numberingScripts");645if (numberingScripts != null) {646for (String script : numberingScripts) {647copyIfPresent(map, script + "." + "NumberElements", formatData);648}649} else {650copyIfPresent(map, "NumberElements", formatData);651}652copyIfPresent(map, "NumberPatterns", formatData);653return formatData;654}655656private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) {657Object value = src.get(key);658if (value != null) {659dest.put(key, value);660}661}662663// --- code below here is adapted from java.util.Properties ---664private static final String specialSaveCharsJava = "\"";665private static final String specialSaveCharsProperties = "=: \t\r\n\f#!";666667/*668* Converts unicodes to encoded \uxxxx669* and writes out any of the characters in specialSaveChars670* with a preceding slash671*/672static String saveConvert(String theString, boolean useJava) {673if (theString == null) {674return "";675}676677String specialSaveChars;678if (useJava) {679specialSaveChars = specialSaveCharsJava;680} else {681specialSaveChars = specialSaveCharsProperties;682}683boolean escapeSpace = false;684685int len = theString.length();686StringBuilder outBuffer = new StringBuilder(len * 2);687Formatter formatter = new Formatter(outBuffer, Locale.ROOT);688689for (int x = 0; x < len; x++) {690char aChar = theString.charAt(x);691switch (aChar) {692case ' ':693if (x == 0 || escapeSpace) {694outBuffer.append('\\');695}696outBuffer.append(' ');697break;698case '\\':699outBuffer.append('\\');700outBuffer.append('\\');701break;702case '\t':703outBuffer.append('\\');704outBuffer.append('t');705break;706case '\n':707outBuffer.append('\\');708outBuffer.append('n');709break;710case '\r':711outBuffer.append('\\');712outBuffer.append('r');713break;714case '\f':715outBuffer.append('\\');716outBuffer.append('f');717break;718default:719if (aChar < 0x0020 || (!USE_UTF8 && aChar > 0x007e)) {720formatter.format("\\u%04x", (int)aChar);721} else {722if (specialSaveChars.indexOf(aChar) != -1) {723outBuffer.append('\\');724}725outBuffer.append(aChar);726}727}728}729return outBuffer.toString();730}731732private static String toLanguageTag(String locName) {733if (locName.indexOf('_') == -1) {734return locName;735}736String tag = locName.replaceAll("_", "-");737Locale loc = Locale.forLanguageTag(tag);738return loc.toLanguageTag();739}740741private static String toLocaleName(String tag) {742if (tag.indexOf('-') == -1) {743return tag;744}745return tag.replaceAll("-", "_");746}747}748749750