diff options
10 files changed, 172 insertions, 10 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java index 9fa5018..1ab02c3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java @@ -28,6 +28,7 @@ import com.android.tools.lint.checks.PxUsageDetector; import com.android.tools.lint.checks.ScrollViewChildDetector; import com.android.tools.lint.checks.SecurityDetector; import com.android.tools.lint.checks.TextFieldDetector; +import com.android.tools.lint.checks.TranslationDetector; import com.android.tools.lint.checks.TypoDetector; import com.android.tools.lint.checks.TypographyDetector; import com.android.tools.lint.checks.UseCompoundDrawableDetector; @@ -153,6 +154,7 @@ abstract class LintFix implements ICompletionProposal { sFixes.put(PxUsageDetector.PX_ISSUE.getId(), ConvertToDpFix.class); sFixes.put(TextFieldDetector.ISSUE.getId(), SetAttributeFix.class); sFixes.put(SecurityDetector.EXPORTED_SERVICE.getId(), SetAttributeFix.class); + sFixes.put(TranslationDetector.MISSING.getId(), SetAttributeFix.class); sFixes.put(DetectMissingPrefix.MISSING_NAMESPACE.getId(), AddPrefixFix.class); sFixes.put(ScrollViewChildDetector.ISSUE.getId(), SetScrollViewSizeFix.class); sFixes.put(ObsoleteLayoutParamsDetector.ISSUE.getId(), ObsoleteLayoutParamsFix.class); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java index a07101f..5d38df2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java @@ -98,6 +98,7 @@ import java.util.List; * in the Problems view; perhaps we should use a custom view for these. That would also * make marker management more obvious. */ +@SuppressWarnings("restriction") // DOM model public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssistProcessor { /** Constructs a new {@link LintFixGenerator} */ public LintFixGenerator() { @@ -248,7 +249,6 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi * * @param marker the marker pointing to the error to be suppressed */ - @SuppressWarnings("restriction") // XML model public static void addSuppressAnnotation(IMarker marker) { String id = EclipseLintClient.getId(marker); if (id != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java index 896966e..a860c69 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java @@ -18,17 +18,18 @@ package com.android.ide.eclipse.adt.internal.lint; import static com.android.ide.common.layout.LayoutConstants.ATTR_CONTENT_DESCRIPTION; import static com.android.ide.common.layout.LayoutConstants.ATTR_INPUT_TYPE; import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE; +import static com.android.tools.lint.detector.api.LintConstants.ATTR_TRANSLATABLE; import com.android.tools.lint.checks.AccessibilityDetector; import com.android.tools.lint.checks.InefficientWeightDetector; import com.android.tools.lint.checks.SecurityDetector; import com.android.tools.lint.checks.TextFieldDetector; +import com.android.tools.lint.checks.TranslationDetector; import com.android.tools.lint.detector.api.LintConstants; import org.eclipse.core.resources.IMarker; /** Shared fix class for various builtin attributes */ -@SuppressWarnings("restriction") // DOM model final class SetAttributeFix extends SetPropertyFix { private SetAttributeFix(String id, IMarker marker) { super(id, marker); @@ -44,6 +45,8 @@ final class SetAttributeFix extends SetPropertyFix { return LintConstants.ATTR_PERMISSION; } else if (mId.equals(TextFieldDetector.ISSUE.getId())) { return ATTR_INPUT_TYPE; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return ATTR_TRANSLATABLE; } else { assert false : mId; return ""; @@ -51,6 +54,15 @@ final class SetAttributeFix extends SetPropertyFix { } @Override + protected boolean isAndroidAttribute() { + if (mId.equals(TranslationDetector.MISSING.getId())) { + return false; + } + + return true; + } + + @Override public String getDisplayString() { if (mId.equals(AccessibilityDetector.ISSUE.getId())) { return "Add content description attribute"; @@ -60,6 +72,8 @@ final class SetAttributeFix extends SetPropertyFix { return "Set input type"; } else if (mId.equals(SecurityDetector.EXPORTED_SERVICE.getId())) { return "Add permission attribute"; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return "Mark this as a non-translatable resource"; } else { assert false : mId; return ""; @@ -67,15 +81,37 @@ final class SetAttributeFix extends SetPropertyFix { } @Override + public String getAdditionalProposalInfo() { + String help = super.getAdditionalProposalInfo(); + + if (mId.equals(TranslationDetector.MISSING.getId())) { + help = "<b>Adds translatable=\"false\" to this <string>.</b><br><br>" + help; + } + + return help; + } + + @Override protected boolean invokeCodeCompletion() { return mId.equals(SecurityDetector.EXPORTED_SERVICE.getId()) || mId.equals(TextFieldDetector.ISSUE.getId()); } @Override + public boolean selectValue() { + if (mId.equals(TranslationDetector.MISSING.getId())) { + return false; + } else { + return super.selectValue(); + } + } + + @Override protected String getProposal() { if (mId.equals(InefficientWeightDetector.BASELINE_WEIGHTS.getId())) { return VALUE_FALSE; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return VALUE_FALSE; } return super.getProposal(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java index 2bfe5e8..8b32734 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java @@ -48,6 +48,9 @@ abstract class SetPropertyFix extends DocumentFix { /** Attribute to be added */ protected abstract String getAttribute(); + /** Whether it's in the android: namespace */ + protected abstract boolean isAndroidAttribute(); + protected String getProposal() { return invokeCodeCompletion() ? "" : "TODO"; //$NON-NLS-1$ } @@ -70,7 +73,10 @@ abstract class SetPropertyFix extends DocumentFix { Element element = (Element) node; String proposal = getProposal(); String localAttribute = getAttribute(); - String prefix = XmlUtils.lookupNamespacePrefix(node, ANDROID_URI); + String prefix = null; + if (isAndroidAttribute()) { + prefix = XmlUtils.lookupNamespacePrefix(node, ANDROID_URI); + } String attribute = prefix != null ? prefix + ':' + localAttribute : localAttribute; // This does not work even though it should: it does not include the prefix @@ -78,18 +84,29 @@ abstract class SetPropertyFix extends DocumentFix { // So workaround instead: element.setAttribute(attribute, proposal); - Attr attr = element.getAttributeNodeNS(ANDROID_URI, localAttribute); + Attr attr = null; + if (isAndroidAttribute()) { + attr = element.getAttributeNodeNS(ANDROID_URI, localAttribute); + } else { + attr = element.getAttributeNode(localAttribute); + } if (attr instanceof IndexedRegion) { IndexedRegion region = (IndexedRegion) attr; int offset = region.getStartOffset(); // We only want to select the value part inside the quotes, // so skip the attribute and =" parts added by WST: offset += attribute.length() + 2; - mSelect = new Region(offset, proposal.length()); + if (selectValue()) { + mSelect = new Region(offset, proposal.length()); + } } } } + protected boolean selectValue() { + return true; + } + @Override public void apply(IDocument document) { try { diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java index 85995b1..1d9ba2e 100644 --- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java +++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java @@ -670,4 +670,41 @@ public class LintUtils { return hasManifest; } + + /** + * Look up the locale and region from the given parent folder name and + * return it as a combined string, such as "en", "en-rUS", etc, or null if + * no language is specified. + * + * @param folderName the folder name + * @return the locale+region string or null + */ + @Nullable + public static String getLocaleAndRegion(@NonNull String folderName) { + if (folderName.equals("values")) { //$NON-NLS-1$ + return null; + } + + String locale = null; + + for (String qualifier : Splitter.on('-').split(folderName)) { + int qualifierLength = qualifier.length(); + if (qualifierLength == 2) { + char first = qualifier.charAt(0); + char second = qualifier.charAt(1); + if (first >= 'a' && first <= 'z' && second >= 'a' && second <= 'z') { + locale = qualifier; + } + } else if (qualifierLength == 3 && qualifier.charAt(0) == 'r' && locale != null) { + char first = qualifier.charAt(1); + char second = qualifier.charAt(2); + if (first >= 'A' && first <= 'Z' && second >= 'A' && second <= 'Z') { + return locale + '-' + qualifier; + } + break; + } + } + + return locale; + } } diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/TranslationDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TranslationDetector.java index f89fb81..391033e 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/TranslationDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TranslationDetector.java @@ -30,6 +30,7 @@ import com.android.resources.ResourceFolderType; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.ResourceXmlDetector; import com.android.tools.lint.detector.api.Scope; @@ -73,6 +74,12 @@ public class TranslationDetector extends ResourceXmlDetector { "If an application has more than one locale, then all the strings declared in " + "one language should also be translated in all other languages.\n" + "\n" + + "If the string should *not* be translated, you can add the attribute\n" + + "translatable=\"false\" on the <string> element, or you can define all " + + "your non-translatable strings in a resource file called \"donottranslate.xml\". " + + "Or, you can ignore the issue with a tools:ignore=\"MissingTranslation\" " + + "attribute.\n" + + "\n" + "By default this detector allows regions of a language to just provide a " + "subset of the strings and fall back to the standard language strings. " + "You can require all regions to provide a full translation by setting the " + @@ -90,15 +97,19 @@ public class TranslationDetector extends ResourceXmlDetector { "If a string appears in a specific language translation file, but there is " + "no corresponding string in the default locale, then this string is probably " + "unused. (It's technically possible that your application is only intended to " + - "run in a specific locale, but it's still a good idea to provide a fallback.)", + "run in a specific locale, but it's still a good idea to provide a fallback.).\n" + + "\n" + + "Note that these strings can lead to crashes if the string is looked up on any " + + "locale not providing a translation, so it's important to clean them up.", Category.MESSAGES, 6, - Severity.WARNING, + Severity.FATAL, TranslationDetector.class, Scope.ALL_RESOURCES_SCOPE); private Set<String> mNames; private Set<String> mTranslatedArrays; + private Set<String> mNonTranslatable; private boolean mIgnoreFile; private Map<File, Set<String>> mFileToNames; @@ -457,6 +468,17 @@ public class TranslationDetector extends ResourceXmlDetector { Attr translatable = element.getAttributeNode(ATTR_TRANSLATABLE); if (translatable != null && !Boolean.valueOf(translatable.getValue())) { + String l = LintUtils.getLocaleAndRegion(context.file.getParentFile().getName()); + if (l != null) { + context.report(EXTRA, context.getLocation(translatable), + "Non-translatable resources should only be defined in the base " + + "values/ folder", null); + } else { + if (mNonTranslatable == null) { + mNonTranslatable = new HashSet<String>(); + } + mNonTranslatable.add(name); + } return; } @@ -489,6 +511,12 @@ public class TranslationDetector extends ResourceXmlDetector { mNames.add(name); + if (mNonTranslatable != null && mNonTranslatable.contains(name)) { + String message = String.format("The resource string \"%1$s\" has been marked as " + + "translatable=\"false\"", name); + context.report(EXTRA, context.getLocation(attribute), message, null); + } + // TBD: Also make sure that the strings are not empty or placeholders? } } diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TranslationDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TranslationDetectorTest.java index 6f1c2e6..39231dd 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TranslationDetectorTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TranslationDetectorTest.java @@ -34,9 +34,9 @@ public class TranslationDetectorTest extends AbstractCheckTest { TranslationDetector.COMPLETE_REGIONS = false; assertEquals( // Sample files from the Home app - "values-cs/arrays.xml:3: Warning: \"security_questions\" is translated here but not found in default locale\n" + + "values-cs/arrays.xml:3: Error: \"security_questions\" is translated here but not found in default locale\n" + "=> values-es/strings.xml:12: Also translated here\n" + - "values-de-rDE/strings.xml:11: Warning: \"continue_skip_label\" is translated here but not found in default locale\n" + + "values-de-rDE/strings.xml:11: Error: \"continue_skip_label\" is translated here but not found in default locale\n" + "values/strings.xml:20: Error: \"show_all_apps\" is not translated in nl-rNL\n" + "values/strings.xml:23: Error: \"menu_wallpaper\" is not translated in nl-rNL\n" + "values/strings.xml:25: Error: \"menu_settings\" is not translated in cs, de-rDE, es, es-rUS, nl-rNL", @@ -57,7 +57,7 @@ public class TranslationDetectorTest extends AbstractCheckTest { TranslationDetector.COMPLETE_REGIONS = true; assertEquals( // Sample files from the Home app - "values-de-rDE/strings.xml:11: Warning: \"continue_skip_label\" is translated here but not found in default locale\n" + + "values-de-rDE/strings.xml:11: Error: \"continue_skip_label\" is translated here but not found in default locale\n" + "values/strings.xml:19: Error: \"home_title\" is not translated in es-rUS\n" + "values/strings.xml:20: Error: \"show_all_apps\" is not translated in es-rUS, nl-rNL\n" + "values/strings.xml:23: Error: \"menu_wallpaper\" is not translated in es-rUS, nl-rNL\n" + @@ -137,4 +137,25 @@ public class TranslationDetectorTest extends AbstractCheckTest { "res/values-cs/strings.xml=>../LibraryProject/res/values-nl/strings.xml" )); } + + public void testNonTranslatable1() throws Exception { + TranslationDetector.COMPLETE_REGIONS = true; + assertEquals( + // Sample files from the Home app + "values-nb/nontranslatable.xml:3: Error: The resource string \"dummy\" has been " + + "marked as translatable=\"false\"", + + lintProject("res/values/nontranslatable.xml", + "res/values/nontranslatable2.xml=>res/values-nb/nontranslatable.xml")); + } + + public void testNonTranslatable2() throws Exception { + TranslationDetector.COMPLETE_REGIONS = true; + assertEquals( + // Sample files from the Home app + "values-nb/nontranslatable.xml:3: Error: Non-translatable resources should only " + + "be defined in the base values/ folder", + + lintProject("res/values/nontranslatable.xml=>res/values-nb/nontranslatable.xml")); + } } diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable.xml new file mode 100644 index 0000000..f608bff --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="dummy" translatable="false">Ignore Me</string> +</resources> + diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable2.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable2.xml new file mode 100644 index 0000000..4fcfdc6 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/nontranslatable2.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="dummy">Ignore Me</string> +</resources> + diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java index b1ee6d6..8379618 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java @@ -17,6 +17,7 @@ package com.android.tools.lint.detector.api; import static com.android.tools.lint.detector.api.LintUtils.splitPath; +import static com.android.tools.lint.detector.api.LintUtils.getLocaleAndRegion; import com.android.tools.lint.Main; import com.google.common.collect.Iterables; @@ -274,4 +275,14 @@ public class LintUtilsTest extends TestCase { checkEncoding("UTF_32", true /*bom*/, "\r\n"); checkEncoding("UTF_32LE", true /*bom*/, "\r\n"); } + + public void testGetLocaleAndRegion() throws Exception { + assertNull(getLocaleAndRegion("")); + assertNull(getLocaleAndRegion("values")); + assertNull(getLocaleAndRegion("values-xlarge-port")); + assertEquals("en", getLocaleAndRegion("values-en")); + assertEquals("pt-rPT", getLocaleAndRegion("values-pt-rPT-nokeys")); + assertEquals("zh-rCN", getLocaleAndRegion("values-zh-rCN-keyshidden")); + assertEquals("ms", getLocaleAndRegion("values-ms-keyshidden")); + } }
\ No newline at end of file |