/* * 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.idea.configurations; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; import com.android.ide.common.res2.ValueXmlHelper; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.LanguageQualifier; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.idea.rendering.LocalResourceRepository; import com.android.tools.idea.rendering.Locale; import com.android.tools.idea.rendering.ProjectResourceRepository; import com.android.utils.Pair; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTagValue; import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.table.JBTable; import com.intellij.util.ui.EditableModel; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.util.AndroidResourceUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.table.AbstractTableModel; import java.awt.*; import java.io.IOException; import java.util.*; import java.util.List; import static com.android.SdkConstants.ATTR_NAME; // TODO: How do we deal with translations for string arrays, plurals, etc? // TODO: We're currently editing the flattened strings (e.g. the ones prepared for // text by the same code as the layout editor. Should we let users edit markup like \n and " and \u0041? public class TranslationDialog extends DialogWrapper { private final AndroidFacet myFacet; private Locale myLocale; private TranslationModel myModel; private boolean myCreate; TranslationDialog(@NotNull AndroidFacet facet, @NotNull Locale locale, boolean create) { super(facet.getModule().getProject()); myFacet = facet; myLocale = locale; myCreate = create; String localeLabel = LocaleMenuAction.getLocaleLabel(myLocale, false); setTitle(String.format((create ? "Add" : "Edit") + " Translation for %1$s", localeLabel)); init(); setOKActionEnabled(false); } @Nullable @Override protected JComponent createCenterPanel() { myModel = new TranslationModel(); final JBTable table = new JBTable(myModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setStriped(true); JTextField textField = new JTextField(); DefaultCellEditor cellEditor = new DefaultCellEditor(textField); cellEditor.setClickCountToStart(1); table.setDefaultEditor(String.class, cellEditor); // Ensure we don't crop text when editing with the cell editor table.setRowHeight(textField.getPreferredSize().height); ToolbarDecorator decorator = ToolbarDecorator.createDecorator(table).disableAddAction().disableRemoveAction(); JPanel panel = decorator.createPanel(); panel.setPreferredSize(new Dimension(800, 800)); if (myModel.getRowCount() > 0) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { table.requestFocus(); table.editCellAt(0, NEW_TRANSLATION_COLUMN); } }); } return panel; } public boolean createTranslation() { return myModel.createTranslation(); } private static final int KEY_COLUMN = 0; private static final int DEFAULT_TRANSLATION_COLUMN = 1; private static final int NEW_TRANSLATION_COLUMN = 2; private class TranslationModel extends AbstractTableModel implements EditableModel { private Map<String, String> myTranslations; private Map<String, ResourceValue> myValues; private String[] myKeys; private final FolderConfiguration myFolderConfig = new FolderConfiguration(); public TranslationModel() { LocalResourceRepository resources = ProjectResourceRepository.getProjectResources(myFacet, true); // Nonexistent language qualifier: trick it to fall back to the default locale myFolderConfig.setLanguageQualifier(new LanguageQualifier("xx")); myValues = resources.getConfiguredResources(ResourceType.STRING, myFolderConfig); myKeys = myValues.keySet().toArray(new String[myValues.size()]); Arrays.sort(myKeys); // TODO: Read in the actual XML files providing the default keys here // (they can be obtained via ResourceItem.getSourceFileList()) // such that we can read all the attributes associated with each // item, and if it defines translatable=false, or the filename is // donottranslate.xml, we can ignore it, and in other cases just // duplicate all the attributes (such as "formatted=true", or other // local conventions such as "product=tablet", or "msgid="123123123", // etc.) myTranslations = Maps.newHashMapWithExpectedSize(myKeys.length); if (!myCreate && myLocale.hasLanguage()) { FolderConfiguration config = new FolderConfiguration(); config.setLanguageQualifier(myLocale.language); if (myLocale.hasRegion()) { config.setRegionQualifier(myLocale.region); } for (String key : myKeys) { List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, key); if (items == null) { continue; } ResourceItem match = (ResourceItem) config.findMatchingConfigurable(items); if (match != null) { LanguageQualifier languageQualifier = match.getConfiguration().getLanguageQualifier(); if (!myLocale.language.equals(languageQualifier)) { // This configured value is not in the right language; that means this string // was not translated to this locale continue; } ResourceValue value = match.getResourceValue(false); if (value != null) { myTranslations.put(key, value.getValue()); } } } } } @Override public int getColumnCount() { return 3; } @Override public int getRowCount() { return myKeys.length; } @Override public Object getValueAt(int row, int col) { String key = myKeys[row]; switch (col) { case KEY_COLUMN : return key; case DEFAULT_TRANSLATION_COLUMN : { ResourceValue value = myValues.get(key); if (value != null) { return value.getValue(); } return ""; } case NEW_TRANSLATION_COLUMN : default: String translation = myTranslations.get(key); if (translation != null) { return translation; } return ""; } } @Override public String getColumnName(int column) { switch (column) { case KEY_COLUMN : return "Key"; case DEFAULT_TRANSLATION_COLUMN : return "Default"; case NEW_TRANSLATION_COLUMN : { return LocaleMenuAction.getLocaleLabel(myLocale, false); } default: assert false : column; return ""; } } @Override public Class getColumnClass(int c) { return String.class; } @Override public boolean isCellEditable(int row, int col) { return col == NEW_TRANSLATION_COLUMN; } @Override public void setValueAt(Object aValue, int row, int col) { String string = aValue.toString(); if (!string.isEmpty() && !isOKActionEnabled()) { setOKActionEnabled(true); } myTranslations.put(myKeys[row], string); } @Override public void addRow() { throw new UnsupportedOperationException(); } @Override public void removeRow(int index) { throw new UnsupportedOperationException(); } @Override public boolean canExchangeRows(int oldIndex, int newIndex) { return true; } @Override public void exchangeRows(int oldIndex, int newIndex) { String temp = myKeys[oldIndex]; myKeys[oldIndex] = myKeys[newIndex]; myKeys[newIndex] = temp; } /** Actually create the new translation file and write it to disk */ public boolean createTranslation() { Pair<String, VirtualFile> result = ApplicationManager.getApplication().runWriteAction(new Computable<Pair<String, VirtualFile>>() { @Nullable @Override public Pair<String, VirtualFile> compute() { // First update existing translations if (myCreate) { return createTranslationFile(); } else { return updateTranslations(); } } }); String error = result.getFirst(); VirtualFile newFile = result.getSecond(); Project project = myFacet.getModule().getProject(); if (error != null) { Messages.showErrorDialog(project, error, "Create Translation"); return false; } else if (newFile != null) { OpenFileDescriptor descriptor = new OpenFileDescriptor(project, newFile, -1); FileEditorManager.getInstance(project).openEditor(descriptor, true); } return true; } private Pair<String, VirtualFile> createTranslationFile() { FolderConfiguration folderConfig = new FolderConfiguration(); folderConfig.setLanguageQualifier(myLocale.language); if (myLocale.hasRegion()) { folderConfig.setRegionQualifier(myLocale.region); } String folderName = folderConfig.getFolderName(ResourceFolderType.VALUES); String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STRING); assert fileName != null; try { VirtualFile res = myFacet.getPrimaryResourceDir(); assert res != null; VirtualFile newParentFolder = res.findChild(folderName); if (newParentFolder == null) { newParentFolder = res.createChildDirectory(this, folderName); if (newParentFolder == null) { return Pair.of(String.format("Could not create folder %1$s in %2$s", folderName, res.getPath()), null); } } final VirtualFile existing = newParentFolder.findChild(fileName); String text = createTranslationXml(true); if (existing != null && existing.exists()) { return Pair.of(String.format("File 'res/%1$s/%2$s' already exists!", folderName, fileName), null); } VirtualFile newFile = newParentFolder.createChildData(this, fileName); VfsUtil.saveText(newFile, text); return Pair.of(null, newFile); } catch (IOException e2) { return Pair.of(String.format("Failed to create File 'res/%1$s/%2$s' : %3$s", folderName, fileName, e2.getMessage()), null); } } private Pair<String, VirtualFile> updateTranslations() { VirtualFile firstFile = null; PsiManager manager = PsiManager.getInstance(myFacet.getModule().getProject()); FolderConfiguration config = new FolderConfiguration(); config.setLanguageQualifier(myLocale.language); if (myLocale.hasRegion()) { config.setRegionQualifier(myLocale.region); } LocalResourceRepository resources = ProjectResourceRepository.getProjectResources(myFacet, true); Map<String, ResourceValue> existing = resources.getConfiguredResources(ResourceType.STRING, config); Set<String> handled = Sets.newHashSet(); for (String key : myKeys) { ResourceValue value = existing.get(key); if (value != null) { if (value.getValue().equals(myTranslations.get(key))) { handled.add(key); } } } for (String key : myKeys) { List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, key); if (items == null) { continue; } ResourceItem match = (ResourceItem) config.findMatchingConfigurable(items); if (match != null) { LanguageQualifier languageQualifier = match.getConfiguration().getLanguageQualifier(); if (!myLocale.language.equals(languageQualifier)) { // This configured value is not in the right language; that means this string // was not translated to this locale continue; } ResourceFile source = match.getSource(); if (source != null) { VirtualFile definedIn = LocalFileSystem.getInstance().findFileByIoFile(source.getFile()); if (definedIn == null) { continue; } if (firstFile == null) { firstFile = definedIn; } if (handled.contains(key)) { continue; } PsiFile file = manager.findFile(definedIn); if (file == null || !(file instanceof XmlFile)) { continue; } XmlFile resourceFile = (XmlFile)file; XmlTag rootTag = resourceFile.getRootTag(); if (rootTag == null) { continue; } for (XmlTag item : rootTag.getSubTags()) { XmlAttribute name = item.getAttribute(ATTR_NAME); if (name != null && key.equals(name.getValue())) { String translation = myTranslations.get(key); if (translation == null || translation.isEmpty()) { item.delete(); } else { String escaped = ValueXmlHelper.escapeResourceString(translation); XmlTagValue itemValue = item.getValue(); itemValue.setText(escaped); } firstFile = definedIn; handled.add(key); break; } } } } } String fileName = AndroidResourceUtil.getDefaultResourceFileName(ResourceType.STRING); assert fileName != null; String folderName = ResourceFolderType.VALUES.getName() + '-' + myLocale.language.getValue(); boolean format = false; List<String> folders = Collections.singletonList(folderName); for (String key : myKeys) { if (!handled.contains(key)) { String value = myTranslations.get(key); if (value != null && !value.trim().isEmpty()) { Module module = myFacet.getModule(); AndroidResourceUtil.createValueResource(module, key, ResourceType.STRING, fileName, folders, value); format = true; } } } if (format) { // This is what AndroidResourceUtil uses (see AndroidResourceUtil#findOrCreateResourceFile) final VirtualFile resDir = myFacet.getPrimaryResourceDir(); if (resDir != null) { VirtualFile file = resDir.findFileByRelativePath(folderName + '/' + fileName);// deliberately system independent path if (file != null) { firstFile = file; // Format file PsiFile psiFile = manager.findFile(file); if (psiFile != null) { CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(manager.getProject()); codeStyleManager.reformat(psiFile); } } } } // All translations were handled as edits to existing XML definitions; no need // to create new file return Pair.of(null, firstFile); } private String createTranslationXml(boolean includeRoot) { StringBuilder sb = new StringBuilder(myKeys.length * 120); if (includeRoot) { sb.append("<resources>\n\n"); } for (String key : myKeys) { String value = myTranslations.get(key); if (value == null || value.trim().isEmpty()) { continue; } sb.append(" <string name=\""); sb.append(key); sb.append("\">"); sb.append(ValueXmlHelper.escapeResourceString(value)); sb.append("</string>\n"); } if (includeRoot) { sb.append("\n</resources>"); } return sb.toString(); } } }