All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.PluralsDetector Maven / Gradle / Ivy

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tools.lint.checks;

import static com.android.SdkConstants.ATTR_QUANTITY;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_PLURALS;
import static com.android.tools.lint.checks.PluralsDetector.Quantity.few;
import static com.android.tools.lint.checks.PluralsDetector.Quantity.many;
import static com.android.tools.lint.checks.PluralsDetector.Quantity.one;
import static com.android.tools.lint.checks.PluralsDetector.Quantity.two;
import static com.android.tools.lint.checks.PluralsDetector.Quantity.zero;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Maps;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

/**
 * Checks for issues with quantity strings
 * https://code.google.com/p/android/issues/detail?id=53015
 * 53015: lint could report incorrect usage of Resource.getQuantityString
 */
public class PluralsDetector extends ResourceXmlDetector {
    private static final Implementation IMPLEMENTATION = new Implementation(
            PluralsDetector.class,
            Scope.RESOURCE_FILE_SCOPE);

    /** This locale should define a quantity string for the given quantity */
    public static final Issue MISSING = Issue.create(
            "MissingQuantity", //$NON-NLS-1$
            "Missing quantity translation",
            "Checks for missing quantity strings relevant to each locale",
            "Different languages have different rules for grammatical agreement with " +
            "quantity. In English, for example, the quantity 1 is a special case. " +
            "We write \"1 book\", but for any other quantity we'd write \"n books\". " +
            "This distinction between singular and plural is very common, but other " +
            "languages make finer distinctions.\n" +
            "\n" +
            "This lint check looks at each translation of a `` and makes sure " +
            "that all the quantity strings considered by the given language are provided " +
            "by this translation.\n" +
            "\n" +
            "For example, an English translation must provide a string for `quantity=\"one\"`. " +
            "Similarly, a Czech translation must provide a string for `quantity=\"few\"`.",
            Category.MESSAGES,
            8,
            Severity.WARNING,
            IMPLEMENTATION).addMoreInfo(
            "http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");

    /** This translation is not needed in this locale */
    public static final Issue EXTRA = Issue.create(
            "UnusedQuantity", //$NON-NLS-1$
            "Unused quantity translations",
            "Checks for quantity string translations which are not used in this language",
            "Android defines a number of different quantity strings, such as `zero`, `one`, " +
            "`few` and `many`. However, many languages do not distinguish grammatically " +
            "between all these different quantities.\n" +
            "\n" +
            "This lint check looks at the quantity strings defined for each translation and " +
            "flags any quantity strings that are unused (because the language does not make that " +
            "quantity distinction, and Android will therefore not look it up.)." +
            "\n" +
            "For example, in Chinese, only the `other` quantity is used, so even if you " +
            "provide translations for `zero` and `one`, these strings will *not* be returned " +
            "when `getQuantityString()` is called, even with `0` or `1`.",
            Category.MESSAGES,
            3,
            Severity.WARNING,
            IMPLEMENTATION).addMoreInfo(
            "http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");

    /** Constructs a new {@link PluralsDetector} */
    public PluralsDetector() {
    }

    @Override
    public boolean appliesTo(@NonNull ResourceFolderType folderType) {
        return folderType == ResourceFolderType.VALUES;
    }

    @Override
    public Collection getApplicableElements() {
        return Collections.singletonList(TAG_PLURALS);
    }

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        int count = LintUtils.getChildCount(element);
        if (count == 0) {
            context.report(MISSING, element, context.getLocation(element),
                    "There should be at least one quantity string in this  definition",
                    null);
            return;
        }

        Pair locale = TypoDetector.getLocale(context);
        if (locale == null) {
            return;
        }
        String language = locale.getFirst();
        if (language == null) {
            return;
        }

