Merge pull request #345 from JPilson/feature/LocalizeSelectedString

feat(Localize Selected): add LocalizeItAction for localizing selected…
This commit is contained in:
Marcel 2024-04-10 00:11:22 +02:00 committed by GitHub
commit 8ddc7ff6ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 196 additions and 3 deletions

View File

@ -0,0 +1,77 @@
package de.marhali.easyi18n.action;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.DocumentUtil;
import org.jetbrains.annotations.NotNull;
/**
* Represents an action to localize text in the editor.
*/
class LocalizeItAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
DataContext dataContext = anActionEvent.getDataContext();
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
if (editor == null) return;
String text = editor.getSelectionModel().getSelectedText();
if (text == null || text.isEmpty()) return;
if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
text = text.substring(1);
text = text.substring(0, text.length() - 1);
}
Project project = anActionEvent.getProject();
if (project == null) {
throw new RuntimeException("Project is null!");
}
AddDialog dialog = new AddDialog(project, new KeyPath(text), text, (key) -> replaceSelectedText(project, editor, key));
dialog.showAndHandle();
}
/**
* Replaces the selected text in the editor with a new text generated from the provided key.
*
* @param project the project where the editor belongs
* @param editor the editor where the text is selected
* @param key the key used to generate the replacement text
*/
private void replaceSelectedText(Project project, @NotNull Editor editor, @NotNull String key) {
int selectionStart = editor.getSelectionModel().getSelectionStart();
int selectionEnd = editor.getSelectionModel().getSelectionEnd();
String flavorTemplate = ProjectSettingsService.get(project).getState().getFlavorTemplate();
DocumentUtil documentUtil = new DocumentUtil(editor.getDocument());
String replacement = buildReplacement(flavorTemplate, key, documentUtil);
WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> documentUtil.getDocument().replaceString(selectionStart, selectionEnd, replacement));
}
/**
* Builds a replacement string based on the provided flavor template, key, and document util.
*
* @param flavorTemplate the flavor template string
* @param key the key used to generate the replacement text
* @param documentUtil the document util object used to determine the document type
* @return the built replacement string
*/
private String buildReplacement(String flavorTemplate, String key, DocumentUtil documentUtil) {
if (documentUtil.isVue() || documentUtil.isJsOrTs()) return flavorTemplate + "('" + key + "')";
return flavorTemplate + "(\"" + key + "\")";
}
}

View File

