1 package org.unicode.cldr.test; 2 3 import java.util.Collection; 4 import java.util.Collections; 5 import java.util.EnumSet; 6 import java.util.HashSet; 7 import java.util.Iterator; 8 import java.util.LinkedHashSet; 9 import java.util.List; 10 import java.util.Map.Entry; 11 import java.util.Set; 12 import java.util.TreeSet; 13 import java.util.regex.Matcher; 14 import java.util.regex.Pattern; 15 16 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype; 17 import org.unicode.cldr.util.CLDRConfig; 18 import org.unicode.cldr.util.LocaleIDParser; 19 import org.unicode.cldr.util.Pair; 20 import org.unicode.cldr.util.PatternCache; 21 import org.unicode.cldr.util.XPathParts; 22 import org.unicode.cldr.util.personname.PersonNameFormatter; 23 import org.unicode.cldr.util.personname.PersonNameFormatter.Field; 24 import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters; 25 import org.unicode.cldr.util.personname.PersonNameFormatter.ModifiedField; 26 import org.unicode.cldr.util.personname.PersonNameFormatter.Modifier; 27 import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern; 28 import org.unicode.cldr.util.personname.PersonNameFormatter.Order; 29 import org.unicode.cldr.util.personname.PersonNameFormatter.Usage; 30 31 import com.google.common.base.Joiner; 32 import com.google.common.base.Splitter; 33 import com.google.common.collect.ImmutableSet; 34 import com.google.common.collect.Multimap; 35 import com.google.common.collect.Sets; 36 import com.ibm.icu.text.UnicodeSet; 37 import com.ibm.icu.util.Output; 38 39 public class CheckPlaceHolders extends CheckCLDR { 40 41 private static final Pattern PLACEHOLDER_PATTERN = PatternCache.get("([0-9]|[1-9][0-9]+)"); 42 private static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults(); 43 private static final Joiner JOIN_SPACE = Joiner.on(' '); 44 45 private static final Pattern SKIP_PATH_LIST = Pattern 46 .compile("//ldml/characters/(exemplarCharacters|parseLenient).*"); 47 48 // private static final LocaleMatchValue LOCALE_MATCH_VALUE = new LocaleMatchValue(ImmutableSet.of( 49 // Validity.Status.regular, 50 // Validity.Status.special, 51 // Validity.Status.unknown) 52 // ); 53 54 /** 55 * Contains all CLDR locales, plus some special cases 56 */ 57 private static final Set<String> CLDR_LOCALES_FOR_NAME_ORDER; 58 static { 59 Set<String> valid = new HashSet<>(); 60 valid.addAll(CLDRConfig.getInstance().getCldrFactory().getAvailable()); 61 valid.add("zxx"); 62 valid.add("und"); 63 CLDR_LOCALES_FOR_NAME_ORDER = ImmutableSet.copyOf(valid); 64 } 65 66 private static final ImmutableSet<Modifier> SINGLE_CORE = ImmutableSet.of(Modifier.core); 67 private static final ImmutableSet<Modifier> SINGLE_PREFIX = ImmutableSet.of(Modifier.prefix); 68 private static final ImmutableSet<Modifier> CORE_AND_PREFIX = ImmutableSet.of(Modifier.prefix, Modifier.core); 69 70 @Override handleCheck(String path, String fullPath, String value, Options options, List<CheckStatus> result)71 public CheckCLDR handleCheck(String path, String fullPath, String value, Options options, 72 List<CheckStatus> result) { 73 if (value == null 74 || path.endsWith("/alias") 75 || SKIP_PATH_LIST.matcher(path).matches()) { 76 return this; 77 } 78 79 if (path.contains("/personNames")) { 80 XPathParts parts = XPathParts.getFrozenInstance(path); 81 switch(parts.getElement(2)) { 82 default: 83 break;// skip to rest of handleCheck 84 case "initialPattern": 85 checkInitialPattern(this, path, value, result); 86 break;// skip to rest of handleCheck 87 case "foreignSpaceReplacement": 88 checkForeignSpaceReplacement(this, value, result); 89 return this; 90 case "nameOrderLocales": 91 checkNameOrder(this, path, value, result); 92 return this; 93 case "sampleName": 94 checkSampleNames(this, parts, value, result); 95 return this; 96 case "personName": 97 checkPersonNamePatterns(this, path, parts, value, result); 98 return this; 99 } 100 // done with person names 101 // note: depending on the switch value, may fall through 102 } 103 104 checkBasicPlaceholders(value, result); 105 checkListPatterns(path, value, result); 106 return this; 107 } 108 109 110 /** 111 * Verify the that nameOrder items are clean. 112 */ checkNameOrder(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)113 public static void checkNameOrder(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) { 114 //ldml/personNames/nameOrderLocales[@order="givenFirst"] 115 final String localeID = checkAccessor.getLocaleID(); 116 Set<String> items = new TreeSet<>(); 117 Set<String> orderErrors = checkForErrorsAndGetLocales(localeID, value, items); 118 if (orderErrors != null) { 119 result.add(new CheckStatus().setCause(checkAccessor) 120 .setMainType(CheckStatus.errorType) 121 .setSubtype(Subtype.invalidLocale) 122 .setMessage("Invalid locales: " + JOIN_SPACE.join(orderErrors))); 123 return; 124 } 125 // Check to see that user's language and und are explicitly mentioned. 126 // but only if the value is not inherited. 127 String unresolvedValue = checkAccessor.getUnresolvedStringValue(path); 128 if (unresolvedValue != null) { 129 // And the other value is not inherited. 130 String otherPath = path.contains("givenFirst") 131 ? path.replace("givenFirst", "surnameFirst") 132 : path.replace("surnameFirst", "givenFirst"); 133 String otherValue = checkAccessor.getStringValue(otherPath); 134 if (otherValue != null) { 135 String myLanguage = localeID; 136 if (!myLanguage.equals("root")) { // skip root 137 138 Set<String> items2 = new TreeSet<>(); 139 orderErrors = checkForErrorsAndGetLocales(localeID, otherValue, items2); // adds locales from other path. We don't check for errors there. 140 if (!Collections.disjoint(items, items2)) { 141 result.add(new CheckStatus().setCause(checkAccessor) 142 .setMainType(CheckStatus.errorType) 143 .setSubtype(Subtype.invalidLocale) 144 .setMessage("Locale codes can occur only once: " + JOIN_SPACE.join(Sets.intersection(items, items2)))); 145 } 146 147 items.addAll(items2); // get the union for checking below 148 myLanguage = new LocaleIDParser().set(myLanguage).getLanguage(); 149 150 if (!items.contains(myLanguage)) { 151 result.add(new CheckStatus().setCause(checkAccessor) 152 .setMainType(CheckStatus.errorType) 153 .setSubtype(Subtype.missingLanguage) 154 .setMessage("Your locale code (" + myLanguage 155 + ") must be explicitly listed in one of the nameOrderLocales:" 156 + " either in givenFirst or in surnameFirst.")); 157 } 158 159 if (!items.contains("und")) { 160 result.add(new CheckStatus().setCause(checkAccessor) 161 .setMainType(CheckStatus.errorType) 162 .setSubtype(Subtype.missingLanguage) 163 .setMessage("The special code ‘und’ must be explicitly listed in one of the nameOrderLocales: either givenFirst or surnameFirst.")); 164 } 165 } 166 } 167 } 168 } 169 170 /** 171 * Verify the that sampleName items are clean. 172 * @param checkAccessor 173 */ checkSampleNames(CheckAccessor checkAccessor, XPathParts pathParts, String value, List<CheckStatus> result)174 public static void checkSampleNames(CheckAccessor checkAccessor, XPathParts pathParts, String value, List<CheckStatus> result) { 175 //ldml/personNames/sampleName[@item="informal"]/nameField[@type="surname"] 176 177 // check basic consistency of modifier set 178 ModifiedField fieldType = ModifiedField.from(pathParts.getAttributeValue(-1, "type")); 179 Field field = fieldType.getField(); 180 Set<Modifier> modifiers = fieldType.getModifiers(); 181 Output<String> errorMessage = new Output<>(); 182 Modifier.getCleanSet(modifiers, errorMessage); 183 if (errorMessage.value != null) { 184 result.add(new CheckStatus().setCause(checkAccessor) 185 .setMainType(CheckStatus.warningType) 186 .setSubtype(Subtype.invalidPlaceHolder) 187 .setMessage(errorMessage.value)); 188 return; 189 } 190 191 if (value.equals("∅∅∅")) { 192 // check for required values 193 194 switch(field) { 195 case given: 196 // we must have a given 197 if (fieldType.getModifiers().isEmpty()) { 198 result.add(new CheckStatus().setCause(checkAccessor) 199 .setMainType(CheckStatus.warningType) 200 .setSubtype(Subtype.invalidPlaceHolder) 201 .setMessage("Names must have a value for the ‘given‘ field. Mononyms (like ‘Lady Gaga’) use given, not surname")); 202 } 203 break; 204 case surname: 205 // can't have surname2 unless we have surname 206 String modPath = pathParts.cloneAsThawed().setAttribute(-1, "type", Field.surname2.toString()).toString(); 207 String surname2Value = checkAccessor.getStringValue(modPath); 208 if (surname2Value != null && !surname2Value.equals("∅∅∅")) { 209 result.add(new CheckStatus().setCause(checkAccessor) 210 .setMainType(CheckStatus.errorType) 211 .setSubtype(Subtype.invalidPlaceHolder) 212 .setMessage("Names must have a value for the ‘surname’ field if they have a ‘surname2’ field.")); 213 } 214 break; 215 default: 216 break; 217 } 218 } else if (value.equals("zxx")) { // mistaken "we don't use this" 219 result.add(new CheckStatus().setCause(checkAccessor) 220 .setMainType(CheckStatus.errorType) 221 .setSubtype(Subtype.invalidPlaceHolder) 222 .setMessage("Illegal name field; zxx is only appropriate for NameOrder locales")); 223 } else { // real value 224 // special checks for prefix/core 225 final boolean hasPrefix = modifiers.contains(Modifier.prefix); 226 final boolean hasCore = modifiers.contains(Modifier.core); 227 if (hasPrefix || hasCore) { 228 // We need consistency among the 3 values if we have either prefix or core 229 230 String coreValue = hasCore ? value : modifiedFieldValue(checkAccessor, pathParts, field, modifiers, Modifier.core); 231 String prefixValue = hasPrefix ? value : modifiedFieldValue(checkAccessor, pathParts, field, modifiers, Modifier.prefix); 232 String plainValue = modifiedFieldValue(checkAccessor, pathParts, field, modifiers, null); 233 234 String errorMessage2 = Modifier.inconsistentPrefixCorePlainValues(prefixValue, coreValue, plainValue); 235 if (errorMessage2 != null) { 236 result.add(new CheckStatus().setCause(checkAccessor) 237 .setMainType(CheckStatus.errorType) 238 .setSubtype(Subtype.invalidPlaceHolder) 239 .setMessage(errorMessage2)); 240 } 241 } 242 } 243 } 244 245 static final ImmutableSet<Object> givenFirstSortingLocales = ImmutableSet.of("is", "ta", "si"); // TODO should be data-driven 246 247 /** 248 * Verify the that personName patterns are clean. 249 * @param path TODO 250 */ checkPersonNamePatterns(CheckAccessor checkAccessor, String path, XPathParts pathParts, String value, List<CheckStatus> result)251 public static void checkPersonNamePatterns(CheckAccessor checkAccessor, String path, XPathParts pathParts, String value, List<CheckStatus> result) { 252 //ldml/personNames/personName[@order="sorting"][@length="long"][@usage="addressing"][@style="formal"]/namePattern 253 254 // check that the name pattern is valid 255 256 Pair<FormatParameters, NamePattern> pair = null; 257 try { 258 pair = PersonNameFormatter.fromPathValue(pathParts, value); 259 } catch (Exception e) { 260 result.add(new CheckStatus().setCause(checkAccessor) 261 .setMainType(CheckStatus.errorType) 262 .setSubtype(Subtype.invalidPlaceHolder) 263 .setMessage("Invalid placeholder in value: \"" + value + "\"")); 264 return; // fatal error, don't bother with others 265 } 266 267 final FormatParameters parameterMatcher = pair.getFirst(); 268 final NamePattern namePattern = pair.getSecond(); 269 270 // now check that the namePattern is reasonable 271 272 Multimap<Field, Integer> fieldToPositions = namePattern.getFieldPositions(); 273 274 // Check for special cases: https://unicode-org.atlassian.net/browse/CLDR-15782 275 276 boolean usageIsMonogram = parameterMatcher.matches(new FormatParameters(null, null, Usage.monogram, null)); 277 278 ModifiedField lastModifiedField = null; 279 for (int i = 0; i < namePattern.getElementCount(); ++i) { 280 ModifiedField modifiedField = namePattern.getModifiedField(i); 281 if (modifiedField == null) { // literal 282 String literal = namePattern.getLiteral(i); 283 if (literal.contains(".")) { 284 if (lastModifiedField != null) { 285 Set<Modifier> lastModifiers = lastModifiedField.getModifiers(); 286 if (lastModifiers.contains(Modifier.initial) && lastModifiers.contains(Modifier.initialCap)) { 287 result.add(new CheckStatus().setCause(checkAccessor) 288 .setMainType(CheckStatus.warningType) 289 .setSubtype(Subtype.namePlaceholderProblem) 290 .setMessage("“.” is strongly discouraged after an -initial or -initialCap placeholder in {" + lastModifiedField + "}")); 291 continue; 292 } 293 } 294 if (usageIsMonogram) { 295 result.add(new CheckStatus().setCause(checkAccessor) 296 .setMainType(CheckStatus.warningType) 297 .setSubtype(Subtype.namePlaceholderProblem) 298 .setMessage("“.” is discouraged when usage=monogram, in " + namePattern)); 299 } 300 } 301 } else { 302 lastModifiedField = modifiedField; 303 Set<Modifier> modifiers = modifiedField.getModifiers(); 304 Field field = modifiedField.getField(); 305 switch (field) { 306 case prefix: case suffix: 307 if (usageIsMonogram) { 308 result.add(new CheckStatus().setCause(checkAccessor) 309 .setMainType(CheckStatus.errorType) 310 .setSubtype(Subtype.invalidPlaceHolder) 311 .setMessage("Disallowed when usage=monogram: {" + field + "…}")); 312 } 313 break; 314 default: 315 final boolean monogramModifier = modifiers.contains(Modifier.monogram); 316 final boolean allCapsModifier = modifiers.contains(Modifier.allCaps); 317 if (!usageIsMonogram) { 318 if (monogramModifier) { 319 result.add(new CheckStatus().setCause(checkAccessor) 320 .setMainType(CheckStatus.warningType) 321 .setSubtype(Subtype.invalidPlaceHolder) 322 .setMessage("-monogram is strongly discouraged when usage≠monogram, in {" + modifiedField + "}")); 323 } 324 } else if (usageIsMonogram) { 325 if (!monogramModifier) { 326 result.add(new CheckStatus().setCause(checkAccessor) 327 .setMainType(CheckStatus.errorType) 328 .setSubtype(Subtype.invalidPlaceHolder) 329 .setMessage("-monogram is required when usage=monogram, in {" + modifiedField + "}")); 330 } else if (!allCapsModifier) { 331 result.add(new CheckStatus().setCause(checkAccessor) 332 .setMainType(CheckStatus.warningType) 333 .setSubtype(Subtype.invalidPlaceHolder) 334 .setMessage("-allCaps is strongly encouraged with -monogram, in {" + modifiedField + "}")); 335 } 336 } 337 } 338 } 339 lastModifiedField = modifiedField; 340 } 341 342 // gather information about the fields 343 int firstSurname = Integer.MAX_VALUE; 344 int firstGiven = Integer.MAX_VALUE; 345 346 // TODO ALL check for combinations we should enforce; eg, only have given2 if there is a given; only have surname2 if there is a surname; others? 347 348 for (Entry<Field, Collection<Integer>> entry : fieldToPositions.asMap().entrySet()) { 349 350 // If a field occurs twice, probably an error. Could relax this upon feedback 351 352 Collection<Integer> positions = entry.getValue(); 353 if (positions.size() > 1) { 354 355 // However, do allow prefix&core together 356 357 boolean skip = false; 358 if (entry.getKey() == Field.surname) { 359 Iterator<Integer> it = positions.iterator(); 360 Set<Modifier> m1 = namePattern.getModifiedField(it.next()).getModifiers(); 361 Set<Modifier> m2 = namePattern.getModifiedField(it.next()).getModifiers(); 362 skip = m1.contains(Modifier.core) && m2.contains(Modifier.prefix) 363 || m1.contains(Modifier.prefix) && m2.contains(Modifier.core); 364 } 365 366 if (!skip) { 367 result.add(new CheckStatus().setCause(checkAccessor) 368 .setMainType(CheckStatus.errorType) 369 .setSubtype(Subtype.invalidPlaceHolder) 370 .setMessage("Duplicate fields: " + entry)); 371 } 372 } 373 374 // gather some info for later 375 376 Integer leastPosition = positions.iterator().next(); 377 switch (entry.getKey()) { 378 case given: case given2: 379 firstGiven = Math.min(leastPosition, firstGiven); 380 break; 381 case surname: case surname2: 382 firstSurname = Math.min(leastPosition, firstSurname); 383 break; 384 default: // ignore 385 } 386 } 387 388 // the rest of the tests are of the pattern, and only apply when we have both given and surname 389 // and not inheriting 390 391 if (firstGiven < Integer.MAX_VALUE && firstSurname < Integer.MAX_VALUE 392 && checkAccessor.getUnresolvedStringValue(path) != null) { 393 394 Order orderRaw = parameterMatcher.getOrder(); 395 Set<Order> order = orderRaw == null ? Order.ALL : ImmutableSet.of(orderRaw); 396 // TODO, fix to avoid set (a holdover from using PatternMatcher) 397 398 // Handle 'sorting' value. Will usually be compatible with surnameFirst in foundOrder, except for known exceptions 399 400 if (order.contains(Order.sorting)) { 401 EnumSet<Order> temp = EnumSet.noneOf(Order.class); 402 temp.addAll(order); 403 temp.remove(Order.sorting); 404 if (givenFirstSortingLocales.contains(checkAccessor.getLocaleID())) { // TODO Mark cover contains-by-inheritance also 405 temp.add(Order.givenFirst); 406 } else { 407 temp.add(Order.surnameFirst); 408 } 409 order = temp; 410 } 411 412 if (order.isEmpty()) { 413 order = Order.ALL; 414 } 415 416 // check that we don't have a difference in the order AND there is a surname or surname2 417 // that is, it is ok to coalesce patterns of different orders where the order doesn't make a difference 418 419 { // TODO: clean up to avoid block 420 421 if(order.contains(Order.givenFirst) 422 && order.contains(Order.surnameFirst) 423 ) { 424 result.add(new CheckStatus().setCause(checkAccessor) 425 .setMainType(CheckStatus.errorType) 426 .setSubtype(Subtype.invalidPlaceHolder) 427 .setMessage("Conflicting Order values: " + order)); 428 } 429 430 // now check order in pattern is consistent with Order 431 432 Order foundOrder = firstGiven < firstSurname ? Order.givenFirst : Order.surnameFirst; 433 final Order first = order.iterator().next(); 434 435 if (first != foundOrder) { 436 437 // if (first == Order.givenFirst && !"en".equals(checkAccessor.getLocaleID())) { // TODO Mark Drop HACK once root is ok 438 // return; 439 // } 440 441 result.add(new CheckStatus().setCause(checkAccessor) 442 .setMainType(CheckStatus.errorType) 443 .setSubtype(Subtype.invalidPlaceHolder) 444 .setMessage("Pattern order {0} is inconsistent with code order {1}", foundOrder, first)); 445 } 446 } 447 } 448 } 449 450 /** 451 * Check that {\d+} placeholders are ok; no unterminated, only digits 452 */ 453 private void checkBasicPlaceholders(String value, List<CheckStatus> result) { 454 int startPlaceHolder = 0; 455 int endPlaceHolder; 456 while (startPlaceHolder != -1 && startPlaceHolder < value.length()) { 457 startPlaceHolder = value.indexOf('{', startPlaceHolder + 1); 458 if (startPlaceHolder != -1) { 459 endPlaceHolder = value.indexOf('}', startPlaceHolder + 1); 460 if (endPlaceHolder == -1) { 461 result.add(new CheckStatus().setCause(this) 462 .setMainType(CheckStatus.errorType) 463 .setSubtype(Subtype.invalidPlaceHolder) 464 .setMessage("Invalid placeholder (missing terminator) in value \"" + value + "\"")); 465 } else { 466 String placeHolderString = value.substring(startPlaceHolder + 1, endPlaceHolder); 467 Matcher matcher = PLACEHOLDER_PATTERN.matcher(placeHolderString); 468 if (!matcher.matches()) { 469 result.add(new CheckStatus().setCause(this) 470 .setMainType(CheckStatus.errorType) 471 .setSubtype(Subtype.invalidPlaceHolder) 472 .setMessage("Invalid placeholder (contents \"" + placeHolderString + "\") in value \"" + value + "\"")); 473 } 474 startPlaceHolder = endPlaceHolder; 475 } 476 } 477 } 478 } 479 480 /** 481 * Check that list patterns are "ordered" so that they only compose from the right. 482 */ 483 484 private void checkListPatterns(String path, String value, List<CheckStatus> result) { 485 // eg 486 //ldml/listPatterns/listPattern/listPatternPart[@type="start"] 487 //ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"] 488 if (path.startsWith("//ldml/listPatterns/listPattern")) { 489 XPathParts parts = XPathParts.getFrozenInstance(path); 490 // check order, {0} must be before {1} 491 492 switch(parts.getAttributeValue(-1, "type")) { 493 case "start": 494 checkNothingAfter1(value, result); 495 break; 496 case "middle": 497 checkNothingBefore0(value, result); 498 checkNothingAfter1(value, result); 499 break; 500 case "end": 501 checkNothingBefore0(value, result); 502 break; 503 case "2": { 504 int pos1 = value.indexOf("{0}"); 505 int pos2 = value.indexOf("{1}"); 506 if (pos1 > pos2) { 507 result.add(new CheckStatus().setCause(this) 508 .setMainType(CheckStatus.errorType) 509 .setSubtype(Subtype.invalidPlaceHolder) 510 .setMessage("Invalid list pattern «" + value + "»: the placeholder {0} must be before {1}.")); 511 }} 512 break; 513 case "3": { 514 int pos1 = value.indexOf("{0}"); 515 int pos2 = value.indexOf("{1}"); 516 int pos3 = value.indexOf("{2}"); 517 if (pos1 > pos2 || pos2 > pos3) { 518 result.add(new CheckStatus().setCause(this) 519 .setMainType(CheckStatus.errorType) 520 .setSubtype(Subtype.invalidPlaceHolder) 521 .setMessage("Invalid list pattern «" + value + "»: the placeholders {0}, {1}, {2} must appear in that order.")); 522 }} 523 break; 524 } 525 } 526 } 527 528 /** 529 * Check that both patterns don't have the same literals. 530 */ checkInitialPattern(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)531 public static void checkInitialPattern(CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) { 532 if (path.contains("initialSequence")) { 533 String valueLiterals = value.replace("{0}", "").replace("{1}", ""); 534 if (!valueLiterals.isBlank()) { 535 String otherPath = path.replace("initialSequence", "initial"); 536 String otherValue = checkAccessor.getStringValue(otherPath); 537 if (otherValue != null) { 538 String literals = otherValue.replace("{0}", ""); 539 if (!literals.isBlank() && value.contains(literals)) { 540 result.add(new CheckStatus().setCause(checkAccessor) 541 .setMainType(CheckStatus.errorType) 542 .setSubtype(Subtype.namePlaceholderProblem) 543 .setMessage("The initialSequence pattern must not contain initial pattern literals: «" + literals + "»")); 544 return; 545 } 546 } 547 result.add(new CheckStatus().setCause(checkAccessor) 548 .setMainType(CheckStatus.warningType) 549 .setSubtype(Subtype.namePlaceholderProblem) 550 .setMessage("Non-space characters are discouraged in the initialSequence pattern: «" + valueLiterals.replace(" ", "") + "»")); 551 } 552 } 553 // no current check for the type="initial" 554 } 555 556 static final UnicodeSet allowedForeignSpaceReplacements = new UnicodeSet("[[:whitespace:][:punctuation:]]"); 557 558 /** 559 * Check that the value is limited to punctuation or space, or inherits 560 */ checkForeignSpaceReplacement(CheckAccessor checkAccessor, String value, List<CheckStatus> result)561 public static void checkForeignSpaceReplacement(CheckAccessor checkAccessor, String value, List<CheckStatus> result) { 562 if (!allowedForeignSpaceReplacements.containsAll(value) && !value.equals("↑↑↑")) { 563 result.add(new CheckStatus().setCause(checkAccessor) 564 .setMainType(CheckStatus.errorType) 565 .setSubtype(Subtype.invalidLocale) 566 .setMessage("Invalid choice, must be punctuation or a space: «" + value + "»")); 567 } 568 } 569 570 571 /** 572 * Gets a string value for a modified path 573 */ modifiedFieldValue(CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd)574 private static String modifiedFieldValue(CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd) { 575 Set<Modifier> adjustedModifiers = Sets.difference(modifiers, CORE_AND_PREFIX); 576 if (toAdd != null) { 577 switch (toAdd) { 578 case core: 579 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_CORE); 580 break; 581 case prefix: 582 adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_PREFIX); 583 break; 584 default: 585 break; 586 } 587 } 588 String modPath = parts.cloneAsThawed().setAttribute(-1, "type", new ModifiedField(field, adjustedModifiers).toString()).toString(); 589 String value = checkAccessor.getStringValue(modPath); 590 return "∅∅∅".equals(value) ? null : value; 591 } 592 checkForErrorsAndGetLocales(String locale, String value, Set<String> items)593 public static Set<String> checkForErrorsAndGetLocales(String locale, String value, Set<String> items) { 594 if (value.isEmpty()) { 595 return null; 596 } 597 Set<String> orderErrors = null; 598 for (String item : SPLIT_SPACE.split(value)) { 599 boolean mv = (item.equals(locale)) 600 || CLDR_LOCALES_FOR_NAME_ORDER.contains(item); 601 if (!mv) { 602 if (orderErrors == null) { 603 orderErrors = new LinkedHashSet<>(); 604 } 605 orderErrors.add(item); 606 } else { 607 items.add(item); 608 } 609 } 610 return orderErrors; 611 } 612 checkNothingAfter1(String value, List<CheckStatus> result)613 private void checkNothingAfter1(String value, List<CheckStatus> result) { 614 if (!value.endsWith("{1}")) { 615 result.add(new CheckStatus().setCause(this) 616 .setMainType(CheckStatus.errorType) 617 .setSubtype(Subtype.invalidPlaceHolder) 618 .setMessage("Invalid list pattern «" + value + "», no text can come after {1}.")); 619 } 620 621 } 622 checkNothingBefore0(String value, List<CheckStatus> result)623 private void checkNothingBefore0(String value, List<CheckStatus> result) { 624 if (!value.startsWith("{0}")) { 625 result.add(new CheckStatus().setCause(this) 626 .setMainType(CheckStatus.errorType) 627 .setSubtype(Subtype.invalidPlaceHolder) 628 .setMessage("Invalid list pattern «" + value + "», no text can come before {0}.")); 629 } 630 } 631 } 632