        EnumSet defined = EnumSet.noneOf(Quantity.class);
        NodeList children = element.getChildNodes();
        for (int i = 0, n = children.getLength(); i < n; i++) {
            Node noe = children.item(i);
            if (noe.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            Element child = (Element) noe;
            if (!TAG_ITEM.equals(child.getTagName())) {
                continue;
            }
            String quantityString = child.getAttribute(ATTR_QUANTITY);
            if (quantityString == null || quantityString.isEmpty()) {
                continue;
            }
            Quantity quantity = Quantity.get(quantityString);
            if (quantity == Quantity.other) { // Not stored in the database
                continue;
            }
            if (quantity != null) {
                defined.add(quantity);
            }
        }

        EnumSet relevant = getRelevant(language);
        if (relevant == null) {
            return;
        }

        if (relevant.equals(defined)) {
            return;
        }

        // Look for missing
        EnumSet missing = relevant.clone();
        missing.removeAll(defined);
        if (!missing.isEmpty()) {
            String message = String.format(
                    "For locale \"%1$s\" the following quantities should also be defined: %2$s",
                    language, formatSet(missing));
            context.report(MISSING, element, context.getLocation(element), message, null);
        }

        // Look for irrelevant
        EnumSet extra = defined.clone();
        extra.removeAll(relevant);
        if (!extra.isEmpty()) {
            String message = String.format(
                    "For language \"%1$s\" the following quantities are not relevant: %2$s",
                    language, formatSet(extra));
            context.report(MISSING, element, context.getLocation(element), message, null);
        }
    }

    private static String formatSet(EnumSet set) {
        List list = new ArrayList(set.size());
        for (Quantity quantity : set) {
            list.add(quantity.name());
        }
        return LintUtils.formatList(list, Integer.MAX_VALUE);
    }

    enum Quantity {
        few, many, one, two, zero, other; // deliberately lower case to match attribute names

        @Nullable
        public static Quantity get(@NonNull String name) {
            for (Quantity quantity : values()) {
                if (name.equals(quantity.name())) {
                    return quantity;
                }
            }

            return null;
        }
    }

    private static Map> sPlurals;