@ -13,6 +13,8 @@ import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
/** /**
* Dialog to create a new translation with all associated locale values. * Dialog to create a new translation with all associated locale values.
* Supports optional prefill technique for translation key or locale value. * Supports optional prefill technique for translation key or locale value.
@ -20,6 +22,8 @@ import org.jetbrains.annotations.Nullable;
*/ */
public class AddDialog extends TranslationDialog { public class AddDialog extends TranslationDialog {
private Consumer<String> onCreated;
/** /**
* Constructs a new create dialog with prefilled fields * Constructs a new create dialog with prefilled fields
* @param project Opened project * @param project Opened project
@ -35,6 +39,16 @@ public class AddDialog extends TranslationDialog {
setTitle(bundle.getString("action.add")); setTitle(bundle.getString("action.add"));
} }
public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale,Consumer<String> onCreated) {
super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(),
prefillLocale != null
? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale)
: null)
);
this.onCreated = onCreated;
setTitle(bundle.getString("action.add"));
}
/** /**
* Constructs a new create dialog without prefilled fields. * Constructs a new create dialog without prefilled fields.
@ -47,6 +61,7 @@ public class AddDialog extends TranslationDialog {
@Override @Override
protected @Nullable TranslationUpdate handleExit(int exitCode) { protected @Nullable TranslationUpdate handleExit(int exitCode) {
if(exitCode == DialogWrapper.OK_EXIT_CODE) { if(exitCode == DialogWrapper.OK_EXIT_CODE) {
if(onCreated != null) onCreated.accept(this.getKeyField().getText());
return new TranslationCreate(getState()); return new TranslationCreate(getState());
} }
return null; return null;

View File

@ -37,6 +37,10 @@ abstract class TranslationDialog extends DialogWrapper {
protected final @NotNull KeyPathConverter converter; protected final @NotNull KeyPathConverter converter;
protected final @NotNull Translation origin; protected final @NotNull Translation origin;
public JTextField getKeyField() {
return keyField;
}
protected final JTextField keyField; protected final JTextField keyField;
protected final Map<String, JTextField> localeValueFields; protected final Map<String, JTextField> localeValueFields;

View File

@ -33,4 +33,5 @@ public interface ProjectSettings {
// Experimental Configuration // Experimental Configuration
boolean isAlwaysFold(); boolean isAlwaysFold();
String getFlavorTemplate();
} }

View File

@ -26,6 +26,7 @@ import java.util.ResourceBundle;
/** /**
* Configuration panel with all possible options for this plugin. * Configuration panel with all possible options for this plugin.
*
* @author marhali * @author marhali
*/ */
public class ProjectSettingsComponent extends ProjectSettingsComponentState { public class ProjectSettingsComponent extends ProjectSettingsComponentState {
@ -64,10 +65,12 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
.addVerticalGap(24) .addVerticalGap(24)
.addComponent(new TitledSeparator(bundle.getString("settings.experimental.title"))) .addComponent(new TitledSeparator(bundle.getString("settings.experimental.title")))
.addComponent(constructAlwaysFoldField()) .addComponent(constructAlwaysFoldField())
.addLabeledComponent(bundle.getString("settings.experimental.flavor-template"), constructFlavorTemplate(), 1, false)
.addComponentFillVertically(new JPanel(), 0) .addComponentFillVertically(new JPanel(), 0)
.getPanel(); .getPanel();
} }
private JComponent constructPresetField() { private JComponent constructPresetField() {
preset = new ComboBox<>(Preset.values()); preset = new ComboBox<>(Preset.values());
preset.setToolTipText(bundle.getString("settings.preset.tooltip")); preset.setToolTipText(bundle.getString("settings.preset.tooltip"));
@ -219,6 +222,12 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
return alwaysFold; return alwaysFold;
} }
private JComponent constructFlavorTemplate() {
flavorTemplate = new ExtendableTextField(20);
flavorTemplate.setToolTipText(bundle.getString("settings.experimental.flavor-template-tooltip"));
return flavorTemplate;
}
private ItemListener handleParserChange() { private ItemListener handleParserChange() {
return e -> { return e -> {
if (e.getStateChange() == ItemEvent.SELECTED) { if (e.getStateChange() == ItemEvent.SELECTED) {

View File

@ -40,6 +40,8 @@ public class ProjectSettingsComponentState {
// Experimental configuration // Experimental configuration
protected JCheckBox alwaysFold; protected JCheckBox alwaysFold;
protected JTextField flavorTemplate;
protected ProjectSettingsState getState() { protected ProjectSettingsState getState() {
// Every field needs to provide its state // Every field needs to provide its state
ProjectSettingsState state = new ProjectSettingsState(); ProjectSettingsState state = new ProjectSettingsState();
@ -63,6 +65,7 @@ public class ProjectSettingsComponentState {
state.setAssistance(assistance.isSelected()); state.setAssistance(assistance.isSelected());
state.setAlwaysFold(alwaysFold.isSelected()); state.setAlwaysFold(alwaysFold.isSelected());
state.setFlavorTemplate(flavorTemplate.getText());
return state; return state;
} }
@ -88,5 +91,7 @@ public class ProjectSettingsComponentState {
assistance.setSelected(state.isAssistance()); assistance.setSelected(state.isAssistance());
alwaysFold.setSelected(state.isAlwaysFold()); alwaysFold.setSelected(state.isAlwaysFold());
flavorTemplate.setText(state.getFlavorTemplate());
} }
} }

View File

@ -40,6 +40,15 @@ public class ProjectSettingsState implements ProjectSettings {
// Experimental configuration // Experimental configuration
@Property private Boolean alwaysFold; @Property private Boolean alwaysFold;
/**
* The `flavorTemplate` specifies the format used for replacing strings with their i18n (internationalization) counterparts.
* For example:
* In many situations, the default representation for i18n follows the `$i18n.t('key')` pattern. However, this can vary depending on
* the specific framework or developers' preferences for handling i18n. The ability to dynamically change this template adds flexibility and customization
* to cater to different i18n handling methods.
*/
@Property private String flavorTemplate;
public ProjectSettingsState() { public ProjectSettingsState() {
this(new DefaultPreset()); this(new DefaultPreset());
} }
@ -65,6 +74,7 @@ public class ProjectSettingsState implements ProjectSettings {
this.assistance = defaults.isAssistance(); this.assistance = defaults.isAssistance();
this.alwaysFold = defaults.isAlwaysFold(); this.alwaysFold = defaults.isAlwaysFold();
this.flavorTemplate = defaults.getFlavorTemplate();
} }
@Override @Override
@ -143,6 +153,11 @@ public class ProjectSettingsState implements ProjectSettings {
return alwaysFold; return alwaysFold;
} }
@Override
public String getFlavorTemplate() {
return this.flavorTemplate;
}
public void setLocalesDirectory(String localesDirectory) { public void setLocalesDirectory(String localesDirectory) {
this.localesDirectory = localesDirectory; this.localesDirectory = localesDirectory;
} }
@ -202,6 +217,9 @@ public class ProjectSettingsState implements ProjectSettings {
public void setAlwaysFold(Boolean alwaysFold) { public void setAlwaysFold(Boolean alwaysFold) {
this.alwaysFold = alwaysFold; this.alwaysFold = alwaysFold;
} }
public void setFlavorTemplate(String flavorTemplate){
this.flavorTemplate = flavorTemplate;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@ -222,7 +240,8 @@ public class ProjectSettingsState implements ProjectSettings {
&& Objects.equals(previewLocale, that.previewLocale) && Objects.equals(previewLocale, that.previewLocale)
&& Objects.equals(nestedKeys, that.nestedKeys) && Objects.equals(nestedKeys, that.nestedKeys)
&& Objects.equals(assistance, that.assistance) && Objects.equals(assistance, that.assistance)
&& Objects.equals(alwaysFold, that.alwaysFold); && Objects.equals(alwaysFold, that.alwaysFold)
&& Objects.equals(flavorTemplate,that.flavorTemplate);
} }
@Override @Override
@ -230,7 +249,7 @@ public class ProjectSettingsState implements ProjectSettings {
return Objects.hash( return Objects.hash(
localesDirectory, folderStrategy, parserStrategy, filePattern, includeSubDirs, localesDirectory, folderStrategy, parserStrategy, filePattern, includeSubDirs,
sorting, namespaceDelimiter, sectionDelimiter, contextDelimiter, pluralDelimiter, sorting, namespaceDelimiter, sectionDelimiter, contextDelimiter, pluralDelimiter,
defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold,flavorTemplate
); );
} }
@ -252,6 +271,7 @@ public class ProjectSettingsState implements ProjectSettings {
", nestedKeys=" + nestedKeys + ", nestedKeys=" + nestedKeys +
", assistance=" + assistance + ", assistance=" + assistance +
", alwaysFold=" + alwaysFold + ", alwaysFold=" + alwaysFold +
", flavorTemplate=" + flavorTemplate +
'}'; '}';
} }
} }

View File

@ -86,4 +86,9 @@ public class DefaultPreset implements ProjectSettings {
public boolean isAlwaysFold() { public boolean isAlwaysFold() {
return false; return false;
} }
@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
} }

View File

@ -86,4 +86,9 @@ public class ReactI18NextPreset implements ProjectSettings {
public boolean isAlwaysFold() { public boolean isAlwaysFold() {
return false; return false;
} }
@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
} }

View File

@ -85,4 +85,9 @@ public class VueI18nPreset implements ProjectSettings {
public boolean isAlwaysFold() { public boolean isAlwaysFold() {
return false; return false;
} }
@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
} }

View File

@ -0,0 +1,38 @@
package de.marhali.easyi18n.util;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.vfs.VirtualFile;
public class DocumentUtil {
protected Document document;
FileType fileType;
public Document getDocument() {
return document;
}
public void setDocument(Document document) {
this.document = document;
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
VirtualFile virtualFile = fileDocumentManager.getFile(document);
if (virtualFile != null) {
fileType = virtualFile.getFileType();
}
}
public DocumentUtil(Document document) {
setDocument(document);
}
public boolean isJsOrTs() {
return (fileType.getDefaultExtension().contains("js") || fileType.getDescription().contains("ts"));
}
public boolean isVue() {
return fileType.getDefaultExtension().contains("vue");
}
}

View File

@ -22,6 +22,13 @@
> >
<add-to-group group-id="NewGroup"/> <add-to-group group-id="NewGroup"/>
</action> </action>
<action id="de.marhali.easyi18n.action.LocalizeItAction"
class="de.marhali.easyi18n.action.LocalizeItAction"
text="Localize It"
description="Apply localization to the selected string"
icon="/icons/translate13.svg">
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
</action>
</actions> </actions>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">

View File

@ -60,6 +60,8 @@ settings.editor.assistance.tooltip=Activates editor support to reference, auto-c
settings.experimental.title=Experimental Configuration settings.experimental.title=Experimental Configuration
settings.experimental.always-fold.title=Always fold translation keys settings.experimental.always-fold.title=Always fold translation keys
settings.experimental.always-fold.tooltip=Forces the editor to always display the value behind a translation key. The value cannot be unfolded when this function is active. settings.experimental.always-fold.tooltip=Forces the editor to always display the value behind a translation key. The value cannot be unfolded when this function is active.
settings.experimental.flavor-template =I18n flavor template
settings.experimental.flavor-template-tooltip = Specify How to replace strings with i18n representation.
error.io=An error occurred while processing translation files. \n\ error.io=An error occurred while processing translation files. \n\
Config: {0} => {1} ({2}) \n\ Config: {0} => {1} ({2}) \n\
Path: {3} \n\ Path: {3} \n\