indicate translations with duplicated values yellow

This commit is contained in:
marhali 2022-05-28 21:46:22 +02:00
parent d4ef478d48
commit 21e71f53d5
10 changed files with 181 additions and 115 deletions

View File

@ -5,6 +5,7 @@
## [Unreleased]
### Added
- Duplicate translation values filter
- Indicate translations with duplicated values yellow
- Multiple translation filters can be used together
### Changed

View File

@ -98,7 +98,7 @@ _For more examples, please refer to the [Examples Directory](https://github.com/
- [X] Define default namespace to use if none was provided
- [X] Enhance editor code assistance
- [ ] XML Support
- [ ] Mark duplicate translation values
- [X] Mark duplicate translation values
- [ ] Python language assistance
See the [open issues](https://github.com/marhali/easy-i18n/issues) for a full list of proposed features (and known issues).

View File

@ -2,21 +2,17 @@ package de.marhali.easyi18n;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.model.bus.ExpandAllListener;
import de.marhali.easyi18n.model.bus.FilteredBusListener;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.settings.ProjectSettingsState;
import de.marhali.easyi18n.util.KeyPathConverter;
import de.marhali.easyi18n.util.TranslationUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
@ -109,21 +105,21 @@ public class FilteredDataBus implements BusListener {
// filter incomplete translations
if(filterIncomplete) {
if(!isIncomplete(value)) {
if(!TranslationUtil.isIncomplete(value, this.data)) {
shadow.setTranslation(key, null);
}
}
// filter duplicate values
if(filterDuplicate) {
if(!isDuplicate(key, value)) {
if(!TranslationUtil.hasDuplicates(new Translation(key, value), this.data)) {
shadow.setTranslation(key, null);
}
}
// full-text-search
if(searchQuery != null) {
if(!isSearched(key, value)) {
if(!TranslationUtil.isSearched(settings, new Translation(key, value), searchQuery)) {
shadow.setTranslation(key, null);
}
}
@ -144,54 +140,4 @@ public class FilteredDataBus implements BusListener {
private void fire(@NotNull Consumer<FilteredBusListener> li) {
listener.forEach(li);
}
/**
* Filter translations with missing translation values for any locale
*/
private boolean isIncomplete(@NotNull TranslationValue value) {
return this.data.getLocales().size() != value.getLocaleContents().size();
}
/**
* Filter duplicate translation values
*/
private boolean isDuplicate(@NotNull KeyPath key, @NotNull TranslationValue value) {
Collection<String> contents = value.getLocaleContents();
for (KeyPath currentKey : this.data.getFullKeys()) {
TranslationValue currentValue = this.data.getTranslation(currentKey);
assert currentValue != null;
if(currentKey.equals(key)) { // Only consider other translations
continue;
}
for (String currentContent : currentValue.getLocaleContents()) {
if(contents.contains(currentContent)) {
return true;
}
}
}
return false;
}
/**
* Filter by search query
*/
private boolean isSearched(@NotNull KeyPath key, @NotNull TranslationValue value) {
String concatKey = new KeyPathConverter(settings).toString(key).toLowerCase();
if(searchQuery.contains(concatKey) || concatKey.contains(searchQuery)) {
return true;
}
for (String localeContent : value.getLocaleContents()) {
if(localeContent.toLowerCase().contains(searchQuery)) {
return true;
}
}
return false;
}
}

View File

@ -1,41 +0,0 @@
package de.marhali.easyi18n.renderer;
import com.intellij.ui.JBColor;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
/**
* Similar to {@link DefaultTableCellRenderer} but will mark the first column red if any column is empty.
* @author marhali
*/
public class TableRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if(column == 0 && missesValues(row, table)) {
component.setForeground(JBColor.RED);
} else { // Reset color
component.setForeground(null);
}
return component;
}
private boolean missesValues(int row, JTable table) {
int columns = table.getColumnCount();
for(int i = 1; i < columns; i++) {
Object value = table.getValueAt(row, i);
if(value == null || value.toString().isEmpty()) {
return true;
}
}
return false;
}
}

View File

@ -15,7 +15,7 @@ import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.model.bus.FilteredBusListener;
import de.marhali.easyi18n.renderer.TableRenderer;
import de.marhali.easyi18n.tabs.renderer.TableRenderer;
import de.marhali.easyi18n.tabs.mapper.TableModelMapper;
import de.marhali.easyi18n.util.KeyPathConverter;

View File

@ -21,7 +21,7 @@ import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.model.bus.FilteredBusListener;
import de.marhali.easyi18n.renderer.TreeRenderer;
import de.marhali.easyi18n.tabs.renderer.TreeRenderer;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.tabs.mapper.TreeModelMapper;
import de.marhali.easyi18n.util.TreeUtil;

View File

@ -3,10 +3,12 @@ package de.marhali.easyi18n.tabs.mapper;
import com.intellij.ide.projectView.PresentationData;
import com.intellij.ui.JBColor;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.util.TranslationUtil;
import de.marhali.easyi18n.util.UiUtil;
import org.jetbrains.annotations.NotNull;
@ -33,31 +35,33 @@ public class TreeModelMapper extends DefaultTreeModel {
this.state = state;
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
this.generateNodes(rootNode, this.data.getRootNode());
this.generateNodes(rootNode, new KeyPath(), this.data.getRootNode());
super.setRoot(rootNode);
}
/**
* @param parent Parent tree node
* @param translationNode Layer of translation node to write to tree
* @return true if children nodes misses any translation values
* @return color to apply on the parent node
*/
private boolean generateNodes(@NotNull DefaultMutableTreeNode parent, @NotNull TranslationNode translationNode) {
boolean foundMissing = false;
private JBColor generateNodes(@NotNull DefaultMutableTreeNode parent, @NotNull KeyPath parentPath, @NotNull TranslationNode translationNode) {
JBColor color = null;
for(Map.Entry<String, TranslationNode> entry : translationNode.getChildren().entrySet()) {
String key = entry.getKey();
KeyPath keyPath = new KeyPath(parentPath, key);
TranslationNode childTranslationNode = entry.getValue();
if(!childTranslationNode.isLeaf()) {
// Nested node - run recursively
if(!childTranslationNode.isLeaf()) { // Nested node - run recursively
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(key);
if(this.generateNodes(childNode, childTranslationNode)) { // Mark red if any children misses translations
JBColor childColor = this.generateNodes(childNode, keyPath, childTranslationNode);
if(childColor != null) {
PresentationData data = new PresentationData(key, null, null, null);
data.setForcedTextForeground(JBColor.RED);
data.setForcedTextForeground(childColor);
childNode.setUserObject(data);
foundMissing = true;
color = childColor;
}
parent.add(childNode);
@ -72,14 +76,17 @@ public class TreeModelMapper extends DefaultTreeModel {
if(childTranslationNode.getValue().size() != this.data.getLocales().size()) {
data.setForcedTextForeground(JBColor.RED);
foundMissing = true;
color = JBColor.RED;
} else if(TranslationUtil.hasDuplicates(new Translation(keyPath, childTranslationNode.getValue()), this.data)) {
data.setForcedTextForeground(JBColor.YELLOW);
color = JBColor.YELLOW;
}
parent.add(new DefaultMutableTreeNode(data));
}
}
return foundMissing;
return color;
}
/**

View File

@ -0,0 +1,74 @@
package de.marhali.easyi18n.tabs.renderer;
import com.intellij.ui.JBColor;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.util.HashSet;
import java.util.Set;
/**
* Similar to {@link DefaultTableCellRenderer} but will mark the first column red if any column is empty.
* @author marhali
*/
public class TableRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
// Always reset color
component.setForeground(null);
if(column != 0) {
return component;
}
if(missesValues(row, table)) {
component.setForeground(JBColor.RED);
} else if(hasDuplicates(row, table)) {
component.setForeground(JBColor.YELLOW);
}
return component;
}
private boolean missesValues(int row, JTable table) {
int columns = table.getColumnCount();
for(int i = 1; i < columns; i++) {
Object value = table.getValueAt(row, i);
if(value == null || value.toString().isEmpty()) {
return true;
}
}
return false;
}
private boolean hasDuplicates(int checkRow, JTable table) {
int columns = table.getColumnCount();
int rows = table.getRowCount();
Set<String> contents = new HashSet<>();
for(int column = 1; column < columns; column++) {
contents.add(String.valueOf(table.getValueAt(checkRow, column)));
}
for(int row = 1; row < rows; row++) {
if(row == checkRow) {
continue;
}
for(int column = 1; column < columns; column++) {
if(contents.contains(String.valueOf(table.getValueAt(row, column)))) {
return true;
}
}
}
return false;
}
}

View File

@ -1,4 +1,4 @@
package de.marhali.easyi18n.renderer;
package de.marhali.easyi18n.tabs.renderer;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.navigation.ItemPresentation;

View File

@ -0,0 +1,79 @@
package de.marhali.easyi18n.util;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettingsState;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Objects;
/**
* Utilities for translations
* @author marhali
*/
public class TranslationUtil {
/**
* Check whether a given translation has duplicated values.
* @param translation The translation to check
* @param data Translation data cache
* @return true if duplicates were found otherwise false
*/
public static boolean hasDuplicates(@NotNull Translation translation, @NotNull TranslationData data) {
assert translation.getValue() != null;
Collection<String> contents = translation.getValue().getLocaleContents();
for (KeyPath key : data.getFullKeys()) {
if(translation.getKey().equals(key)) { // Only consider other translations
continue;
}
for (String localeContent : Objects.requireNonNull(data.getTranslation(key)).getLocaleContents()) {
if(contents.contains(localeContent)) {
return true;
}
}
}
return false;
}
/**
* Check whether a given translation has missing locale values.
* @param value The translation to check
* @param data Translation data cache
* @return true if missing values were found otherwise false
*/
public static boolean isIncomplete(@NotNull TranslationValue value, @NotNull TranslationData data) {
return value.getLocaleContents().size() != data.getLocales().size();
}
/**
* Check whether a given translation falls under a specified search query.
* @param settings Project specific settings
* @param translation The translation to check
* @param searchQuery Full-text search term
* @return true if translations is applicable otherwise false
*/
public static boolean isSearched(@NotNull ProjectSettingsState settings,
@NotNull Translation translation, @NotNull String searchQuery) {
String concatKey = new KeyPathConverter(settings).toString(translation.getKey()).toLowerCase();
if(searchQuery.contains(concatKey) || concatKey.contains(searchQuery)) {
return true;
}
assert translation.getValue() != null;
for (String localeContent : translation.getValue().getLocaleContents()) {
if(localeContent.toLowerCase().contains(searchQuery)) {
return true;
}
}
return false;
}
}