feat(i18n): allow selecting message XML file via file browser

- Updated the I18n settings to allow selecting the message bundle XML file directly via a file browser (TextFieldWithBrowseButton).
- Modified I18nUtils to load message files via absolute paths from project settings, improving reliability.
- Bumped plugin version to 3.2.2 and updated change notes.
This commit is contained in:
2026-04-15 19:23:10 +07:00
parent 660c7a058c
commit b13cb216db
5 changed files with 98 additions and 22 deletions

View File

@@ -5,7 +5,7 @@ plugins {
} }
group = "com.sdk.dynform.tools" group = "com.sdk.dynform.tools"
version = "3.2.0" version = "3.2.2"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -39,6 +39,15 @@ intellijPlatform {
} }
changeNotes = """ changeNotes = """
<h2>[3.2.2]</h2>
<ul>
<li><strong>UI/UX Improvement:</strong> Updated the I18n settings to allow selecting the message bundle XML file directly via a file browser, improving configuration usability.</li>
<li><strong>Core Enhancement:</strong> Modified I18nUtils to load message files via absolute paths from project settings, increasing reliability across different project structures.</li>
</ul>
<h2>[3.2.1]</h2>
<ul>
<li><strong>Bug Fix:</strong> Resolved a <code>ConcurrentModificationException</code> in the I18n cache mechanism caused by concurrent background thread access.</li>
</ul>
<h2>[3.2.0]</h2> <h2>[3.2.0]</h2>
<ul> <ul>
<li><strong>Schema Validation:</strong> Introduced project-level automatic XSD registration. Configure a target folder and namespace prefix to seamlessly map <code>.xsd</code> files to the <code>ExternalResourceManager</code>.</li> <li><strong>Schema Validation:</strong> Introduced project-level automatic XSD registration. Configure a target folder and namespace prefix to seamlessly map <code>.xsd</code> files to the <code>ExternalResourceManager</code>.</li>

View File

@@ -1,11 +1,13 @@
package com.sdk.dynform.tools.config; package com.sdk.dynform.tools.config;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextComponentAccessor; import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.vfs.VirtualFile;
import com.sdk.dynform.tools.dynform.DynFormXsdScanner; import com.sdk.dynform.tools.dynform.DynFormXsdScanner;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -20,6 +22,7 @@ public class DynFormConfigurable implements Configurable {
private JRadioButton inlayBtn; private JRadioButton inlayBtn;
private JRadioButton disabledBtn; private JRadioButton disabledBtn;
private JCheckBox showIconCheck; private JCheckBox showIconCheck;
private TextFieldWithBrowseButton i18nMessageFileField;
private TextFieldWithBrowseButton xsdFolderField; private TextFieldWithBrowseButton xsdFolderField;
private JTextField xsdPrefixField; private JTextField xsdPrefixField;
@@ -43,27 +46,58 @@ public class DynFormConfigurable implements Configurable {
gbc.insets = new Insets(5, 5, 5, 5); gbc.insets = new Insets(5, 5, 5, 5);
// --- I18n Settings Group --- // --- I18n Settings Group ---
JPanel i18nPanel = new JPanel(); JPanel i18nPanel = new JPanel(new GridBagLayout());
i18nPanel.setLayout(new BoxLayout(i18nPanel, BoxLayout.Y_AXIS));
i18nPanel.setBorder(BorderFactory.createTitledBorder("Internationalization (i18n)")); i18nPanel.setBorder(BorderFactory.createTitledBorder("Internationalization (i18n)"));
i18nPanel.add(new JLabel("Message Display Mode:")); GridBagConstraints i18nGbc = new GridBagConstraints();
i18nGbc.fill = GridBagConstraints.HORIZONTAL;
i18nGbc.insets = new Insets(2, 2, 2, 2);
i18nGbc.gridx = 0;
i18nGbc.gridy = 0;
i18nGbc.weightx = 0.0;
i18nPanel.add(new JLabel("Message Display Mode:"), i18nGbc);
i18nGbc.gridy++;
foldingBtn = new JRadioButton("Folding (Hide key, show translation)"); foldingBtn = new JRadioButton("Folding (Hide key, show translation)");
i18nPanel.add(foldingBtn, i18nGbc);
i18nGbc.gridy++;
inlayBtn = new JRadioButton("Inlay Hints (Show translation next to key)"); inlayBtn = new JRadioButton("Inlay Hints (Show translation next to key)");
i18nPanel.add(inlayBtn, i18nGbc);
i18nGbc.gridy++;
disabledBtn = new JRadioButton("Disabled"); disabledBtn = new JRadioButton("Disabled");
i18nPanel.add(disabledBtn, i18nGbc);
ButtonGroup group = new ButtonGroup(); ButtonGroup group = new ButtonGroup();
group.add(foldingBtn); group.add(foldingBtn);
group.add(inlayBtn); group.add(inlayBtn);
group.add(disabledBtn); group.add(disabledBtn);
i18nPanel.add(foldingBtn); i18nGbc.gridy++;
i18nPanel.add(inlayBtn);
i18nPanel.add(disabledBtn);
i18nPanel.add(Box.createVerticalStrut(10));
showIconCheck = new JCheckBox("Show icon in code completion"); showIconCheck = new JCheckBox("Show icon in code completion");
i18nPanel.add(showIconCheck); i18nPanel.add(showIconCheck, i18nGbc);
i18nGbc.gridy++;
i18nGbc.weightx = 0.0;
i18nPanel.add(new JLabel("Message Bundle File:"), i18nGbc);
i18nGbc.gridx = 1;
i18nGbc.weightx = 1.0;
i18nMessageFileField = new TextFieldWithBrowseButton();
FileChooserDescriptor xmlDescriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
@Override
public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
return super.isFileVisible(file, showHiddenFiles) && (file.isDirectory() || "xml".equalsIgnoreCase(file.getExtension()));
}
};
i18nMessageFileField.addBrowseFolderListener("Select Message XML File", "Select the main message bundle XML file",
project, xmlDescriptor, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
i18nPanel.add(i18nMessageFileField, i18nGbc);
mainPanel.add(i18nPanel, gbc); mainPanel.add(i18nPanel, gbc);
@@ -121,6 +155,7 @@ public class DynFormConfigurable implements Configurable {
DynFormSettings settings = DynFormSettings.getInstance(project); DynFormSettings settings = DynFormSettings.getInstance(project);
return settings.displayMode != getCurrentModeFromUI() || return settings.displayMode != getCurrentModeFromUI() ||
settings.showIcon != showIconCheck.isSelected() || settings.showIcon != showIconCheck.isSelected() ||
!settings.i18nMessageFile.equals(i18nMessageFileField.getText()) ||
!settings.xsdFolderPath.equals(xsdFolderField.getText()) || !settings.xsdFolderPath.equals(xsdFolderField.getText()) ||
!settings.xsdPrefix.equals(xsdPrefixField.getText()); !settings.xsdPrefix.equals(xsdPrefixField.getText());
} }
@@ -130,6 +165,7 @@ public class DynFormConfigurable implements Configurable {
DynFormSettings settings = DynFormSettings.getInstance(project); DynFormSettings settings = DynFormSettings.getInstance(project);
settings.displayMode = getCurrentModeFromUI(); settings.displayMode = getCurrentModeFromUI();
settings.showIcon = showIconCheck.isSelected(); settings.showIcon = showIconCheck.isSelected();
settings.i18nMessageFile = i18nMessageFileField.getText();
settings.xsdFolderPath = xsdFolderField.getText(); settings.xsdFolderPath = xsdFolderField.getText();
settings.xsdPrefix = xsdPrefixField.getText(); settings.xsdPrefix = xsdPrefixField.getText();
@@ -155,6 +191,7 @@ public class DynFormConfigurable implements Configurable {
case DISABLED: disabledBtn.setSelected(true); break; case DISABLED: disabledBtn.setSelected(true); break;
} }
showIconCheck.setSelected(settings.showIcon); showIconCheck.setSelected(settings.showIcon);
i18nMessageFileField.setText(settings.i18nMessageFile);
xsdFolderField.setText(settings.xsdFolderPath); xsdFolderField.setText(settings.xsdFolderPath);
xsdPrefixField.setText(settings.xsdPrefix); xsdPrefixField.setText(settings.xsdPrefix);
} }

View File

@@ -22,6 +22,7 @@ public class DynFormSettings implements PersistentStateComponent<DynFormSettings
public DisplayMode displayMode = DisplayMode.FOLDING; public DisplayMode displayMode = DisplayMode.FOLDING;
public boolean showIcon = true; public boolean showIcon = true;
public String i18nMessageFile = "message.xml";
public String xsdFolderPath = ""; public String xsdFolderPath = "";
public String xsdPrefix = "/dynf"; public String xsdPrefix = "/dynf";

View File

@@ -1,6 +1,7 @@
package com.sdk.dynform.tools.i18n; package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
@@ -9,31 +10,50 @@ import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTag;
import com.sdk.dynform.tools.config.DynFormSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class I18nUtils { public class I18nUtils {
private static final List<String> MESSAGE_FILENAMES = Arrays.asList("message.xml", "message_th.xml", "message_en.xml");
@NotNull @NotNull
public static List<XmlFile> findMessageFiles(@NotNull Project project) { public static List<XmlFile> findMessageFiles(@NotNull Project project) {
DynFormSettings settings = DynFormSettings.getInstance(project);
String filePath = settings.i18nMessageFile.trim();
List<XmlFile> result = new ArrayList<>(); List<XmlFile> result = new ArrayList<>();
for (String filename : MESSAGE_FILENAMES) { if (filePath.isEmpty()) {
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(filename, GlobalSearchScope.everythingScope(project)); return result;
for (VirtualFile file : files) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile instanceof XmlFile) {
result.add((XmlFile) psiFile);
}
}
} }
// Try to find the file using its absolute path
VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
// If not found by absolute path, fallback to searching by filename in the project
if (file == null) {
String filename = new java.io.File(filePath).getName();
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(filename, GlobalSearchScope.everythingScope(project));
for (VirtualFile vFile : files) {
addXmlFile(project, vFile, result);
}
} else {
addXmlFile(project, file, result);
}
return result; return result;
} }
private static final Map<String, Map<String, String>> cache = new LinkedHashMap<>(); private static void addXmlFile(Project project, VirtualFile virtualFile, List<XmlFile> result) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
if (psiFile instanceof XmlFile) {
result.add((XmlFile) psiFile);
}
}
private static final Map<String, Map<String, String>> cache = new ConcurrentHashMap<>();
private static long lastCacheUpdate = 0; private static long lastCacheUpdate = 0;
private static final long CACHE_TIMEOUT = 5000; // 5 seconds private static final long CACHE_TIMEOUT = 5000; // 5 seconds
@@ -138,7 +158,7 @@ public class I18nUtils {
return result.toString().trim(); return result.toString().trim();
} }
private static void updateCache(@NotNull Project project) { private static synchronized void updateCache(@NotNull Project project) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (currentTime - lastCacheUpdate < CACHE_TIMEOUT && !cache.isEmpty()) { if (currentTime - lastCacheUpdate < CACHE_TIMEOUT && !cache.isEmpty()) {
return; return;

View File

@@ -34,6 +34,15 @@
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<h2>[3.2.2]</h2>
<ul>
<li><strong>UI/UX Improvement:</strong> Updated the I18n settings to allow selecting the message bundle XML file directly via a file browser, improving configuration usability.</li>
<li><strong>Core Enhancement:</strong> Modified I18nUtils to load message files via absolute paths from project settings, increasing reliability across different project structures.</li>
</ul>
<h2>[3.2.1]</h2>
<ul>
<li><strong>Bug Fix:</strong> Resolved a <code>ConcurrentModificationException</code> in the I18n cache mechanism caused by concurrent background thread access.</li>
</ul>
<h2>[3.2.0]</h2> <h2>[3.2.0]</h2>
<ul> <ul>
<li><strong>Schema Validation:</strong> Introduced project-level automatic XSD registration. Configure a target folder and namespace prefix to seamlessly map <code>.xsd</code> files to the <code>ExternalResourceManager</code>.</li> <li><strong>Schema Validation:</strong> Introduced project-level automatic XSD registration. Configure a target folder and namespace prefix to seamlessly map <code>.xsd</code> files to the <code>ExternalResourceManager</code>.</li>