    @Nullable
    public EnumSet getRelevant(@NonNull String language) {
        // Based on the plurals table in plurals.txt in icu4c
        if (sPlurals == null) {
            EnumSet empty = EnumSet.noneOf(Quantity.class);
            EnumSet set1 = EnumSet.of(few, many, one, two, zero);
            EnumSet set10 = EnumSet.of(few, many, one);
            EnumSet set11 = EnumSet.of(few, one);
            EnumSet set12 = set10;
            EnumSet set13 = EnumSet.of(few, one, two);
            EnumSet set14 = set12;
            EnumSet set15 = EnumSet.of(one);
            EnumSet set16 = set1;
            EnumSet set17 = EnumSet.of(one, zero);
            EnumSet set18 = set11;
            EnumSet set19 = EnumSet.of(few, many, one, two);
            EnumSet set2 = set15;
            EnumSet set20 = set17;
            EnumSet set21 = set2;
            EnumSet set22 = set2;
            EnumSet set23 = set13;
            EnumSet set3 = set2;
            EnumSet set4 = set2;
            EnumSet set5 = set20;
            EnumSet set6 = EnumSet.of(one, two);
            EnumSet set7 = set19;
            EnumSet set8 = set11;
            EnumSet set9 = set8;

            final int INITIAL_CAPACITY = 124;
            sPlurals = Maps.newHashMapWithExpectedSize(INITIAL_CAPACITY);
            sPlurals.put("af", set2);
            sPlurals.put("ak", set3);
            sPlurals.put("am", set3);
            sPlurals.put("ar", set1);
            sPlurals.put("az", empty);
            sPlurals.put("be", set10);
            sPlurals.put("bg", set2);
            sPlurals.put("bh", set3);
            sPlurals.put("bm", empty);
            sPlurals.put("bn", set2);
            sPlurals.put("bo", empty);
            sPlurals.put("br", set19);
            sPlurals.put("bs", set10);
            sPlurals.put("ca", set2);
            sPlurals.put("cs", set11);
            sPlurals.put("cy", set16);
            sPlurals.put("da", set2);
            sPlurals.put("de", set2);
            sPlurals.put("dv", set2);
            sPlurals.put("dz", empty);
            sPlurals.put("ee", set2);
            sPlurals.put("el", set2);
            sPlurals.put("en", set2);
            sPlurals.put("eo", set2);
            sPlurals.put("es", set2);
            sPlurals.put("et", set2);
            sPlurals.put("eu", set2);
            sPlurals.put("fa", empty);
            sPlurals.put("ff", set4);
            sPlurals.put("fi", set2);
            sPlurals.put("fo", set2);
            sPlurals.put("fr", set4);
            sPlurals.put("fy", set2);
            sPlurals.put("ga", set7);
            sPlurals.put("gd", set23);
            sPlurals.put("gl", set2);
            sPlurals.put("gu", set2);
            sPlurals.put("gv", set22);
            sPlurals.put("ha", set2);
            sPlurals.put("he", set2);
            sPlurals.put("hi", set3);
            sPlurals.put("hr", set10);
            sPlurals.put("hu", empty);
            sPlurals.put("id", empty);
            sPlurals.put("ig", empty);
            sPlurals.put("ii", empty);
            sPlurals.put("is", set2);
            sPlurals.put("it", set2);
            sPlurals.put("iu", set6);
            sPlurals.put("ja", empty);
            sPlurals.put("jv", empty);
            sPlurals.put("ka", empty);
            sPlurals.put("kk", set2);
            sPlurals.put("kl", set2);
            sPlurals.put("km", empty);
            sPlurals.put("kn", empty);
            sPlurals.put("ko", empty);
            sPlurals.put("ku", set2);
            sPlurals.put("kw", set6);
            sPlurals.put("lb", set2);
            sPlurals.put("lg", set2);
            sPlurals.put("ln", set3);
            sPlurals.put("lo", empty);
            sPlurals.put("lt", set9);
            sPlurals.put("lv", set5);
            sPlurals.put("mg", set3);
            sPlurals.put("mk", set15);
            sPlurals.put("ml", set2);
            sPlurals.put("mn", set2);
            sPlurals.put("mo", set8);
            sPlurals.put("mr", set2);
            sPlurals.put("ms", empty);
            sPlurals.put("mt", set14);
            sPlurals.put("my", empty);
            sPlurals.put("nb", set2);
            sPlurals.put("nd", set2);
            sPlurals.put("ne", set2);
            sPlurals.put("nl", set2);
            sPlurals.put("nn", set2);
            sPlurals.put("no", set2);
            sPlurals.put("nr", set2);
            sPlurals.put("ny", set2);
            sPlurals.put("om", set2);
            sPlurals.put("or", set2);
            sPlurals.put("pa", set2);
            sPlurals.put("pl", set12);
            sPlurals.put("ps", set2);
            sPlurals.put("pt", set2);
            sPlurals.put("rm", set2);
            sPlurals.put("ro", set8);
            sPlurals.put("ru", set10);
            sPlurals.put("se", set6);
            sPlurals.put("sg", empty);
            sPlurals.put("sh", set10);
            sPlurals.put("sk", set11);
            sPlurals.put("sl", set13);
            sPlurals.put("sn", set2);
            sPlurals.put("so", set2);
            sPlurals.put("sq", set2);
            sPlurals.put("sr", set10);
            sPlurals.put("ss", set2);
            sPlurals.put("st", set2);
            sPlurals.put("sv", set2);
            sPlurals.put("sw", set2);
            sPlurals.put("ta", set2);
            sPlurals.put("te", set2);
            sPlurals.put("th", empty);
            sPlurals.put("ti", set3);
            sPlurals.put("tk", set2);
            sPlurals.put("tl", set3);
            sPlurals.put("tn", set2);
            sPlurals.put("to", empty);
            sPlurals.put("tr", empty);
            sPlurals.put("ts", set2);
            sPlurals.put("uk", set10);
            sPlurals.put("ur", set2);
            sPlurals.put("ve", set2);
            sPlurals.put("vi", empty);
            sPlurals.put("wa", set3);
            sPlurals.put("wo", empty);
            sPlurals.put("xh", set2);
            sPlurals.put("yo", empty);
            sPlurals.put("zh", empty);
            sPlurals.put("zu", set2);
            assert sPlurals.size() == INITIAL_CAPACITY : sPlurals.size();
        }
        return sPlurals.get(language);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy