/* * Copyright (C) 2014 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.idea.editors.strings; import com.android.SdkConstants; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.res2.ResourceItem; import com.android.tools.idea.configurations.LocaleMenuAction; import com.android.tools.idea.rendering.Locale; import com.android.tools.idea.rendering.PsiResourceItem; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.*; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.xml.XmlTag; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class StringResourceData { private final AndroidFacet myFacet; private final List<String> myKeys; private final Set<String> myUntranslatableKeys; private final List<Locale> myLocales; private final Map<String, ResourceItem> myDefaultValues; private final HashBasedTable<String, Locale, ResourceItem> myTranslations; public StringResourceData(@NotNull AndroidFacet facet, @NotNull List<String> keys, @NotNull Collection<String> untranslatableKeys, @NotNull Collection<Locale> locales, @NotNull Map<String, ResourceItem> defaultValues, @NotNull Table<String, Locale, ResourceItem> translations) { myFacet = facet; myKeys = Lists.newArrayList(keys); myUntranslatableKeys = Sets.newHashSet(untranslatableKeys); myLocales = Lists.newArrayList(locales); myDefaultValues = Maps.newHashMap(defaultValues); myTranslations = HashBasedTable.create(translations); } @NotNull public List<String> getKeys() { return myKeys; } @NotNull public Set<String> getUntranslatableKeys() { return myUntranslatableKeys; } @NotNull public List<Locale> getLocales() { return myLocales; } @NotNull public Map<String, ResourceItem> getDefaultValues() { return myDefaultValues; } @NotNull public Table<String, Locale, ResourceItem> getTranslations() { return myTranslations; } @NotNull public static String resourceToString(@NotNull ResourceItem item) { ResourceValue value = item.getResourceValue(false); return value == null ? "" : value.getRawXmlValue().trim(); } @Nullable public static XmlTag resourceToXmlTag(@NotNull ResourceItem item) { if (item instanceof PsiResourceItem) { XmlTag tag = ((PsiResourceItem) item).getTag(); return tag != null && tag.isValid() ? tag : null; } return null; } public void changeKeyName(int index, String name) { if (index >= myKeys.size()) { throw new IllegalArgumentException(String.format("Cannot change key at index %1$d (# of entries: %2$d)", index, myKeys.size())); } if (myKeys.contains(name)) { throw new IllegalArgumentException("Key " + name + " already exists."); } String currentName = myKeys.get(index); ResourceItem defaultValue = myDefaultValues.get(currentName); Map<Locale, ResourceItem> translations = myTranslations.row(currentName); myKeys.remove(index); myKeys.add(name); Collections.sort(myKeys); if (defaultValue != null) { myDefaultValues.remove(currentName); myDefaultValues.put(name, defaultValue); } if (!translations.isEmpty()) { // TODO: can this be done? wouldn't this have to be re-read since the ResourceItems might be different? // TODO: Is this whole thing better done as a refactoring operation? myTranslations.row(name).putAll(translations); translations.clear(); } } public boolean setDoNotTranslate(String key, boolean doNotTranslate) { ResourceItem item = myDefaultValues.get(key); if (item != null) { String translatable; if (doNotTranslate) { translatable = SdkConstants.VALUE_FALSE; myUntranslatableKeys.add(key); } else{ translatable = null; myUntranslatableKeys.remove(key); } return StringsWriteUtils.setAttributeForItems(myFacet.getModule().getProject(), SdkConstants.ATTR_TRANSLATABLE, translatable, Collections.singletonList(item)); } return false; } public boolean setTranslation(@NotNull String key, @Nullable Locale locale, @NotNull String value) { ResourceItem currentItem = locale == null ? myDefaultValues.get(key) : myTranslations.get(key, locale); if (currentItem != null) { // modify existing item String oldText = resourceToString(currentItem); if (!StringUtil.equals(oldText, value)) { boolean changed = StringsWriteUtils.setItemText(myFacet.getModule().getProject(), currentItem, value); if (changed && value.isEmpty()) { if (locale == null) { myDefaultValues.remove(key); } else { myTranslations.remove(key, locale); } } return changed; } } else { // create new item ResourceItem item = StringsWriteUtils.createItem(myFacet, myFacet.getPrimaryResourceDir(), locale, key, value, !getUntranslatableKeys().contains(key)); if (item != null) { if (locale == null) { myDefaultValues.put(key, item); } else { myTranslations.put(key, locale, item); } return true; } return false; } return false; } @Nullable public String validateKey(@NotNull String key) { if (!myKeys.contains(key)) { throw new IllegalArgumentException("Key " + key + " does not exist."); } Map<Locale, ResourceItem> translationsForKey = myTranslations.row(key); if (myUntranslatableKeys.contains(key)) { if (!translationsForKey.isEmpty()) { Set<Locale> localesWithTranslation = translationsForKey.keySet(); return String.format("Key '%1$s' is marked as non translatable, but is translated in %2$s %3$s", key, StringUtil.pluralize("locale", localesWithTranslation.size()), summarizeLocales(localesWithTranslation)); } } else { // translatable key if (myDefaultValues.get(key) == null) { return "Key '" + key + "' missing default value"; } Set<Locale> missingTranslations = getMissingTranslations(key); if (!missingTranslations.isEmpty()) { return String.format("Key '%1$s' has translations missing for %2$s %3$s", key, StringUtil.pluralize("locale", missingTranslations.size()), summarizeLocales(missingTranslations)); } } return null; } @Nullable public String validateTranslation(@NotNull String key, @Nullable Locale locale) { if (!myKeys.contains(key)) { throw new IllegalArgumentException("Key " + key + " does not exist."); } if (locale == null) { ResourceItem item = myDefaultValues.get(key); return (item == null) ? String.format("Key '%1$s' is missing the default value", key) : null; } final boolean translationMissing = isTranslationMissing(key, locale); final boolean doNotTranslate = myUntranslatableKeys.contains(key); if (translationMissing && !doNotTranslate) { return String.format("Key '%1$s' is missing %2$s translation", key, getLabel(locale)); } else if (doNotTranslate && !translationMissing) { return String.format("Key '%1$s' is marked as non-localizable, and should not be translated to %2$s", key, getLabel(locale)); } return null; } @NotNull private Set<Locale> getMissingTranslations(@NotNull String key) { Map<Locale, ResourceItem> translations = myTranslations.row(key); if (translations.size() == myLocales.size()) { return Collections.emptySet(); } Set<Locale> missingTranslations = Sets.newHashSet(); for (Locale locale : myLocales) { if (isTranslationMissing(key, locale)) { missingTranslations.add(locale); } } return missingTranslations; } private boolean isTranslationMissing(@NotNull String key, @NotNull Locale locale) { // TODO: do we need to check the value of resource item != empty or just item != null? if (myTranslations.get(key, locale) != null) { return false; } if (locale.hasRegion()) { Locale base = Locale.create(locale.language, null); if (myTranslations.get(key, base) != null) { return false; } } return true; } @VisibleForTesting @NotNull static String summarizeLocales(@NotNull Iterable<Locale> locales) { if (Iterables.isEmpty(locales)) { return ""; } final int size = Iterables.size(locales); final int max = 3; if (size == 1) { return getLabel(Iterables.getFirst(locales, null)); } else if (size <= max) { return String.format("%1$s and %2$s", getLabels(Iterables.limit(locales, size - 1)), getLabel(Iterables.get(locales, size - 1))); } else { return String.format("%1$s and %2$d more", getLabels(Iterables.limit(locales, max)), size - max); } } private static String getLabels(Iterable<Locale> locales) { return Joiner.on(", ").join(Iterables.transform(locales, new Function<Locale, String>() { @Override public String apply(Locale locale) { return getLabel(locale); } })); } private static String getLabel(@Nullable Locale locale) { return locale == null ? "" : LocaleMenuAction.getLocaleLabel(locale, false); } }