1 /* 2 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package build.tools.cldrconverter; 27 28 import java.io.File; 29 import java.nio.file.DirectoryStream; 30 import java.nio.file.FileSystems; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.util.*; 34 import javax.xml.parsers.SAXParser; 35 import javax.xml.parsers.SAXParserFactory; 36 37 38 /** 39 * Converts locale data from "Locale Data Markup Language" format to 40 * JRE resource bundle format. LDML is the format used by the Common 41 * Locale Data Repository maintained by the Unicode Consortium. 42 */ 43 public class CLDRConverter { 44 45 static final String LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldml.dtd"; 46 static final String SPPL_LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldmlSupplemental.dtd"; 47 48 private static String CLDR_BASE = "../CLDR/21.0.1/"; 49 static String LOCAL_LDML_DTD; 50 static String LOCAL_SPPL_LDML_DTD; 51 private static String SOURCE_FILE_DIR; 52 private static String SPPL_SOURCE_FILE; 53 private static String NUMBERING_SOURCE_FILE; 54 private static String METAZONES_SOURCE_FILE; 55 static String DESTINATION_DIR = "build/gensrc"; 56 57 static final String LOCALE_NAME_PREFIX = "locale.displayname."; 58 static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol."; 59 static final String CURRENCY_NAME_PREFIX = "currency.displayname."; 60 static final String TIMEZONE_ID_PREFIX = "timezone.id."; 61 static final String TIMEZONE_NAME_PREFIX = "timezone.displayname."; 62 static final String METAZONE_ID_PREFIX = "metazone.id."; 63 static final String METAZONE_NAME_PREFIX = "metazone.displayname."; 64 65 private static SupplementDataParseHandler handlerSuppl; 66 static NumberingSystemsParseHandler handlerNumbering; 67 static MetaZonesParseHandler handlerMetaZones; 68 private static BundleGenerator bundleGenerator; 69 70 static int draftType; 71 private static final String DRAFT_UNCONFIRMED = "unconfirmed"; 72 private static final String DRAFT_PROVISIONAL = "provisional"; 73 private static final String DRAFT_CONTRIBUTED = "contributed"; 74 private static final String DRAFT_APPROVED = "approved"; 75 private static final String DRAFT_TRUE = "true"; 76 private static final String DRAFT_FALSE = "false"; 77 private static final String DRAFT_DEFAULT = DRAFT_APPROVED; 78 static final Map<String, Integer> DRAFT_MAP = new HashMap<>(); 79 80 static { 81 DRAFT_MAP.put(DRAFT_UNCONFIRMED, 0); 82 DRAFT_MAP.put(DRAFT_PROVISIONAL, 1); 83 DRAFT_MAP.put(DRAFT_CONTRIBUTED, 2); 84 DRAFT_MAP.put(DRAFT_APPROVED, 3); 85 DRAFT_MAP.put(DRAFT_TRUE, 0); 86 DRAFT_MAP.put(DRAFT_FALSE, 2); 87 draftType = DRAFT_MAP.get(DRAFT_DEFAULT); 88 }; 89 90 static boolean USE_UTF8 = false; 91 private static boolean verbose; 92 93 private CLDRConverter() { 94 // no instantiation 95 } 96 97 @SuppressWarnings("AssignmentToForLoopParameter") 98 public static void main(String[] args) throws Exception { 99 if (args.length != 0) { 100 String currentArg = null; 101 try { 102 for (int i = 0; i < args.length; i++) { 103 currentArg = args[i]; 104 switch (currentArg) { 105 case "-draft": 106 String draftDataType = args[++i]; 107 try { 108 draftType = DRAFT_MAP.get(draftDataType); 109 } catch (NullPointerException e) { 110 severe("Error: incorrect draft value: %s%n", draftDataType); 111 System.exit(1); 112 } 113 info("Using the specified data type: %s%n", draftDataType); 114 break; 115 116 case "-base": 117 // base directory for input files 118 CLDR_BASE = args[++i]; 119 if (!CLDR_BASE.endsWith("/")) { 120 CLDR_BASE += "/"; 121 } 122 break; 123 124 case "-o": 125 // output directory 126 DESTINATION_DIR = args[++i]; 127 break; 128 129 case "-utf8": 130 USE_UTF8 = true; 131 break; 132 133 case "-verbose": 134 verbose = true; 135 break; 136 137 case "-help": 138 usage(); 139 System.exit(0); 140 break; 141 142 default: 143 throw new RuntimeException(); 144 } 145 } 146 } catch (RuntimeException e) { 147 severe("unknown or imcomplete arg(s): " + currentArg); 148 usage(); 149 System.exit(1); 150 } 151 } 152 153 // Set up path names 154 LOCAL_LDML_DTD = CLDR_BASE + "common/dtd/ldml.dtd"; 155 LOCAL_SPPL_LDML_DTD = CLDR_BASE + "common/dtd/ldmlSupplemental.dtd"; 156 SOURCE_FILE_DIR = CLDR_BASE + "common/main"; 157 SPPL_SOURCE_FILE = CLDR_BASE + "common/supplemental/supplementalData.xml"; 158 NUMBERING_SOURCE_FILE = CLDR_BASE + "common/supplemental/numberingSystems.xml"; 159 METAZONES_SOURCE_FILE = CLDR_BASE + "common/supplemental/metaZones.xml"; 160 161 bundleGenerator = new ResourceBundleGenerator(); 162 163 List<Bundle> bundles = readBundleList(); 164 convertBundles(bundles); 165 } 166 167 private static void usage() { 168 errout("Usage: java CLDRConverter [options]%n" 169 + "\t-help output this usage message and exit%n" 170 + "\t-verbose output information%n" 171 + "\t-draft [approved | provisional | unconfirmed]%n" 172 + "\t\t draft level for using data (default: approved)%n" 173 + "\t-base dir base directory for CLDR input files%n" 174 + "\t-o dir output directory (defaut: ./build/gensrc)%n" 175 + "\t-utf8 use UTF-8 rather than \\uxxxx (for debug)%n"); 176 } 177 178 static void info(String fmt, Object... args) { 179 if (verbose) { 180 System.out.printf(fmt, args); 181 } 182 } 183 184 static void info(String msg) { 185 if (verbose) { 186 System.out.println(msg); 187 } 188 } 189 190 static void warning(String fmt, Object... args) { 191 System.err.print("Warning: "); 192 System.err.printf(fmt, args); 193 } 194 195 static void warning(String msg) { 196 System.err.print("Warning: "); 197 errout(msg); 198 } 199 200 static void severe(String fmt, Object... args) { 201 System.err.print("Error: "); 202 System.err.printf(fmt, args); 203 } 204 205 static void severe(String msg) { 206 System.err.print("Error: "); 207 errout(msg); 208 } 209 210 private static void errout(String msg) { 211 if (msg.contains("%n")) { 212 System.err.printf(msg); 213 } else { 214 System.err.println(msg); 215 } 216 } 217 218 private static List<Bundle> readBundleList() throws Exception { 219 ResourceBundle.Control defCon = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT); 220 List<Bundle> retList = new ArrayList<>(); 221 Path path = FileSystems.getDefault().getPath(SOURCE_FILE_DIR); 222 try (DirectoryStream<Path> dirStr = Files.newDirectoryStream(path)) { 223 for (Path entry : dirStr) { 224 String fileName = entry.getFileName().toString(); 225 if (fileName.endsWith(".xml")) { 226 String id = fileName.substring(0, fileName.indexOf('.')); 227 Locale cldrLoc = Locale.forLanguageTag(toLanguageTag(id)); 228 List<Locale> candList = defCon.getCandidateLocales("", cldrLoc); 229 StringBuilder sb = new StringBuilder(); 230 for (Locale loc : candList) { 231 if (!loc.equals(Locale.ROOT)) { 232 sb.append(toLocaleName(loc.toLanguageTag())); 233 sb.append(","); 234 } 235 } 236 if (sb.indexOf("root") == -1) { 237 sb.append("root"); 238 } 239 retList.add(new Bundle(id, sb.toString(), null, null)); 240 } 241 } 242 } 243 return retList; 244 } 245 246 private static Map<String, Map<String, Object>> cldrBundles = new HashMap<>(); 247 248 static Map<String, Object> getCLDRBundle(String id) throws Exception { 249 Map<String, Object> bundle = cldrBundles.get(id); 250 if (bundle != null) { 251 return bundle; 252 } 253 SAXParserFactory factory = SAXParserFactory.newInstance(); 254 factory.setValidating(true); 255 SAXParser parser = factory.newSAXParser(); 256 LDMLParseHandler handler = new LDMLParseHandler(id); 257 File file = new File(SOURCE_FILE_DIR + File.separator + id + ".xml"); 258 if (!file.exists()) { 259 // Skip if the file doesn't exist. 260 return Collections.emptyMap(); 261 } 262 263 info("..... main directory ....."); 264 info("Reading file " + file); 265 parser.parse(file, handler); 266 267 bundle = handler.getData(); 268 cldrBundles.put(id, bundle); 269 String country = getCountryCode(id); 270 if (country != null) { 271 bundle = handlerSuppl.getData(country); 272 if (bundle != null) { 273 //merge two maps into one map 274 Map<String, Object> temp = cldrBundles.remove(id); 275 bundle.putAll(temp); 276 cldrBundles.put(id, bundle); 277 } 278 } 279 return bundle; 280 } 281 282 private static void convertBundles(List<Bundle> bundles) throws Exception { 283 // Parse SupplementalData file and store the information in the HashMap 284 // Calendar information such as firstDay and minDay are stored in 285 // supplementalData.xml as of CLDR1.4. Individual territory is listed 286 // with its ISO 3166 country code while default is listed using UNM49 287 // region and composition numerical code (001 for World.) 288 SAXParserFactory factorySuppl = SAXParserFactory.newInstance(); 289 factorySuppl.setValidating(true); 290 SAXParser parserSuppl = factorySuppl.newSAXParser(); 291 handlerSuppl = new SupplementDataParseHandler(); 292 File fileSupply = new File(SPPL_SOURCE_FILE); 293 parserSuppl.parse(fileSupply, handlerSuppl); 294 295 // Parse numberingSystems to get digit zero character information. 296 SAXParserFactory numberingParser = SAXParserFactory.newInstance(); 297 numberingParser.setValidating(true); 298 SAXParser parserNumbering = numberingParser.newSAXParser(); 299 handlerNumbering = new NumberingSystemsParseHandler(); 300 File fileNumbering = new File(NUMBERING_SOURCE_FILE); 301 parserNumbering.parse(fileNumbering, handlerNumbering); 302 303 // Parse metaZones to create mappings between Olson tzids and CLDR meta zone names 304 SAXParserFactory metazonesParser = SAXParserFactory.newInstance(); 305 metazonesParser.setValidating(true); 306 SAXParser parserMetaZones = metazonesParser.newSAXParser(); 307 handlerMetaZones = new MetaZonesParseHandler(); 308 File fileMetaZones = new File(METAZONES_SOURCE_FILE); 309 parserNumbering.parse(fileMetaZones, handlerMetaZones); 310 311 // For generating information on supported locales. 312 Map<String, SortedSet<String>> metaInfo = new HashMap<>(); 313 metaInfo.put("LocaleNames", new TreeSet<String>()); 314 metaInfo.put("CurrencyNames", new TreeSet<String>()); 315 metaInfo.put("CalendarData", new TreeSet<String>()); 316 metaInfo.put("FormatData", new TreeSet<String>()); 317 318 for (Bundle bundle : bundles) { 319 // Get the target map, which contains all the data that should be 320 // visible for the bundle's locale 321 322 Map<String, Object> targetMap = bundle.getTargetMap(); 323 324 EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes(); 325 326 // Fill in any missing resources in the base bundle from en and en-US data. 327 // This is because CLDR root.xml is supposed to be language neutral and doesn't 328 // provide some resource data. Currently, the runtime assumes that there are all 329 // resources though the parent resource bundle chain. 330 if (bundle.isRoot()) { 331 Map<String, Object> enData = new HashMap<>(); 332 // Create a superset of en-US and en bundles data in order to 333 // fill in any missing resources in the base bundle. 334 enData.putAll(Bundle.getBundle("en").getTargetMap()); 335 enData.putAll(Bundle.getBundle("en_US").getTargetMap()); 336 for (String key : enData.keySet()) { 337 if (!targetMap.containsKey(key)) { 338 targetMap.put(key, enData.get(key)); 339 } 340 } 341 // Add DateTimePatternChars because CLDR no longer supports localized patterns. 342 targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ"); 343 } 344 345 // Now the map contains just the entries that need to be in the resources bundles. 346 // Go ahead and generate them. 347 if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) { 348 Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID()); 349 if (!localeNamesMap.isEmpty() || bundle.isRoot()) { 350 metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID())); 351 bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, true); 352 } 353 } 354 if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) { 355 Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies()); 356 if (!currencyNamesMap.isEmpty() || bundle.isRoot()) { 357 metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID())); 358 bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, true); 359 } 360 } 361 if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) { 362 Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID()); 363 } 364 if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) { 365 Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID()); 366 if (!calendarDataMap.isEmpty() || bundle.isRoot()) { 367 metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID())); 368 bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, false); 369 } 370 } 371 if (bundleTypes.contains(Bundle.Type.FORMATDATA)) { 372 Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID()); 373 // LocaleData.getAvailableLocales depends on having FormatData bundles around 374 if (!formatDataMap.isEmpty() || bundle.isRoot()) { 375 metaInfo.get("FormatData").add(toLanguageTag(bundle.getID())); 376 bundleGenerator.generateBundle("text", "FormatData", bundle.getID(), true, formatDataMap, false); 377 } 378 } 379 // For testing 380 SortedSet<String> allLocales = new TreeSet<>(); 381 allLocales.addAll(metaInfo.get("CurrencyNames")); 382 allLocales.addAll(metaInfo.get("LocaleNames")); 383 allLocales.addAll(metaInfo.get("CalendarData")); 384 allLocales.addAll(metaInfo.get("FormatData")); 385 metaInfo.put("All", allLocales); 386 } 387 388 bundleGenerator.generateMetaInfo(metaInfo); 389 } 390 391 /* 392 * Returns the language portion of the given id. 393 * If id is "root", "" is returned. 394 */ 395 static String getLanguageCode(String id) { 396 int index = id.indexOf('_'); 397 String lang = null; 398 if (index != -1) { 399 lang = id.substring(0, index); 400 } else { 401 lang = "root".equals(id) ? "" : id; 402 } 403 return lang; 404 } 405 406 /** 407 * Examine if the id includes the country (territory) code. If it does, it returns 408 * the country code. 409 * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG". 410 */ 411 private static String getCountryCode(String id) { 412 //Truncate a variant code with '@' if there is any 413 //(eg. de_DE@collation=phonebook,currency=DOM) 414 if (id.indexOf('@') != -1) { 415 id = id.substring(0, id.indexOf('@')); 416 } 417 String[] tokens = id.split("_"); 418 for (int index = 1; index < tokens.length; ++index) { 419 if (tokens[index].length() == 2 420 && Character.isLetter(tokens[index].charAt(0)) 421 && Character.isLetter(tokens[index].charAt(1))) { 422 return tokens[index]; 423 } 424 } 425 return null; 426 } 427 428 private static class KeyComparator implements Comparator<String> { 429 static KeyComparator INSTANCE = new KeyComparator(); 430 431 private KeyComparator() { 432 } 433 434 public int compare(String o1, String o2) { 435 int len1 = o1.length(); 436 int len2 = o2.length(); 437 if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) { 438 // Shorter string comes first unless either starts with a digit. 439 if (len1 < len2) { 440 return -1; 441 } 442 if (len1 > len2) { 443 return 1; 444 } 445 } 446 return o1.compareTo(o2); 447 } 448 449 private boolean isDigit(char c) { 450 return c >= '0' && c <= '9'; 451 } 452 } 453 454 private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) { 455 Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE); 456 for (String key : map.keySet()) { 457 if (key.startsWith(LOCALE_NAME_PREFIX)) { 458 localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key)); 459 } 460 } 461 return localeNames; 462 } 463 464 @SuppressWarnings("AssignmentToForLoopParameter") 465 private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names) 466 throws Exception { 467 Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE); 468 for (String key : map.keySet()) { 469 if (key.startsWith(CURRENCY_NAME_PREFIX)) { 470 currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key)); 471 } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) { 472 currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key)); 473 } 474 } 475 return currencyNames; 476 } 477 478 private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) { 479 return null; 480 } 481 482 private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) { 483 Map<String, Object> calendarData = new LinkedHashMap<>(); 484 copyIfPresent(map, "firstDayOfWeek", calendarData); 485 copyIfPresent(map, "minimalDaysInFirstWeek", calendarData); 486 return calendarData; 487 } 488 489 private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) { 490 Map<String, Object> formatData = new LinkedHashMap<>(); 491 for (CalendarType calendarType : CalendarType.values()) { 492 String prefix = calendarType.keyElementName(); 493 copyIfPresent(map, prefix + "MonthNames", formatData); // default FORMAT since JDK8 494 copyIfPresent(map, prefix + "standalone.MonthNames", formatData); 495 copyIfPresent(map, prefix + "MonthAbbreviations", formatData); 496 copyIfPresent(map, prefix + "standalone.MonthAbbreviations", formatData); 497 copyIfPresent(map, prefix + "DayNames", formatData); 498 copyIfPresent(map, prefix + "DayAbbreviations", formatData); 499 copyIfPresent(map, prefix + "AmPmMarkers", formatData); 500 copyIfPresent(map, prefix + "Eras", formatData); 501 copyIfPresent(map, prefix + "short.Eras", formatData); 502 copyIfPresent(map, prefix + "TimePatterns", formatData); 503 copyIfPresent(map, prefix + "DatePatterns", formatData); 504 copyIfPresent(map, prefix + "DateTimePatterns", formatData); 505 copyIfPresent(map, prefix + "DateTimePatternChars", formatData); 506 } 507 508 copyIfPresent(map, "DefaultNumberingSystem", formatData); 509 String defaultScript = (String) map.get("DefaultNumberingSystem"); 510 @SuppressWarnings("unchecked") 511 List<String> numberingScripts = (List<String>) map.remove("numberingScripts"); 512 if (numberingScripts != null) { 513 for (String script : numberingScripts) { 514 copyIfPresent(map, script + "." + "NumberElements", formatData); 515 } 516 } else { 517 copyIfPresent(map, "NumberElements", formatData); 518 } 519 copyIfPresent(map, "NumberPatterns", formatData); 520 return formatData; 521 } 522 523 private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) { 524 Object value = src.get(key); 525 if (value != null) { 526 dest.put(key, value); 527 } 528 } 529 530 // --- code below here is adapted from java.util.Properties --- 531 private static final String specialSaveCharsJava = "\""; 532 private static final String specialSaveCharsProperties = "=: \t\r\n\f#!"; 533 534 /* 535 * Converts unicodes to encoded \uxxxx 536 * and writes out any of the characters in specialSaveChars 537 * with a preceding slash 538 */ 539 static String saveConvert(String theString, boolean useJava) { 540 if (theString == null) { 541 return ""; 542 } 543 544 String specialSaveChars; 545 if (useJava) { 546 specialSaveChars = specialSaveCharsJava; 547 } else { 548 specialSaveChars = specialSaveCharsProperties; 549 } 550 boolean escapeSpace = false; 551 552 int len = theString.length(); 553 StringBuilder outBuffer = new StringBuilder(len * 2); 554 Formatter formatter = new Formatter(outBuffer, Locale.ROOT); 555 556 for (int x = 0; x < len; x++) { 557 char aChar = theString.charAt(x); 558 switch (aChar) { 559 case ' ': 560 if (x == 0 || escapeSpace) { 561 outBuffer.append('\\'); 562 } 563 564 outBuffer.append(' '); 565 break; 566 case '\\': 567 outBuffer.append('\\'); 568 outBuffer.append('\\'); 569 break; 570 case '\t': 571 outBuffer.append('\\'); 572 outBuffer.append('t'); 573 break; 574 case '\n': 575 outBuffer.append('\\'); 576 outBuffer.append('n'); 577 break; 578 case '\r': 579 outBuffer.append('\\'); 580 outBuffer.append('r'); 581 break; 582 case '\f': 583 outBuffer.append('\\'); 584 outBuffer.append('f'); 585 break; 586 default: 587 if (!USE_UTF8 && ((aChar < 0x0020) || (aChar > 0x007e))) { 588 formatter.format("\\u%04x", (int)aChar); 589 } else { 590 if (specialSaveChars.indexOf(aChar) != -1) { 591 outBuffer.append('\\'); 592 } 593 outBuffer.append(aChar); 594 } 595 } 596 } 597 return outBuffer.toString(); 598 } 599 600 private static String toLanguageTag(String locName) { 601 if (locName.indexOf('_') == -1) { 602 return locName; 603 } 604 String tag = locName.replaceAll("_", "-"); 605 Locale loc = Locale.forLanguageTag(tag); 606 return loc.toLanguageTag(); 607 } 608 609 private static String toLocaleName(String tag) { 610 if (tag.indexOf('-') == -1) { 611 return tag; 612 } 613 return tag.replaceAll("-", "_"); 614 } 615 }