rework plugin configuration

This commit is contained in:
marhali 2022-04-09 12:39:40 +02:00
parent b6fc0c08ff
commit d7f34a35db
8 changed files with 639 additions and 21 deletions

View File

@ -0,0 +1,32 @@
package de.marhali.easyi18n.settings;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.FolderStrategyType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* API to access the project-specific configuration for this plugin.
* @author marhaliu
*/
public interface ProjectSettings {
// Resource Configuration
@Nullable String getLocalesDirectory();
@NotNull FolderStrategyType getFolderStrategy();
@NotNull ParserStrategyType getParserStrategy();
@NotNull String getFilePattern();
boolean isSorting();
// Editor Configuration
@Nullable String getNamespaceDelimiter();
@NotNull String getSectionDelimiter();
@Nullable String getContextDelimiter();
@Nullable String getPluralDelimiter();
@Nullable String getDefaultNamespace();
@NotNull String getPreviewLocale();
boolean isNestedKeys();
boolean isAssistance();
}

View File

@ -0,0 +1,212 @@
package de.marhali.easyi18n.settings;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.JBColor;
import com.intellij.ui.TitledSeparator;
import com.intellij.ui.components.*;
import com.intellij.ui.components.fields.ExtendableTextField;
import com.intellij.util.ui.FormBuilder;
import de.marhali.easyi18n.io.parser.ArrayMapper;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.settings.presets.Preset;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ResourceBundle;
/**
* Configuration panel with all possible options for this plugin.
* @author marhali
*/
public class ProjectSettingsComponent extends ProjectSettingsComponentState {
private final Project project;
private final ResourceBundle bundle;
private final JPanel mainPanel;
// Data fields are provided by the underlying state class
public ProjectSettingsComponent(Project project) {
this.project = project;
this.bundle = ResourceBundle.getBundle("messages");
this.mainPanel = FormBuilder.createFormBuilder()
.addComponent(new JBLabel(bundle.getString("settings.hint.text")))
.addComponent(new ActionLink(bundle.getString("settings.hint.action"),
(ActionListener) (var) -> BrowserUtil.browse("https://github.com/marhali/easy-i18n")))
.addVerticalGap(24)
.addLabeledComponent(bundle.getString("settings.preset.title"), constructPresetField(), 1, false)
.addVerticalGap(12)
.addComponent(new TitledSeparator(bundle.getString("settings.resource.title")))
.addLabeledComponent(bundle.getString("settings.resource.path.title"), constructLocalesDirectoryField(), 1, false)
.addLabeledComponent(bundle.getString("settings.resource.strategy"), constructFileStrategyPanel(), 1, false)
.addVerticalGap(12)
.addComponent(constructSortingField())
.addVerticalGap(24)
.addComponent(new TitledSeparator(bundle.getString("settings.editor.title")))
.addLabeledComponent(bundle.getString("settings.editor.key.title"), constructKeyStrategyPanel(), 1, false)
.addLabeledComponent(bundle.getString("settings.editor.default-namespace.title"), constructDefaultNamespaceField(), 1, false)
.addLabeledComponent(bundle.getString("settings.editor.preview.title"), constructPreviewLocaleField(), 1, false)
.addVerticalGap(12)
.addComponent(constructNestedKeysField())
.addComponent(constructAssistanceField())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
private JComponent constructPresetField() {
preset = new ComboBox<>(Preset.values());
preset.setToolTipText(bundle.getString("settings.preset.tooltip"));
preset.setMinimumAndPreferredWidth(196);
preset.addActionListener(e -> setState(preset.getItem().config())); // Listen to selection change
return preset;
}
private JComponent constructLocalesDirectoryField() {
localesDirectory = new TextFieldWithBrowseButton();
localesDirectory.setToolTipText(bundle.getString("settings.resource.path.tooltip"));
localesDirectory.addBrowseFolderListener(bundle.getString("settings.resource.path.window"),
bundle.getString("settings.resource.path.tooltip"), project,
new FileChooserDescriptor(false, true,
false, false, false, false));
// Listen to value change
localesDirectory.getTextField().getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
@Override
public void removeUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
@Override
public void changedUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
});
validateLocalesDirectory();
return localesDirectory;
}
private void validateLocalesDirectory() {
// Paint red border to indicate missing value
localesDirectory.setBorder(localesDirectory.getText().isEmpty()
? BorderFactory.createLineBorder(JBColor.red) : null);
}
private JPanel constructFileStrategyPanel() {
JPanel panel = new JBPanel<>(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
/* folder strategy */
folderStrategy = new ComboBox<>(bundle.getString("settings.resource.folder.items").split(ArrayMapper.SPLITERATOR_REGEX));
folderStrategy.setToolTipText(bundle.getString("settings.resource.folder.tooltip"));
folderStrategy.setMinimumAndPreferredWidth(256);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 0;
constraints.gridy = 0;
panel.add(folderStrategy, constraints);
/* parser strategy */
parserStrategy = new ComboBox<>(bundle.getString("settings.resource.parser.items").split(ArrayMapper.SPLITERATOR_REGEX));
parserStrategy.setToolTipText(bundle.getString("settings.resource.parser.tooltip"));
parserStrategy.addItemListener(handleParserChange());
parserStrategy.setMinimumAndPreferredWidth(128);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.gridy = 0;
panel.add(parserStrategy, constraints);
/* file pattern strategy */
filePattern = new JBTextField();
filePattern.setToolTipText(bundle.getString("settings.resource.file-pattern.tooltip"));
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 2;
constraints.gridy = 0;
constraints.weightx = 1;
panel.add(filePattern, constraints);
return panel;
}
private JComponent constructSortingField() {
sorting = new JBCheckBox(bundle.getString("settings.resource.sorting.title"));
sorting.setToolTipText(bundle.getString("settings.resource.sorting.tooltip"));
return sorting;
}
private JPanel constructKeyStrategyPanel() {
JPanel panel = new JBPanel<>(new FlowLayout(FlowLayout.LEFT));
panel.add(new JBLabel(bundle.getString("settings.editor.key.namespace.title")));
panel.add(namespaceDelimiter = createDelimiterField(bundle.getString("settings.editor.key.namespace.tooltip")));
panel.add(new JBLabel(bundle.getString("settings.editor.key.section.title")));
panel.add(sectionDelimiter = createDelimiterField(bundle.getString("settings.editor.key.section.tooltip")));
panel.add(new JBLabel(bundle.getString("settings.editor.key.node.title")));
panel.add(contextDelimiter = createDelimiterField(bundle.getString("settings.editor.key.context.tooltip")));
panel.add(new JBLabel(bundle.getString("settings.editor.key.context.title")));
panel.add(pluralDelimiter = createDelimiterField(bundle.getString("settings.editor.key.plural.tooltip")));
panel.add(new JBLabel(bundle.getString("settings.editor.key.plural.title")));
return panel;
}
private JTextField createDelimiterField(String tooltip) {
JBTextField field = new JBTextField();
field.setHorizontalAlignment(JTextField.CENTER);
field.setToolTipText(tooltip);
return field;
}
private JComponent constructDefaultNamespaceField() {
defaultNamespace = new ExtendableTextField(20);
defaultNamespace.setToolTipText(bundle.getString("settings.editor.default-namespace.tooltip"));
return defaultNamespace;
}
private JComponent constructPreviewLocaleField() {
previewLocale = new ExtendableTextField(12);
previewLocale.setToolTipText(bundle.getString("settings.editor.preview.tooltip"));
return previewLocale;
}
private JComponent constructNestedKeysField() {
nestedKeys = new JBCheckBox(bundle.getString("settings.editor.key.nesting.title"));
nestedKeys.setToolTipText(bundle.getString("settings.editor.key.nesting.tooltip"));
return nestedKeys;
}
private JComponent constructAssistanceField() {
assistance = new JBCheckBox(bundle.getString("settings.editor.assistance.title"));
assistance.setToolTipText(bundle.getString("settings.editor.assistance.tooltip"));
return assistance;
}
private ItemListener handleParserChange() {
return e -> {
if(e.getStateChange() == ItemEvent.SELECTED) {
// Automatically suggest file pattern option on parser change
ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex());
filePattern.setText(newStrategy.getExampleFilePattern());
}
};
}
public JPanel getMainPanel() {
return mainPanel;
}
}

View File

@ -0,0 +1,82 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.FolderStrategyType;
import de.marhali.easyi18n.settings.presets.Preset;
import javax.swing.*;
/**
* Mandatory for state management for the project settings component.
* @author marhali
*/
public class ProjectSettingsComponentState {
protected ComboBox<Preset> preset;
// Resource Configuration
protected TextFieldWithBrowseButton localesDirectory;
protected ComboBox<String> folderStrategy;
protected ComboBox<String> parserStrategy;
protected JTextField filePattern;
protected JCheckBox sorting;
// Editor configuration
protected JTextField namespaceDelimiter;
protected JTextField sectionDelimiter;
protected JTextField contextDelimiter;
protected JTextField pluralDelimiter;
protected JTextField defaultNamespace;
protected JTextField previewLocale;
protected JCheckBox nestedKeys;
protected JCheckBox assistance;
protected ProjectSettingsState getState() {
// Every field needs to provide its state
ProjectSettingsState state = new ProjectSettingsState();
state.setLocalesDirectory(localesDirectory.getText());
state.setFolderStrategy(FolderStrategyType.fromIndex(folderStrategy.getSelectedIndex()));
state.setParserStrategy(ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex()));
state.setFilePattern(filePattern.getText());
state.setSorting(sorting.isSelected());
state.setNamespaceDelimiter(namespaceDelimiter.getText());
state.setSectionDelimiter(sectionDelimiter.getText());
state.setContextDelimiter(contextDelimiter.getText());
state.setPluralDelimiter(pluralDelimiter.getText());
state.setDefaultNamespace(defaultNamespace.getText());
state.setPreviewLocale(previewLocale.getText());
state.setNestedKeys(nestedKeys.isSelected());
state.setAssistance(assistance.isSelected());
return state;
}
protected void setState(ProjectSettings state) {
// Update every field with the new state
localesDirectory.setText(state.getLocalesDirectory());
folderStrategy.setSelectedIndex(state.getFolderStrategy().toIndex());
parserStrategy.setSelectedIndex((state.getParserStrategy().toIndex()));
filePattern.setText(state.getFilePattern());
sorting.setSelected(state.isSorting());
namespaceDelimiter.setText(state.getNamespaceDelimiter());
sectionDelimiter.setText(state.getSectionDelimiter());
contextDelimiter.setText(state.getContextDelimiter());
pluralDelimiter.setText(state.getPluralDelimiter());
defaultNamespace.setText(state.getDefaultNamespace());
previewLocale.setText(state.getPreviewLocale());
nestedKeys.setSelected(state.isNestedKeys());
assistance.setSelected(state.isAssistance());
}
}

View File

@ -0,0 +1,59 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
/**
* IDE settings panel for this plugin
* @author marhali
*/
public class ProjectSettingsConfigurable implements Configurable {
private final Project project;
private ProjectSettingsComponent component;
public ProjectSettingsConfigurable(Project project) {
this.project = project;
}
@Override
public String getDisplayName() {
return "Easy I18n";
}
@Override
public @Nullable JComponent createComponent() {
component = new ProjectSettingsComponent(project);
component.setState(ProjectSettingsService.get(project).getState());
return component.getMainPanel();
}
@Override
public boolean isModified() {
ProjectSettingsState originState = ProjectSettingsService.get(project).getState();
return !originState.equals(component.getState());
}
@Override
public void apply() {
ProjectSettingsService.get(project).setState(component.getState());
InstanceManager.get(project).reload();
}
@Override
public void reset() {
component.setState(ProjectSettingsService.get(project).getState());
}
@Override
public void disposeUIResources() {
component = null;
}
}

View File

@ -0,0 +1,47 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
/**
* Persistent storage for project-specific settings.
* @author marhali
*/
@State(
name = "ProjectSettingsService",
storages = @Storage("easy-i18n.xml")
)
public class ProjectSettingsService implements PersistentStateComponent<ProjectSettingsState> {
public static @NotNull ProjectSettingsService get(@NotNull Project project) {
return project.getService(ProjectSettingsService.class);
}
private ProjectSettingsState state;
public ProjectSettingsService() {
this.state = new ProjectSettingsState();
}
/**
* Sets the provided configuration and invalidates the merged state.
* @param state New configuration
*/
protected void setState(@NotNull ProjectSettingsState state) {
this.state = state;
}
@Override
public @NotNull ProjectSettingsState getState() {
return state;
}
@Override
public void loadState(@NotNull ProjectSettingsState state) {
this.state = state;
}
}

View File

