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 build.tools.cldrconverter.BundleGenerator.BundleType; 29 import java.io.File; 30 import java.nio.file.DirectoryStream; 31 import java.nio.file.FileSystems; 32 import java.nio.file.Files; 33 import java.nio.file.Path; 34 import java.util.*; 35 import javax.xml.parsers.SAXParser; 36 import javax.xml.parsers.SAXParserFactory; 37 38 39 /** 40 * Converts locale data from "Locale Data Markup Language" format to 41 * JRE resource bundle format. LDML is the format used by the Common 42 * Locale Data Repository maintained by the Unicode Consortium. 43 */ 44 public class CLDRConverter { 45 46 static final String LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldml.dtd"; 47 static final String SPPL_LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldmlSupplemental.dtd"; 48 49 private static String CLDR_BASE = "../CLDR/21.0.1/"; 50 static String LOCAL_LDML_DTD; 51 static String LOCAL_SPPL_LDML_DTD; 52 private static String SOURCE_FILE_DIR; 53 private static String SPPL_SOURCE_FILE; 54 private static String NUMBERING_SOURCE_FILE; 55 private static String METAZONES_SOURCE_FILE; 56 static String DESTINATION_DIR = "build/gensrc"; 57 58 static final String LOCALE_NAME_PREFIX = "locale.displayname."; 59 static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol."; 60 static final String CURRENCY_NAME_PREFIX = "currency.displayname."; 61 static final String TIMEZONE_ID_PREFIX = "timezone.id."; 62 static final String ZONE_NAME_PREFIX = "timezone.displayname."; 63 static final String METAZONE_ID_PREFIX = "metazone.id."; 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 Bundle b = new Bundle(id, sb.toString(), null, null); 240 // Insert the bundle for en at the top so that it will get 241 // processed first. 242 if ("en".equals(id)) { 243 retList.add(0, b); 244 } else { 245 retList.add(b); 246 } 247 } 248 } 249 } 250 return retList; 251 } 252 253 private static Map<String, Map<String, Object>> cldrBundles = new HashMap<>(); 254 255 static Map<String, Object> getCLDRBundle(String id) throws Exception { 256 Map<String, Object> bundle = cldrBundles.get(id); 257 if (bundle != null) { 258 return bundle; 259 } 260 SAXParserFactory factory = SAXParserFactory.newInstance(); 261 factory.setValidating(true); 262 SAXParser parser = factory.newSAXParser(); 263 LDMLParseHandler handler = new LDMLParseHandler(id); 264 File file = new File(SOURCE_FILE_DIR + File.separator + id + ".xml"); 265 if (!file.exists()) { 266 // Skip if the file doesn't exist. 267 return Collections.emptyMap(); 268 } 269 270 info("..... main directory ....."); 271 info("Reading file " + file); 272 parser.parse(file, handler); 273 274 bundle = handler.getData(); 275 cldrBundles.put(id, bundle); 276 String country = getCountryCode(id); 277 if (country != null) { 278 bundle = handlerSuppl.getData(country); 279 if (bundle != null) { 280 //merge two maps into one map 281 Map<String, Object> temp = cldrBundles.remove(id); 282 bundle.putAll(temp); 283 cldrBundles.put(id, bundle); 284 } 285 } 286 return bundle; 287 } 288 289 private static void convertBundles(List<Bundle> bundles) throws Exception { 290 // Parse SupplementalData file and store the information in the HashMap 291 // Calendar information such as firstDay and minDay are stored in 292 // supplementalData.xml as of CLDR1.4. Individual territory is listed 293 // with its ISO 3166 country code while default is listed using UNM49 294 // region and composition numerical code (001 for World.) 295 SAXParserFactory factorySuppl = SAXParserFactory.newInstance(); 296 factorySuppl.setValidating(true); 297 SAXParser parserSuppl = factorySuppl.newSAXParser(); 298 handlerSuppl = new SupplementDataParseHandler(); 299 File fileSupply = new File(SPPL_SOURCE_FILE); 300 parserSuppl.parse(fileSupply, handlerSuppl); 301 302 // Parse numberingSystems to get digit zero character information. 303 SAXParserFactory numberingParser = SAXParserFactory.newInstance(); 304 numberingParser.setValidating(true); 305 SAXParser parserNumbering = numberingParser.newSAXParser(); 306 handlerNumbering = new NumberingSystemsParseHandler(); 307 File fileNumbering = new File(NUMBERING_SOURCE_FILE); 308 parserNumbering.parse(fileNumbering, handlerNumbering); 309 310 // Parse metaZones to create mappings between Olson tzids and CLDR meta zone names 311 SAXParserFactory metazonesParser = SAXParserFactory.newInstance(); 312 metazonesParser.setValidating(true); 313 SAXParser parserMetaZones = metazonesParser.newSAXParser(); 314 handlerMetaZones = new MetaZonesParseHandler(); 315 File fileMetaZones = new File(METAZONES_SOURCE_FILE); 316 parserNumbering.parse(fileMetaZones, handlerMetaZones); 317 318 // For generating information on supported locales. 319 Map<String, SortedSet<String>> metaInfo = new HashMap<>(); 320 metaInfo.put("LocaleNames", new TreeSet<String>()); 321 metaInfo.put("CurrencyNames", new TreeSet<String>()); 322 metaInfo.put("TimeZoneNames", new TreeSet<String>()); 323 metaInfo.put("CalendarData", new TreeSet<String>()); 324 metaInfo.put("FormatData", new TreeSet<String>()); 325 326 for (Bundle bundle : bundles) { 327 // Get the target map, which contains all the data that should be 328 // visible for the bundle's locale 329 330 Map<String, Object> targetMap = bundle.getTargetMap(); 331 332 EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes(); 333 334 // Fill in any missing resources in the base bundle from en and en-US data. 335 // This is because CLDR root.xml is supposed to be language neutral and doesn't 336 // provide some resource data. Currently, the runtime assumes that there are all 337 // resources though the parent resource bundle chain. 338 if (bundle.isRoot()) { 339 Map<String, Object> enData = new HashMap<>(); 340 // Create a superset of en-US and en bundles data in order to 341 // fill in any missing resources in the base bundle. 342 enData.putAll(Bundle.getBundle("en").getTargetMap()); 343 enData.putAll(Bundle.getBundle("en_US").getTargetMap()); 344 for (String key : enData.keySet()) { 345 if (!targetMap.containsKey(key)) { 346 targetMap.put(key, enData.get(key)); 347 } 348 } 349 // Add DateTimePatternChars because CLDR no longer supports localized patterns. 350 targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ"); 351 } 352 353 // Now the map contains just the entries that need to be in the resources bundles. 354 // Go ahead and generate them. 355 if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) { 356 Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID()); 357 if (!localeNamesMap.isEmpty() || bundle.isRoot()) { 358 metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID())); 359 bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, BundleType.OPEN); 360 } 361 } 362 if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) { 363 Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies()); 364 if (!currencyNamesMap.isEmpty() || bundle.isRoot()) { 365 metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID())); 366 bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, BundleType.OPEN); 367 } 368 } 369 if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) { 370 Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID()); 371 if (!zoneNamesMap.isEmpty() || bundle.isRoot()) { 372 metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID())); 373 bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getID(), true, zoneNamesMap, BundleType.TIMEZONE); 374 } 375 } 376 if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) { 377 Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID()); 378 if (!calendarDataMap.isEmpty() || bundle.isRoot()) { 379 metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID())); 380 bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, BundleType.PLAIN); 381 } 382 } 383 if (bundleTypes.contains(Bundle.Type.FORMATDATA)) { 384 Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID()); 385 // LocaleData.getAvailableLocales depends on having FormatData bundles around 386 if (!formatDataMap.isEmpty() || bundle.isRoot()) { 387 metaInfo.get("FormatData").add(toLanguageTag(bundle.getID())); 388 bundleGenerator.generateBundle("text", "FormatData", bundle.getID(), true, formatDataMap, BundleType.PLAIN); 389 } 390 } 391 392 // For testing 393 SortedSet<String> allLocales = new TreeSet<>(); 394 allLocales.addAll(metaInfo.get("CurrencyNames")); 395 allLocales.addAll(metaInfo.get("LocaleNames")); 396 allLocales.addAll(metaInfo.get("CalendarData")); 397 allLocales.addAll(metaInfo.get("FormatData")); 398 metaInfo.put("All", allLocales); 399 } 400 401 bundleGenerator.generateMetaInfo(metaInfo); 402 } 403 404 /* 405 * Returns the language portion of the given id. 406 * If id is "root", "" is returned. 407 */ 408 static String getLanguageCode(String id) { 409 int index = id.indexOf('_'); 410 String lang = null; 411 if (index != -1) { 412 lang = id.substring(0, index); 413 } else { 414 lang = "root".equals(id) ? "" : id; 415 } 416 return lang; 417 } 418 419 /** 420 * Examine if the id includes the country (territory) code. If it does, it returns 421 * the country code. 422 * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG". 423 */ 424 private static String getCountryCode(String id) { 425 //Truncate a variant code with '@' if there is any 426 //(eg. de_DE@collation=phonebook,currency=DOM) 427 if (id.indexOf('@') != -1) { 428 id = id.substring(0, id.indexOf('@')); 429 } 430 String[] tokens = id.split("_"); 431 for (int index = 1; index < tokens.length; ++index) { 432 if (tokens[index].length() == 2 433 && Character.isLetter(tokens[index].charAt(0)) 434 && Character.isLetter(tokens[index].charAt(1))) { 435 return tokens[index]; 436 } 437 } 438 return null; 439 } 440 441 private static class KeyComparator implements Comparator<String> { 442 static KeyComparator INSTANCE = new KeyComparator(); 443 444 private KeyComparator() { 445 } 446 447 @Override 448 public int compare(String o1, String o2) { 449 int len1 = o1.length(); 450 int len2 = o2.length(); 451 if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) { 452 // Shorter string comes first unless either starts with a digit. 453 if (len1 < len2) { 454 return -1; 455 } 456 if (len1 > len2) { 457 return 1; 458 } 459 } 460 return o1.compareTo(o2); 461 } 462 463 private boolean isDigit(char c) { 464 return c >= '0' && c <= '9'; 465 } 466 } 467 468 private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) { 469 Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE); 470 for (String key : map.keySet()) { 471 if (key.startsWith(LOCALE_NAME_PREFIX)) { 472 localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key)); 473 } 474 } 475 return localeNames; 476 } 477 478 @SuppressWarnings("AssignmentToForLoopParameter") 479 private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names) 480 throws Exception { 481 Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE); 482 for (String key : map.keySet()) { 483 if (key.startsWith(CURRENCY_NAME_PREFIX)) { 484 currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key)); 485 } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) { 486 currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key)); 487 } 488 } 489 return currencyNames; 490 } 491 492 private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) { 493 Map<String, Object> names = new HashMap<>(); 494 for (String tzid : handlerMetaZones.keySet()) { 495 String tzKey = TIMEZONE_ID_PREFIX + tzid; 496 Object data = map.get(tzKey); 497 if (data instanceof String[]) { 498 names.put(tzid, data); 499 } else { 500 String meta = handlerMetaZones.get(tzid); 501 if (meta != null) { 502 String metaKey = METAZONE_ID_PREFIX + meta; 503 data = map.get(metaKey); 504 if (data instanceof String[]) { 505 // Keep the metazone prefix here. 506 names.put(metaKey, data); 507 names.put(tzid, meta); 508 } 509 } 510 } 511 } 512 return names; 513 } 514 515 private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) { 516 Map<String, Object> calendarData = new LinkedHashMap<>(); 517 copyIfPresent(map, "firstDayOfWeek", calendarData); 518 copyIfPresent(map, "minimalDaysInFirstWeek", calendarData); 519 return calendarData; 520 } 521 522 private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) { 523 Map<String, Object> formatData = new LinkedHashMap<>(); 524 for (CalendarType calendarType : CalendarType.values()) { 525 String prefix = calendarType.keyElementName(); 526 copyIfPresent(map, prefix + "MonthNames", formatData); // default FORMAT since JDK8 527 copyIfPresent(map, prefix + "standalone.MonthNames", formatData); 528 copyIfPresent(map, prefix + "MonthAbbreviations", formatData); 529 copyIfPresent(map, prefix + "standalone.MonthAbbreviations", formatData); 530 copyIfPresent(map, prefix + "MonthNarrow", formatData); 531 copyIfPresent(map, prefix + "standalone.MonthNarrows", formatData); 532 copyIfPresent(map, prefix + "DayNames", formatData); 533 copyIfPresent(map, prefix + "standalone.DayNames", formatData); 534 copyIfPresent(map, prefix + "DayAbbreviations", formatData); 535 copyIfPresent(map, prefix + "standalone.DayAbbreviations", formatData); 536 copyIfPresent(map, prefix + "DayNarrows", formatData); 537 copyIfPresent(map, prefix + "standalone.DayNarrows", formatData); 538 copyIfPresent(map, prefix + "AmPmMarkers", formatData); 539 copyIfPresent(map, prefix + "narrow.AmPmMarkers", formatData); 540 copyIfPresent(map, prefix + "long.Eras", formatData); 541 copyIfPresent(map, prefix + "Eras", formatData); 542 copyIfPresent(map, prefix + "narrow.Eras", formatData); 543 copyIfPresent(map, prefix + "TimePatterns", formatData); 544 copyIfPresent(map, prefix + "DatePatterns", formatData); 545 copyIfPresent(map, prefix + "DateTimePatterns", formatData); 546 copyIfPresent(map, prefix + "DateTimePatternChars", formatData); 547 } 548 549 copyIfPresent(map, "DefaultNumberingSystem", formatData); 550 String defaultScript = (String) map.get("DefaultNumberingSystem"); 551 @SuppressWarnings("unchecked") 552 List<String> numberingScripts = (List<String>) map.remove("numberingScripts"); 553 if (numberingScripts != null) { 554 for (String script : numberingScripts) { 555 copyIfPresent(map, script + "." + "NumberElements", formatData); 556 } 557 } else { 558 copyIfPresent(map, "NumberElements", formatData); 559 } 560 copyIfPresent(map, "NumberPatterns", formatData); 561 return formatData; 562 } 563 564 private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) { 565 Object value = src.get(key); 566 if (value != null) { 567 dest.put(key, value); 568 } 569 } 570 571 // --- code below here is adapted from java.util.Properties --- 572 private static final String specialSaveCharsJava = "\""; 573 private static final String specialSaveCharsProperties = "=: \t\r\n\f#!"; 574 575 /* 576 * Converts unicodes to encoded \uxxxx 577 * and writes out any of the characters in specialSaveChars 578 * with a preceding slash 579 */ 580 static String saveConvert(String theString, boolean useJava) { 581 if (theString == null) { 582 return ""; 583 } 584 585 String specialSaveChars; 586 if (useJava) { 587 specialSaveChars = specialSaveCharsJava; 588 } else { 589 specialSaveChars = specialSaveCharsProperties; 590 } 591 boolean escapeSpace = false; 592 593 int len = theString.length(); 594 StringBuilder outBuffer = new StringBuilder(len * 2); 595 Formatter formatter = new Formatter(outBuffer, Locale.ROOT); 596 597 for (int x = 0; x < len; x++) { 598 char aChar = theString.charAt(x); 599 switch (aChar) { 600 case ' ': 601 if (x == 0 || escapeSpace) { 602 outBuffer.append('\\'); 603 } 604 outBuffer.append(' '); 605 break; 606 case '\\': 607 outBuffer.append('\\'); 608 outBuffer.append('\\'); 609 break; 610 case '\t': 611 outBuffer.append('\\'); 612 outBuffer.append('t'); 613 break; 614 case '\n': 615 outBuffer.append('\\'); 616 outBuffer.append('n'); 617 break; 618 case '\r': 619 outBuffer.append('\\'); 620 outBuffer.append('r'); 621 break; 622 case '\f': 623 outBuffer.append('\\'); 624 outBuffer.append('f'); 625 break; 626 default: 627 if (aChar < 0x0020 || (!USE_UTF8 && aChar > 0x007e)) { 628 formatter.format("\\u%04x", (int)aChar); 629 } else { 630 if (specialSaveChars.indexOf(aChar) != -1) { 631 outBuffer.append('\\'); 632 } 633 outBuffer.append(aChar); 634 } 635 } 636 } 637 return outBuffer.toString(); 638 } 639 640 private static String toLanguageTag(String locName) { 641 if (locName.indexOf('_') == -1) { 642 return locName; 643 } 644 String tag = locName.replaceAll("_", "-"); 645 Locale loc = Locale.forLanguageTag(tag); 646 return loc.toLanguageTag(); 647 } 648 649 private static String toLocaleName(String tag) { 650 if (tag.indexOf('-') == -1) { 651 return tag; 652 } 653 return tag.replaceAll("-", "_"); 654 } 655 }