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.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 @@ import org.jetbrains.annotations.Nullable;
*/
public class AddDialog extends TranslationDialog {
private Consumer<String> onCreated;
/**
* Constructs a new create dialog with prefilled fields
* @param project Opened project
@ -35,6 +39,16 @@ public class AddDialog extends TranslationDialog {
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.
@ -47,6 +61,7 @@ public class AddDialog extends TranslationDialog {
@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;

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import java.util.ResourceBundle;
/**
* Configuration panel with all possible options for this plugin.
*
* @author marhali
*/
public class ProjectSettingsComponent extends ProjectSettingsComponentState {
@ -64,10 +65,12 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
.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();
}
private JComponent constructPresetField() {
preset = new ComboBox<>(Preset.values());
preset.setToolTipText(bundle.getString("settings.preset.tooltip"));
@ -219,9 +222,15 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
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());

View File

@ -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 @@ public class ProjectSettingsComponentState {
state.setAssistance(assistance.isSelected());
state.setAlwaysFold(alwaysFold.isSelected());
state.setFlavorTemplate(flavorTemplate.getText());
return state;
}
@ -88,5 +91,7 @@ public class ProjectSettingsComponentState {
assistance.setSelected(state.isAssistance());
alwaysFold.setSelected(state.isAlwaysFold());
flavorTemplate.setText(state.getFlavorTemplate());
}
}

View File

@ -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 class ProjectSettingsState implements ProjectSettings {
this.assistance = defaults.isAssistance();
this.alwaysFold = defaults.isAlwaysFold();
this.flavorTemplate = defaults.getFlavorTemplate();
}
@Override
@ -143,6 +153,11 @@ public class ProjectSettingsState implements ProjectSettings {
return alwaysFold;
}
@Override
public String getFlavorTemplate() {
return this.flavorTemplate;
}
public void setLocalesDirectory(String localesDirectory) {
this.localesDirectory = localesDirectory;
}
@ -202,6 +217,9 @@ public class ProjectSettingsState implements ProjectSettings {
public void setAlwaysFold(Boolean alwaysFold) {
this.alwaysFold = alwaysFold;
}
public void setFlavorTemplate(String flavorTemplate){
this.flavorTemplate = flavorTemplate;
}
@Override
public boolean equals(Object o) {
@ -222,7 +240,8 @@ public class ProjectSettingsState implements ProjectSettings {
&& 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 +249,7 @@ public class ProjectSettingsState implements ProjectSettings {
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 +271,7 @@ public class ProjectSettingsState implements ProjectSettings {
", nestedKeys=" + nestedKeys +
", assistance=" + assistance +
", alwaysFold=" + alwaysFold +
", flavorTemplate=" + flavorTemplate +
'}';
}
}

View File

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

View File

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

View File

@ -85,4 +85,9 @@ public class VueI18nPreset implements ProjectSettings {
public boolean isAlwaysFold() {
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"/>
</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>
<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.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\