Skip to content

Commit

Permalink
Merge pull request #388 from marhali/feature/localize-it
Browse files Browse the repository at this point in the history
Feature/localize it
  • Loading branch information
marhali authored Apr 10, 2024
2 parents a1065dd + cc5dbb7 commit 4748426
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
83 changes: 83 additions & 0 deletions src/main/java/de/marhali/easyi18n/action/LocalizeItAction.java
Original file line number Diff line number Diff line change
@@ -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 + "\")";
}
}
19 changes: 19 additions & 0 deletions src/main/java/de/marhali/easyi18n/dialog/AddDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
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.
* @author marhali
*/
public class AddDialog extends TranslationDialog {

private Consumer<String> onCreated;

/**
* Constructs a new create dialog with prefilled fields
* @param project Opened project
Expand All @@ -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<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.
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ public interface ProjectSettings {

// Experimental Configuration
boolean isAlwaysFold();
String getFlavorTemplate();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -63,6 +65,7 @@ protected ProjectSettingsState getState() {
state.setAssistance(assistance.isSelected());

state.setAlwaysFold(alwaysFold.isSelected());
state.setFlavorTemplate(flavorTemplate.getText());

return state;
}
Expand All @@ -88,5 +91,6 @@ protected void setState(ProjectSettings state) {
assistance.setSelected(state.isAssistance());

alwaysFold.setSelected(state.isAlwaysFold());
flavorTemplate.setText(state.getFlavorTemplate());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand All @@ -65,6 +74,7 @@ public ProjectSettingsState(ProjectSettings defaults) {
this.assistance = defaults.isAssistance();

this.alwaysFold = defaults.isAlwaysFold();
this.flavorTemplate = defaults.getFlavorTemplate();
}

@Override
Expand Down Expand Up @@ -143,6 +153,11 @@ public boolean isAlwaysFold() {
return alwaysFold;
}

@Override
public String getFlavorTemplate() {
return this.flavorTemplate;
}

public void setLocalesDirectory(String localesDirectory) {
this.localesDirectory = localesDirectory;
}
Expand Down Expand Up @@ -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;
Expand All @@ -222,15 +241,16 @@ 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
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
);
}

Expand All @@ -252,6 +272,7 @@ public String toString() {
", nestedKeys=" + nestedKeys +
", assistance=" + assistance +
", alwaysFold=" + alwaysFold +
", flavorTemplate=" + flavorTemplate +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ public boolean isAssistance() {
public boolean isAlwaysFold() {
return false;
}

@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ public boolean isAssistance() {
public boolean isAlwaysFold() {
return false;
}

@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ public boolean isAssistance() {
public boolean isAlwaysFold() {
return false;
}

@Override
public String getFlavorTemplate() {
return "$i18n.t";
}
}
36 changes: 36 additions & 0 deletions src/main/java/de/marhali/easyi18n/util/DocumentUtil.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
7 changes: 7 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/de/marhali/easyi18n/KeyPathConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ public boolean isAlwaysFold() {
return false;
}

@Override
public String getFlavorTemplate() {
return "";
}

@Override
public boolean isIncludeSubDirs() {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ public boolean isAlwaysFold() {
return false;
}

@Override
public String getFlavorTemplate() {
return "";
}

@Override
public boolean isIncludeSubDirs() {
return false;
Expand Down
Loading

0 comments on commit 4748426

Please sign in to comment.