@ -0,0 +1,156 @@
package de.marhali.easyi18n.settings;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.FolderStrategyType;
import de.marhali.easyi18n.settings.presets.DefaultPreset;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents the project-specific configuration of this plugin.
* @author marhali
*/
public class ProjectSettingsState implements ProjectSettings {
private static final ProjectSettings defaults = new DefaultPreset();
// Resource Configuration
private String localesDirectory;
private FolderStrategyType folderStrategy;
private ParserStrategyType parserStrategy;
private String filePattern;
private Boolean sorting;
// Editor configuration
private String namespaceDelimiter;
private String sectionDelimiter;
private String contextDelimiter;
private String pluralDelimiter;
private String defaultNamespace;
private String previewLocale;
private Boolean nestedKeys;
private Boolean assistance;
public ProjectSettingsState() {}
@Override
public @Nullable String getLocalesDirectory() {
return localesDirectory != null ? localesDirectory : defaults.getLocalesDirectory();
}
@Override
public @NotNull FolderStrategyType getFolderStrategy() {
return folderStrategy != null ? folderStrategy : defaults.getFolderStrategy();
}
@Override
public @NotNull ParserStrategyType getParserStrategy() {
return parserStrategy != null ? parserStrategy : defaults.getParserStrategy();
}
@Override
public @NotNull String getFilePattern() {
return filePattern != null ? filePattern : defaults.getFilePattern();
}
@Override
public boolean isSorting() {
return sorting != null ? sorting : defaults.isSorting();
}
@Override
public @Nullable String getNamespaceDelimiter() {
return namespaceDelimiter != null ? namespaceDelimiter : defaults.getNamespaceDelimiter();
}
@Override
public @NotNull String getSectionDelimiter() {
return sectionDelimiter != null ? sectionDelimiter : defaults.getSectionDelimiter();
}
@Override
public @Nullable String getContextDelimiter() {
return contextDelimiter != null ? contextDelimiter : defaults.getContextDelimiter();
}
@Override
public @Nullable String getPluralDelimiter() {
return pluralDelimiter != null ? pluralDelimiter : defaults.getPluralDelimiter();
}
@Nullable
@Override
public String getDefaultNamespace() {
return defaultNamespace;
}
@Override
public @NotNull String getPreviewLocale() {
return previewLocale != null ? previewLocale : defaults.getPreviewLocale();
}
@Override
public boolean isNestedKeys() {
return nestedKeys != null ? nestedKeys : defaults.isNestedKeys();
}
@Override
public boolean isAssistance() {
return assistance != null ? assistance : defaults.isAssistance();
}
public void setLocalesDirectory(String localesDirectory) {
this.localesDirectory = localesDirectory;
}
public void setFolderStrategy(FolderStrategyType folderStrategy) {
this.folderStrategy = folderStrategy;
}
public void setParserStrategy(ParserStrategyType parserStrategy) {
this.parserStrategy = parserStrategy;
}
public void setFilePattern(String filePattern) {
this.filePattern = filePattern;
}
public void setSorting(Boolean sorting) {
this.sorting = sorting;
}
public void setNamespaceDelimiter(String namespaceDelimiter) {
this.namespaceDelimiter = namespaceDelimiter;
}
public void setSectionDelimiter(String sectionDelimiter) {
this.sectionDelimiter = sectionDelimiter;
}
public void setContextDelimiter(String contextDelimiter) {
this.contextDelimiter = contextDelimiter;
}
public void setPluralDelimiter(String pluralDelimiter) {
this.pluralDelimiter = pluralDelimiter;
}
public void setDefaultNamespace(String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
}
public void setPreviewLocale(String previewLocale) {
this.previewLocale = previewLocale;
}
public void setNestedKeys(Boolean nestedKeys) {
this.nestedKeys = nestedKeys;
}
public void setAssistance(Boolean assistance) {
this.assistance = assistance;
}
}

View File

