diff --git a/CHANGELOG.md b/CHANGELOG.md index ea35775..3722ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Added - Support for IntelliJ Platform version 2024.1 +- "Localize It" action to extract translations based on current selection. Thanks to @JPilson ### Changed diff --git a/src/main/java/de/marhali/easyi18n/action/LocalizeItAction.java b/src/main/java/de/marhali/easyi18n/action/LocalizeItAction.java new file mode 100644 index 0000000..c7d6656 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/action/LocalizeItAction.java @@ -0,0 +1,83 @@ +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 + "\")"; + } +} diff --git a/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java b/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java index b561943..2746003 100644 --- a/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java +++ b/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java @@ -13,6 +13,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.function.Consumer; + /** * Dialog to create a new translation with all associated locale values. * Supports optional prefill technique for translation key or locale value. @@ -20,6 +22,8 @@ */ public class AddDialog extends TranslationDialog { + private Consumer onCreated; + /** * Constructs a new create dialog with prefilled fields * @param project Opened project @@ -35,6 +39,16 @@ public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullab setTitle(bundle.getString("action.add")); } + public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale,Consumer 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. @@ -47,8 +61,13 @@ public AddDialog(@NotNull Project project) { @Override protected @Nullable TranslationUpdate handleExit(int exitCode) { if(exitCode == DialogWrapper.OK_EXIT_CODE) { + if(onCreated != null) { + onCreated.accept(this.getKeyField().getText()); + } + return new TranslationCreate(getState()); } + return null; } } diff --git a/src/main/java/de/marhali/easyi18n/dialog/TranslationDialog.java b/src/main/java/de/marhali/easyi18n/dialog/TranslationDialog.java index 51307cd..db4a088 100644 --- a/src/main/java/de/marhali/easyi18n/dialog/TranslationDialog.java +++ b/src/main/java/de/marhali/easyi18n/dialog/TranslationDialog.java @@ -68,6 +68,10 @@ protected TranslationDialog(@NotNull Project project, @NotNull Translation origi } } + public JTextField getKeyField() { + return keyField; + } + /** * Registers a callback that is called on dialog close with the final state. * If the user aborts the dialog no callback is called. diff --git a/src/main/java/de/marhali/easyi18n/settings/ProjectSettings.java b/src/main/java/de/marhali/easyi18n/settings/ProjectSettings.java index 5da91a5..019408a 100644 --- a/src/main/java/de/marhali/easyi18n/settings/ProjectSettings.java +++ b/src/main/java/de/marhali/easyi18n/settings/ProjectSettings.java @@ -33,4 +33,5 @@ public interface ProjectSettings { // Experimental Configuration boolean isAlwaysFold(); + String getFlavorTemplate(); } diff --git a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponent.java b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponent.java index 9018437..2ed36e8 100644 --- a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponent.java +++ b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponent.java @@ -64,6 +64,7 @@ public ProjectSettingsComponent(Project project) { .addVerticalGap(24) .addComponent(new TitledSeparator(bundle.getString("settings.experimental.title"))) .addComponent(constructAlwaysFoldField()) + .addLabeledComponent(bundle.getString("settings.experimental.flavor-template"), constructFlavorTemplate(), 1, false) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } @@ -219,9 +220,15 @@ private JComponent constructAlwaysFoldField() { return alwaysFold; } + private JComponent constructFlavorTemplate() { + flavorTemplate = new ExtendableTextField(20); + flavorTemplate.setToolTipText(bundle.getString("settings.experimental.flavor-template-tooltip")); + return flavorTemplate; + } + private ItemListener handleParserChange() { return e -> { - if(e.getStateChange() == ItemEvent.SELECTED) { + if (e.getStateChange() == ItemEvent.SELECTED) { // Automatically suggest file pattern option on parser change ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex()); filePattern.setText(newStrategy.getExampleFilePattern()); diff --git a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponentState.java b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponentState.java index 863dd44..0138a5c 100644 --- a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponentState.java +++ b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsComponentState.java @@ -40,6 +40,8 @@ public class ProjectSettingsComponentState { // Experimental configuration protected JCheckBox alwaysFold; + protected JTextField flavorTemplate; + protected ProjectSettingsState getState() { // Every field needs to provide its state ProjectSettingsState state = new ProjectSettingsState(); @@ -63,6 +65,7 @@ protected ProjectSettingsState getState() { state.setAssistance(assistance.isSelected()); state.setAlwaysFold(alwaysFold.isSelected()); + state.setFlavorTemplate(flavorTemplate.getText()); return state; } @@ -88,5 +91,6 @@ protected void setState(ProjectSettings state) { assistance.setSelected(state.isAssistance()); alwaysFold.setSelected(state.isAlwaysFold()); + flavorTemplate.setText(state.getFlavorTemplate()); } } diff --git a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsState.java b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsState.java index a8648a2..4b9c064 100644 --- a/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsState.java +++ b/src/main/java/de/marhali/easyi18n/settings/ProjectSettingsState.java @@ -40,6 +40,15 @@ public class ProjectSettingsState implements ProjectSettings { // Experimental configuration @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() { this(new DefaultPreset()); } @@ -65,6 +74,7 @@ public ProjectSettingsState(ProjectSettings defaults) { this.assistance = defaults.isAssistance(); this.alwaysFold = defaults.isAlwaysFold(); + this.flavorTemplate = defaults.getFlavorTemplate(); } @Override @@ -143,6 +153,11 @@ public boolean isAlwaysFold() { return alwaysFold; } + @Override + public String getFlavorTemplate() { + return this.flavorTemplate; + } + public void setLocalesDirectory(String localesDirectory) { this.localesDirectory = localesDirectory; } @@ -203,6 +218,10 @@ public void setAlwaysFold(Boolean alwaysFold) { this.alwaysFold = alwaysFold; } + public void setFlavorTemplate(String flavorTemplate){ + this.flavorTemplate = flavorTemplate; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -222,7 +241,8 @@ public boolean equals(Object o) { && Objects.equals(previewLocale, that.previewLocale) && Objects.equals(nestedKeys, that.nestedKeys) && Objects.equals(assistance, that.assistance) - && Objects.equals(alwaysFold, that.alwaysFold); + && Objects.equals(alwaysFold, that.alwaysFold) + && Objects.equals(flavorTemplate,that.flavorTemplate); } @Override @@ -230,7 +250,7 @@ public int hashCode() { return Objects.hash( localesDirectory, folderStrategy, parserStrategy, filePattern, includeSubDirs, sorting, namespaceDelimiter, sectionDelimiter, contextDelimiter, pluralDelimiter, - defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold + defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold,flavorTemplate ); } @@ -252,6 +272,7 @@ public String toString() { ", nestedKeys=" + nestedKeys + ", assistance=" + assistance + ", alwaysFold=" + alwaysFold + + ", flavorTemplate=" + flavorTemplate + '}'; } } diff --git a/src/main/java/de/marhali/easyi18n/settings/presets/DefaultPreset.java b/src/main/java/de/marhali/easyi18n/settings/presets/DefaultPreset.java index 0495db1..383a5d3 100644 --- a/src/main/java/de/marhali/easyi18n/settings/presets/DefaultPreset.java +++ b/src/main/java/de/marhali/easyi18n/settings/presets/DefaultPreset.java @@ -86,4 +86,9 @@ public boolean isAssistance() { public boolean isAlwaysFold() { return false; } + + @Override + public String getFlavorTemplate() { + return "$i18n.t"; + } } diff --git a/src/main/java/de/marhali/easyi18n/settings/presets/ReactI18NextPreset.java b/src/main/java/de/marhali/easyi18n/settings/presets/ReactI18NextPreset.java index ef536d3..26c16b7 100644 --- a/src/main/java/de/marhali/easyi18n/settings/presets/ReactI18NextPreset.java +++ b/src/main/java/de/marhali/easyi18n/settings/presets/ReactI18NextPreset.java @@ -86,4 +86,9 @@ public boolean isAssistance() { public boolean isAlwaysFold() { return false; } + + @Override + public String getFlavorTemplate() { + return "$i18n.t"; + } } diff --git a/src/main/java/de/marhali/easyi18n/settings/presets/VueI18nPreset.java b/src/main/java/de/marhali/easyi18n/settings/presets/VueI18nPreset.java index 55ddfbe..491edf8 100644 --- a/src/main/java/de/marhali/easyi18n/settings/presets/VueI18nPreset.java +++ b/src/main/java/de/marhali/easyi18n/settings/presets/VueI18nPreset.java @@ -85,4 +85,9 @@ public boolean isAssistance() { public boolean isAlwaysFold() { return false; } + + @Override + public String getFlavorTemplate() { + return "$i18n.t"; + } } diff --git a/src/main/java/de/marhali/easyi18n/util/DocumentUtil.java b/src/main/java/de/marhali/easyi18n/util/DocumentUtil.java new file mode 100644 index 0000000..bc13dac --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/util/DocumentUtil.java @@ -0,0 +1,36 @@ +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; + private FileType fileType; + + public DocumentUtil(Document document) { + setDocument(document); + } + + 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 boolean isJsOrTs() { + return (fileType.getDefaultExtension().contains("js") || fileType.getDescription().contains("ts")); + } + + public boolean isVue() { + return fileType.getDefaultExtension().contains("vue"); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 45f9611..1553229 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -22,6 +22,13 @@ > + + + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 69ac416..463b1bb 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -60,6 +60,8 @@ settings.editor.assistance.tooltip=Activates editor support to reference, auto-c settings.experimental.title=Experimental Configuration 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.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\ Config: {0} => {1} ({2}) \n\ Path: {3} \n\ diff --git a/src/test/java/de/marhali/easyi18n/KeyPathConverterTest.java b/src/test/java/de/marhali/easyi18n/KeyPathConverterTest.java index 514e41c..b84d2cb 100644 --- a/src/test/java/de/marhali/easyi18n/KeyPathConverterTest.java +++ b/src/test/java/de/marhali/easyi18n/KeyPathConverterTest.java @@ -163,6 +163,11 @@ public boolean isAlwaysFold() { return false; } + @Override + public String getFlavorTemplate() { + return ""; + } + @Override public boolean isIncludeSubDirs() { return false; diff --git a/src/test/java/de/marhali/easyi18n/mapper/PropertiesMapperTest.java b/src/test/java/de/marhali/easyi18n/mapper/PropertiesMapperTest.java index e7bba6a..71bf2e1 100644 --- a/src/test/java/de/marhali/easyi18n/mapper/PropertiesMapperTest.java +++ b/src/test/java/de/marhali/easyi18n/mapper/PropertiesMapperTest.java @@ -236,6 +236,11 @@ public boolean isAlwaysFold() { return false; } + @Override + public String getFlavorTemplate() { + return ""; + } + @Override public boolean isIncludeSubDirs() { return false; diff --git a/src/test/java/de/marhali/easyi18n/settings/SettingsTestPreset.java b/src/test/java/de/marhali/easyi18n/settings/SettingsTestPreset.java index 2415fe3..ab646ec 100644 --- a/src/test/java/de/marhali/easyi18n/settings/SettingsTestPreset.java +++ b/src/test/java/de/marhali/easyi18n/settings/SettingsTestPreset.java @@ -85,4 +85,9 @@ public boolean isAssistance() { public boolean isAlwaysFold() { return false; } + + @Override + public String getFlavorTemplate() { + return ""; + } }