blob: ee20a6fcbdb995b81bfdb5f4cd30147603098662 [file] [log] [blame]
package org.unicode.cldr.test;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Output;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
import org.unicode.cldr.test.CheckCLDR.CheckStatus.Type;
import org.unicode.cldr.util.CLDRConfig;
import org.unicode.cldr.util.CLDRFile;
import org.unicode.cldr.util.LocaleIDParser;
import org.unicode.cldr.util.LocaleNames;
import org.unicode.cldr.util.Pair;
import org.unicode.cldr.util.PatternCache;
import org.unicode.cldr.util.XPathParts;
import org.unicode.cldr.util.personname.PersonNameFormatter;
import org.unicode.cldr.util.personname.PersonNameFormatter.Field;
import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters;
import org.unicode.cldr.util.personname.PersonNameFormatter.ModifiedField;
import org.unicode.cldr.util.personname.PersonNameFormatter.Modifier;
import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern;
import org.unicode.cldr.util.personname.PersonNameFormatter.Order;
import org.unicode.cldr.util.personname.PersonNameFormatter.Usage;
public class CheckPlaceHolders extends CheckCLDR {
private static final Pattern PLACEHOLDER_PATTERN = PatternCache.get("([0-9]|[1-9][0-9]+)");
private static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults();
private static final Joiner JOIN_SPACE = Joiner.on(' ');
private static final Pattern SKIP_PATH_LIST =
Pattern.compile("//ldml/characters/(exemplarCharacters|parseLenient).*");
// private static final LocaleMatchValue LOCALE_MATCH_VALUE = new
// LocaleMatchValue(ImmutableSet.of(
// Validity.Status.regular,
// Validity.Status.special,
// Validity.Status.unknown)
// );
/** Contains all CLDR locales, plus some special cases */
private static final Set<String> CLDR_LOCALES_FOR_NAME_ORDER;
static {
Set<String> valid = new HashSet<>();
valid.addAll(CLDRConfig.getInstance().getCldrFactory().getAvailable());
valid.add(LocaleNames.ZXX);
valid.add(LocaleNames.UND);
CLDR_LOCALES_FOR_NAME_ORDER = ImmutableSet.copyOf(valid);
}
private static final ImmutableSet<Modifier> SINGLE_CORE = ImmutableSet.of(Modifier.core);
private static final ImmutableSet<Modifier> SINGLE_PREFIX = ImmutableSet.of(Modifier.prefix);
private static final ImmutableSet<Modifier> CORE_AND_PREFIX =
ImmutableSet.of(Modifier.prefix, Modifier.core);
private Set<Modifier> allowedModifiers = null;
@Override
public CheckCLDR handleSetCldrFileToCheck(
CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
super.handleSetCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
allowedModifiers = Modifier.getAllowedModifiers(cldrFileToCheck.getLocaleID());
return this;
}
@Override
public CheckCLDR handleCheck(
String path, String fullPath, String value, Options options, List<CheckStatus> result) {
if (value == null || path.endsWith("/alias") || SKIP_PATH_LIST.matcher(path).matches()) {
return this;
}
// TODO: more skips here
if (!accept(result)) return this;
if (path.contains("/personNames")) {
XPathParts parts = XPathParts.getFrozenInstance(path);
switch (parts.getElement(2)) {
default:
break; // skip to rest of handleCheck
case "initialPattern":
checkInitialPattern(this, path, value, result);
break; // skip to rest of handleCheck
case "nativeSpaceReplacement":
case "foreignSpaceReplacement":
checkForeignSpaceReplacement(this, value, result);
return this;
case "nameOrderLocales":
checkNameOrder(this, path, value, result);
return this;
case "sampleName":
checkSampleNames(this, parts, value, result);
return this;
case "personName":
checkPersonNamePatterns(
this, allowedModifiers, getLocaleID(), path, parts, value, result);
return this;
}
// done with person names
// note: depending on the switch value, may fall through
}
checkBasicPlaceholders(value, result);
checkListPatterns(path, value, result);
return this;
}
/** Verify the that nameOrder items are clean. */
public static void checkNameOrder(
CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
// ldml/personNames/nameOrderLocales[@order="givenFirst"]
final String localeID = checkAccessor.getLocaleID();
Set<String> items = new TreeSet<>();
Set<String> orderErrors = checkForErrorsAndGetLocales(localeID, value, items);
if (orderErrors != null) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidLocale)
.setMessage("Invalid locales: " + JOIN_SPACE.join(orderErrors)));
return;
}
// Check to see that user's language and und are explicitly mentioned.
// but only if the value is not inherited.
String unresolvedValue = checkAccessor.getUnresolvedStringValue(path);
if (unresolvedValue != null) {
// And the other value is not inherited.
String otherPath =
path.contains("givenFirst")
? path.replace("givenFirst", "surnameFirst")
: path.replace("surnameFirst", "givenFirst");
String otherValue = checkAccessor.getStringValue(otherPath);
if (otherValue != null) {
String myLanguage = localeID;
if (!myLanguage.equals("root")) { // skip root
Set<String> items2 = new TreeSet<>();
orderErrors =
checkForErrorsAndGetLocales(
localeID,
otherValue,
items2); // adds locales from other path. We don't check for
// errors there.
if (!Collections.disjoint(items, items2)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidLocale)
.setMessage(
"Locale codes can occur only once: "
+ JOIN_SPACE.join(
Sets.intersection(items, items2))));
}
items.addAll(items2); // get the union for checking below
myLanguage = new LocaleIDParser().set(myLanguage).getLanguage();
if (!items.contains(myLanguage)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.missingLanguage)
.setMessage(
"Your locale code ("
+ myLanguage
+ ") must be explicitly listed in one of the nameOrderLocales:"
+ " either in givenFirst or in surnameFirst."));
}
if (!items.contains(LocaleNames.UND)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.missingLanguage)
.setMessage(
"The special code ‘und’ must be explicitly listed in one of the nameOrderLocales: either givenFirst or surnameFirst."));
}
}
}
}
}
/**
* Verify the that sampleName items are clean.
*
* @param checkAccessor
*/
public static void checkSampleNames(
CheckAccessor checkAccessor,
XPathParts pathParts,
String value,
List<CheckStatus> result) {
// ldml/personNames/sampleName[@item="informal"]/nameField[@type="surname"]
// check basic consistency of modifier set
ModifiedField fieldType = ModifiedField.from(pathParts.getAttributeValue(-1, "type"));
Field field = fieldType.getField();
Set<Modifier> modifiers = fieldType.getModifiers();
Output<String> errorMessage = new Output<>();
Modifier.getCleanSet(modifiers, errorMessage);
final Type mainType =
checkAccessor.getPhase() != Phase.BUILD
? CheckStatus.errorType
: CheckStatus.warningType;
if (errorMessage.value != null) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(mainType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(errorMessage.value));
return;
}
if (value.equals("∅∅∅")) {
// check for required values
switch (field) {
case given:
// we must have a given
if (fieldType.getModifiers().isEmpty()) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(mainType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Names must have a value for the ‘given‘ field. Mononyms (like ‘Zendaya’) use given, not surname"));
}
break;
case surname:
// can't have surname2 unless we have surname
final XPathParts thawedPathParts = pathParts.cloneAsThawed();
String modPath =
thawedPathParts
.setAttribute(-1, "type", Field.surname2.toString())
.toString();
String surname2Value = checkAccessor.getStringValue(modPath);
String modPathcore =
thawedPathParts.setAttribute(-1, "type", "surname-core").toString();
String surnameCoreValue = checkAccessor.getStringValue(modPathcore);
if (surname2Value != null
&& !surname2Value.equals("∅∅∅")
&& (surnameCoreValue == null || surnameCoreValue.equals("∅∅∅"))) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Names must have a value for the ‘surname’ field if they have a ‘surname2’ field."));
}
break;
default:
break;
}
} else if (value.equals(LocaleNames.ZXX)) { // mistaken "we don't use this"
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Illegal name field; zxx is only appropriate for NameOrder locales"));
} else { // real value
// special checks for prefix/core
final boolean hasPrefix = modifiers.contains(Modifier.prefix);
final boolean hasCore = modifiers.contains(Modifier.core);
if (hasPrefix || hasCore) {
// We need consistency among the 3 values if we have either prefix or core
String coreValue =
hasCore
? value
: modifiedFieldValue(
checkAccessor, pathParts, field, modifiers, Modifier.core);
String prefixValue =
hasPrefix
? value
: modifiedFieldValue(
checkAccessor,
pathParts,
field,
modifiers,
Modifier.prefix);
String plainValue =
modifiedFieldValue(checkAccessor, pathParts, field, modifiers, null);
String errorMessage2 =
Modifier.inconsistentPrefixCorePlainValues(
prefixValue, coreValue, plainValue);
if (errorMessage2 != null) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(errorMessage2));
}
}
}
}
static final ImmutableSet<Object> givenFirstSortingLocales =
ImmutableSet.of("is", "ta", "si"); // TODO should be data-driven
/**
* Verify the that personName patterns are clean.
*
* @param path TODO
* @deprecated Use {@link
* #checkPersonNamePatterns(CheckAccessor,Set<Modifier>,String,String,XPathParts,String,List<CheckStatus>)}
* instead
*/
@Deprecated
public static void checkPersonNamePatterns(
CheckAccessor checkAccessor,
Set<Modifier> allowedModifiers,
String path,
XPathParts pathParts,
String value,
List<CheckStatus> result) {
checkPersonNamePatterns(
checkAccessor, allowedModifiers, "fr", path, pathParts, value, result);
}
/**
* Verify the that personName patterns are clean.
*
* @param locale TODO
* @param path TODO
*/
public static void checkPersonNamePatterns(
CheckAccessor checkAccessor,
Set<Modifier> allowedModifiers,
String locale,
String path,
XPathParts pathParts,
String value,
List<CheckStatus> result) {
// ldml/personNames/personName[@order="sorting"][@length="long"][@usage="addressing"][@style="formal"]/namePattern
// check that the name pattern is valid
Pair<FormatParameters, NamePattern> pair = null;
try {
pair = PersonNameFormatter.fromPathValue(pathParts, value);
} catch (Exception e) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage("Invalid placeholder in value: «" + value + "»"));
return; // fatal error, don't bother with others
}
final FormatParameters parameterMatcher = pair.getFirst();
final NamePattern namePattern = pair.getSecond();
// now check that the namePattern is reasonable
Multimap<Field, Integer> fieldToPositions = namePattern.getFieldPositions();
// Check for special cases: https://unicode-org.atlassian.net/browse/CLDR-15782
boolean usageIsMonogram =
parameterMatcher.matches(new FormatParameters(null, null, Usage.monogram, null));
ModifiedField lastModifiedField = null;
for (int i = 0; i < namePattern.getElementCount(); ++i) {
ModifiedField modifiedField = namePattern.getModifiedField(i);
if (modifiedField == null) { // literal
String literal = namePattern.getLiteral(i);
if (literal.contains(".")) {
if (lastModifiedField != null) {
Set<Modifier> lastModifiers = lastModifiedField.getModifiers();
if (lastModifiers.contains(Modifier.initial)
&& lastModifiers.contains(Modifier.initialCap)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.warningType)
.setSubtype(Subtype.namePlaceholderProblem)
.setMessage(
"“.” is strongly discouraged after an -initial or -initialCap placeholder in {"
+ lastModifiedField
+ "}"));
continue;
}
}
if (usageIsMonogram) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.warningType)
.setSubtype(Subtype.namePlaceholderProblem)
.setMessage(
"“.” is discouraged when usage=monogram, in "
+ namePattern));
}
}
} else {
lastModifiedField = modifiedField;
Set<Modifier> modifiers = modifiedField.getModifiers();
Field field = modifiedField.getField();
if (!allowedModifiers.containsAll(modifiers)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Illegal grammatical case modifiers for {0}: {1}",
locale, Sets.difference(modifiers, allowedModifiers)));
}
switch (field) {
case title:
case credentials:
case generation:
if (usageIsMonogram) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Disallowed when usage=monogram: {"
+ field
+ "…}"));
}
break;
default:
final boolean monogramModifier = modifiers.contains(Modifier.monogram);
final boolean allCapsModifier = modifiers.contains(Modifier.allCaps);
if (!usageIsMonogram) {
if (monogramModifier) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.warningType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"-monogram is strongly discouraged when usage≠monogram, in {"
+ modifiedField
+ "}"));
}
} else if (usageIsMonogram) {
if (!monogramModifier) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"-monogram is required when usage=monogram, in {"
+ modifiedField
+ "}"));
} else if (!allCapsModifier) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.warningType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"-allCaps is strongly encouraged with -monogram, in {"
+ modifiedField
+ "}"));
}
}
}
}
lastModifiedField = modifiedField;
}
// gather information about the fields
int firstSurname = Integer.MAX_VALUE;
int firstGiven = Integer.MAX_VALUE;
// 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?
for (Entry<Field, Collection<Integer>> entry : fieldToPositions.asMap().entrySet()) {
// If a field occurs twice, probably an error. Could relax this upon feedback
Collection<Integer> positions = entry.getValue();
if (positions.size() > 1) {
// However, do allow prefix&core together
boolean skip = false;
if (entry.getKey() == Field.surname) {
Iterator<Integer> it = positions.iterator();
Set<Modifier> m1 = namePattern.getModifiedField(it.next()).getModifiers();
Set<Modifier> m2 = namePattern.getModifiedField(it.next()).getModifiers();
skip =
m1.contains(Modifier.core) && m2.contains(Modifier.prefix)
|| m1.contains(Modifier.prefix) && m2.contains(Modifier.core);
}
if (!skip) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage("Duplicate fields: " + entry));
}
}
// gather some info for later
Integer leastPosition = positions.iterator().next();
switch (entry.getKey()) {
case given:
case given2:
firstGiven = Math.min(leastPosition, firstGiven);
break;
case surname:
case surname2:
firstSurname = Math.min(leastPosition, firstSurname);
break;
default: // ignore
}
}
// the rest of the tests are of the pattern, and only apply when we have both given and
// surname
// and not inheriting
if (firstGiven < Integer.MAX_VALUE
&& firstSurname < Integer.MAX_VALUE
&& checkAccessor.getUnresolvedStringValue(path) != null) {
Order orderRaw = parameterMatcher.getOrder();
Set<Order> order = orderRaw == null ? Order.ALL : ImmutableSet.of(orderRaw);
// TODO, fix to avoid set (a holdover from using PatternMatcher)
// Handle 'sorting' value. Will usually be compatible with surnameFirst in foundOrder,
// except for known exceptions
if (order.contains(Order.sorting)) {
EnumSet<Order> temp = EnumSet.noneOf(Order.class);
temp.addAll(order);
temp.remove(Order.sorting);
if (givenFirstSortingLocales.contains(
checkAccessor
.getLocaleID())) { // TODO Mark cover contains-by-inheritance also
temp.add(Order.givenFirst);
} else {
temp.add(Order.surnameFirst);
}
order = temp;
}
if (order.isEmpty()) {
order = Order.ALL;
}
// check that we don't have a difference in the order AND there is a surname or surname2
// that is, it is ok to coalesce patterns of different orders where the order doesn't
// make a difference
{ // TODO: clean up to avoid block
if (order.contains(Order.givenFirst) && order.contains(Order.surnameFirst)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage("Conflicting Order values: " + order));
}
// now check order in pattern is consistent with Order
Order foundOrder =
firstGiven < firstSurname ? Order.givenFirst : Order.surnameFirst;
final Order first = order.iterator().next();
if (first != foundOrder) {
// if (first == Order.givenFirst &&
// !"en".equals(checkAccessor.getLocaleID())) { // TODO Mark Drop HACK once root
// is ok
// return;
// }
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Pattern order {0} is inconsistent with code order {1}",
foundOrder, first));
}
}
}
}
/** Check that {\d+} placeholders are ok; no unterminated, only digits */
private void checkBasicPlaceholders(String value, List<CheckStatus> result) {
int startPlaceHolder = 0;
int endPlaceHolder;
while (startPlaceHolder != -1 && startPlaceHolder < value.length()) {
startPlaceHolder = value.indexOf('{', startPlaceHolder + 1);
if (startPlaceHolder != -1) {
endPlaceHolder = value.indexOf('}', startPlaceHolder + 1);
if (endPlaceHolder == -1) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid placeholder (missing terminator) in value «"
+ value
+ "»"));
} else {
String placeHolderString =
value.substring(startPlaceHolder + 1, endPlaceHolder);
Matcher matcher = PLACEHOLDER_PATTERN.matcher(placeHolderString);
if (!matcher.matches()) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid placeholder (contents \""
+ placeHolderString
+ "\") in value \""
+ value
+ "\""));
}
startPlaceHolder = endPlaceHolder;
}
}
}
}
/** Check that list patterns are "ordered" so that they only compose from the right. */
private void checkListPatterns(String path, String value, List<CheckStatus> result) {
// eg
// ldml/listPatterns/listPattern/listPatternPart[@type="start"]
// ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"]
if (path.startsWith("//ldml/listPatterns/listPattern")) {
XPathParts parts = XPathParts.getFrozenInstance(path);
// check order, {0} must be before {1}
switch (parts.getAttributeValue(-1, "type")) {
case "start":
checkNothingAfter1(value, result);
break;
case "middle":
checkNothingBefore0(value, result);
checkNothingAfter1(value, result);
break;
case "end":
checkNothingBefore0(value, result);
break;
case "2":
{
int pos1 = value.indexOf("{0}");
int pos2 = value.indexOf("{1}");
if (pos1 > pos2) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid list pattern «"
+ value
+ "»: the placeholder {0} must be before {1}."));
}
}
break;
case "3":
{
int pos1 = value.indexOf("{0}");
int pos2 = value.indexOf("{1}");
int pos3 = value.indexOf("{2}");
if (pos1 > pos2 || pos2 > pos3) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid list pattern «"
+ value
+ "»: the placeholders {0}, {1}, {2} must appear in that order."));
}
}
break;
}
}
}
/** Check that both patterns don't have the same literals. */
public static void checkInitialPattern(
CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
if (path.contains("initialSequence")) {
String valueLiterals = value.replace("{0}", "").replace("{1}", "");
if (!valueLiterals.isBlank()) {
String otherPath = path.replace("initialSequence", "initial");
String otherValue = checkAccessor.getStringValue(otherPath);
if (otherValue != null) {
String literals = otherValue.replace("{0}", "");
if (!literals.isBlank() && value.contains(literals)) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.namePlaceholderProblem)
.setMessage(
"The initialSequence pattern must not contain initial pattern literals: «"
+ literals
+ "»"));
return;
}
}
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.warningType)
.setSubtype(Subtype.namePlaceholderProblem)
.setMessage(
"Non-space characters are discouraged in the initialSequence pattern: «"
+ valueLiterals.replace(" ", "")
+ "»"));
}
}
// no current check for the type="initial"
}
static final UnicodeSet allowedForeignSpaceReplacements =
new UnicodeSet("[[:whitespace:][:punctuation:]]");
/** Check that the value is limited to punctuation or space, or inherits */
public static void checkForeignSpaceReplacement(
CheckAccessor checkAccessor, String value, List<CheckStatus> result) {
if (!allowedForeignSpaceReplacements.containsAll(value) && !value.equals("↑↑↑")) {
result.add(
new CheckStatus()
.setCause(checkAccessor)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidLocale)
.setMessage(
"Invalid choice, must be punctuation or a space: «"
+ value
+ "»"));
}
}
/** Gets a string value for a modified path */
private static String modifiedFieldValue(
CheckAccessor checkAccessor,
XPathParts parts,
Field field,
Set<Modifier> modifiers,
Modifier toAdd) {
Set<Modifier> adjustedModifiers = Sets.difference(modifiers, CORE_AND_PREFIX);
if (toAdd != null) {
switch (toAdd) {
case core:
adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_CORE);
break;
case prefix:
adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_PREFIX);
break;
default:
break;
}
}
String modPath =
parts.cloneAsThawed()
.setAttribute(
-1, "type", new ModifiedField(field, adjustedModifiers).toString())
.toString();
String value = checkAccessor.getStringValue(modPath);
return "∅∅∅".equals(value) ? null : value;
}
public static Set<String> checkForErrorsAndGetLocales(
String locale, String value, Set<String> items) {
if (value.isEmpty()) {
return null;
}
Set<String> orderErrors = null;
for (String item : SPLIT_SPACE.split(value)) {
boolean mv = (item.equals(locale)) || CLDR_LOCALES_FOR_NAME_ORDER.contains(item);
if (!mv) {
if (orderErrors == null) {
orderErrors = new LinkedHashSet<>();
}
orderErrors.add(item);
} else {
items.add(item);
}
}
return orderErrors;
}
private void checkNothingAfter1(String value, List<CheckStatus> result) {
if (!value.endsWith("{1}")) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid list pattern «"
+ value
+ "», no text can come after {1}."));
}
}
private void checkNothingBefore0(String value, List<CheckStatus> result) {
if (!value.startsWith("{0}")) {
result.add(
new CheckStatus()
.setCause(this)
.setMainType(CheckStatus.errorType)
.setSubtype(Subtype.invalidPlaceHolder)
.setMessage(
"Invalid list pattern «"
+ value
+ "», no text can come before {0}."));
}
}
}