@ -24,21 +24,26 @@
</actions>
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="Easy I18n" anchor="bottom" factoryClass="de.marhali.easyi18n.service.TranslatorToolWindowFactory" />
<toolWindow id="Easy I18n" anchor="bottom"
factoryClass="de.marhali.easyi18n.service.TranslatorToolWindowFactory"/>
<projectService serviceImplementation="de.marhali.easyi18n.service.SettingsService" />
<projectService serviceImplementation="de.marhali.easyi18n.settings.ProjectSettingsService"/>
<projectConfigurable parentId="tools" instance="de.marhali.easyi18n.settings.ProjectSettingsConfigurable"
id="de.marhali.easyi18n.service.AppSettingsConfigurable"
displayName="Easy I18n" nonDefaultProject="true"/>
<completion.contributor language="any"
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyCompletionContributor" />
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyCompletionContributor"/>
<annotator language=""
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyAnnotator" />
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyAnnotator"/>
<psi.referenceContributor
implementation="de.marhali.easyi18n.editor.generic.GenericKeyReferenceContributor" />
implementation="de.marhali.easyi18n.editor.generic.GenericKeyReferenceContributor"/>
<notificationGroup displayType="BALLOON" id="Easy I18n Notification Group" />
<errorHandler implementation="de.marhali.easyi18n.service.ErrorReportHandler" />
<notificationGroup displayType="BALLOON" id="Easy I18n Notification Group"/>
<errorHandler implementation="de.marhali.easyi18n.service.ErrorReportHandler"/>
</extensions>
</idea-plugin>

View File

@ -12,19 +12,44 @@ action.search=Search...
action.delete=Delete
translation.key=Key
translation.locales=Locales
settings.path.title=Locales Directory
settings.path.text=Locales directory
settings.strategy.title=Translation file structure
settings.strategy.folder=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale
settings.strategy.folder.tooltip=What is the folder structure of your translation files?
settings.strategy.parser=JSON;JSON5;YAML;YML;Properties;ARB
settings.strategy.parser.tooltip=Which file parser should be used to process your translation files?
settings.strategy.file-pattern.tooltip=Defines a wildcard matcher to filter relevant translation files. For example *.json, *.???.
settings.preview=Preview locale
settings.path.prefix=Path prefix
settings.keys.sort=Sort translation keys alphabetically
settings.keys.nested=Escape delimiter character within a section layer.
settings.editor.assistance=I18n key completion, annotation and reference inside editor
# Settings
settings.hint.text=Project-specific configuration for using your translation files. For an easy start, you can use one of the existing presets.
settings.hint.action=Fore more information, see the documentation
settings.preset.title=Preset
settings.preset.tooltip=Choose a configuration template that best fits your project. After that you can make further changes.
# Resource Configuration
settings.resource.title=Resource Configuration
settings.resource.path.window=Locales Directory
settings.resource.path.title=Locales directory
settings.resource.path.tooltip=Define the folder which contains all translation files. For nested folders, use the top folder.
settings.resource.strategy=File structure
settings.resource.folder.items=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale
settings.resource.folder.tooltip=What is the folder structure of your translation files?
settings.resource.parser.items=JSON;JSON5;YAML;YML;Properties;ARB
settings.resource.parser.tooltip=Which file parser should be used to process your translation files?
settings.resource.file-pattern.tooltip=Defines a wildcard matcher to filter relevant translation files. For example *.json, *.??? or *.*.
settings.resource.sorting.title=Sort translation keys alphabetically
settings.resource.sorting.tooltip=Sorts all translation keys alphabetically. If disabled, the original key-order in the files is kept.
# Editor Configuration
settings.editor.title=Editor Configuration
settings.editor.key.title=Key delimiters
settings.editor.key.namespace.title=[namespace]
settings.editor.key.namespace.tooltip=Sets the separator used between namespace and key path.
settings.editor.key.section.title=[section]
settings.editor.key.section.tooltip=Sets the separator used between section nodes of the key path.
settings.editor.key.node.title=[node]
settings.editor.key.context.title=[context]
settings.editor.key.context.tooltip=Sets the separator used to define context-specific variants of a translation.
settings.editor.key.plural.title=[pluralization]
settings.editor.key.plural.tooltip=Sets the separator used to define different pluralization's of a translation.
settings.editor.default-namespace.title=Default namespace
settings.editor.default-namespace.tooltip=Specifies the namespace to use by default if none is specified in a translation key. The field can be left blank to ignore this feature.
settings.editor.preview.title=Preview locale
settings.editor.preview.tooltip=Defines the language to be displayed in editor previews.
settings.editor.key.nesting.title=Escape section delimiter within a section layer
settings.editor.key.nesting.tooltip=Escapes the section delimiter within a section to properly reconstruct nested key sections. Disable this feature if nested key sections are the exception rather than the rule in your translation file.
settings.editor.assistance.title=Editor code assistance for translations
settings.editor.assistance.tooltip=Activates editor support to reference, auto-complete and folding of existing translations.
error.io=An error occurred while processing translation files. \n\
Config: {0} => {1} ({2}) \n\
Path: {3} \n\