blob: fb94bc36efaabb9512d582fc9c257bb1d5548474 [file] [log] [blame]
/*
* 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()) {
// Drop region
Locale base = Locale.create(locale.qualifier.getLanguage());
if (myTranslations.get(key, base) != null) {
return false;
}
}
return true;
}
@VisibleForTesting
@NotNull
static String summarizeLocales(@NotNull Collection<Locale> locales) {
if (locales.isEmpty()) {
return "";
}
final int size = locales.size();
if (size == 1) {
return getLabel(Iterables.getFirst(locales, null));
}
final int max = 3;
List<Locale> sorted = getLowest(locales, max);
if (size <= max) {
return String.format("%1$s and %2$s", getLabels(Iterables.limit(sorted, size - 1)), getLabel(sorted.get(size - 1)));
}
else {
return String.format("%1$s and %2$d more", getLabels(sorted), size - max);
}
}
private static List<Locale> getLowest(Collection<Locale> locales, int n) {
List<Locale> result = Lists.newArrayListWithExpectedSize(n);
List<Locale> input = Lists.newArrayList(locales);
Comparator<Locale> comparator = new Comparator<Locale>() {
@Override
public int compare(Locale l1, Locale l2) {
return getLabel(l1).compareTo(getLabel(l2));
}
};
// rather than sorting the whole list, we just extract the first n
for (int i = 0; i < locales.size() && i < n; i++) {
Locale min = Collections.min(input, comparator);
result.add(min);
input.remove(min);
}
return result;
}
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);
}
}