Added YAML support and prefix support for completions

This commit is contained in:
sunarya-thito 2021-09-14 11:38:29 +07:00
parent 459dfd38ef
commit 7b8d50f5fa
9 changed files with 688 additions and 18 deletions

View File

@ -27,6 +27,7 @@ public class SettingsDialog {
private TextFieldWithBrowseButton pathText; private TextFieldWithBrowseButton pathText;
private JBTextField filePatternText; private JBTextField filePatternText;
private JBTextField previewText; private JBTextField previewText;
private JBTextField prefixText;
private JBCheckBox codeAssistanceCheckbox; private JBCheckBox codeAssistanceCheckbox;
public SettingsDialog(Project project) { public SettingsDialog(Project project) {
@ -37,20 +38,22 @@ public class SettingsDialog {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath(); String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
String filePattern = SettingsService.getInstance(project).getState().getFilePattern(); String filePattern = SettingsService.getInstance(project).getState().getFilePattern();
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String prefixLocale = SettingsService.getInstance(project).getState().getPrefix();
boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance(); boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance();
if(prepare(localesPath, filePattern, previewLocale, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes if(prepare(localesPath, filePattern, previewLocale, prefixLocale, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText()); SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText());
SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText()); SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText());
SettingsService.getInstance(project).getState().setPreviewLocale(previewText.getText()); SettingsService.getInstance(project).getState().setPreviewLocale(previewText.getText());
SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected()); SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected());
SettingsService.getInstance(project).getState().setPrefix(prefixText.getText());
// Reload instance // Reload instance
DataStore.getInstance(project).reloadFromDisk(); DataStore.getInstance(project).reloadFromDisk();
} }
} }
private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, boolean codeAssistance) { private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, String prefixLocale, boolean codeAssistance) {
JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2)); JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text")); JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text"));
@ -80,6 +83,11 @@ public class SettingsDialog {
codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance")); codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance"));
codeAssistanceCheckbox.setSelected(codeAssistance); codeAssistanceCheckbox.setSelected(codeAssistance);
JBLabel prefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.prefix"));
prefixText = new JBTextField(prefixLocale);
rootPanel.add(prefixLabel);
rootPanel.add(prefixText);
rootPanel.add(codeAssistanceCheckbox); rootPanel.add(codeAssistanceCheckbox);
DialogBuilder builder = new DialogBuilder(); DialogBuilder builder = new DialogBuilder();

View File

@ -34,16 +34,18 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
} }
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String prefix = SettingsService.getInstance(project).getState().getPrefix();
String query = result.getPrefixMatcher().getPrefix(); String query = result.getPrefixMatcher().getPrefix();
List<String> sections = TranslationsUtil.getSections(query); List<String> sections = TranslationsUtil.getSections(query);
String lastSection = null;
if(!sections.isEmpty() && !query.endsWith(".")) {
lastSection = sections.remove(sections.size() - 1);
}
String path = TranslationsUtil.sectionsToFullPath(sections); String path = TranslationsUtil.sectionsToFullPath(sections);
if (prefix != null && path.startsWith(prefix)) {
path = path.substring(prefix.length());
if (path.startsWith(".")) {
path = path.substring(1);
}
}
LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes() LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes()
: DataStore.getInstance(project).getTranslations().getNode(path); : DataStore.getInstance(project).getTranslations().getNode(path);
@ -52,13 +54,32 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
return; return;
} }
for(LocalizedNode children : node.getChildren()) { append(node, previewLocale, path, prefix, result);
if(lastSection == null || children.getKey().startsWith(lastSection)) { // for(LocalizedNode children : node.getChildren()) {
// Construct full key path / Fore nested objects add '.' to indicate deeper level // if(lastSection == null || children.getKey().startsWith(lastSection)) {
String fullKey = (path.isEmpty() ? children.getKey() : path + "." + children.getKey()) + (children.isLeaf() ? "" : "."); // // Construct full key path / Fore nested objects add '.' to indicate deeper level
// String fullKey = (path.isEmpty() ? children.getKey() : path + "." + children.getKey()) + (children.isLeaf() ? "" : ".");
//
// if (prefix != null && !fullKey.isEmpty()) {
// fullKey = prefix + "." + fullKey;
// }
//
// result.addElement(LookupElementBuilder.create(fullKey)
// .appendTailText(getTailText(children, previewLocale), true));
// }
// }
}
result.addElement(LookupElementBuilder.create(fullKey) private void append(LocalizedNode node, String locale, String path, String prefix, CompletionResultSet result) {
.appendTailText(getTailText(children, previewLocale), true)); if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
if (prefix != null) {
path = prefix + "." + path;
}
result.addElement(LookupElementBuilder.create(path)
.appendTailText(getTailText(node, locale), true));
} else {
for (LocalizedNode children : node.getChildren()) {
append(children, locale, path == null || path.isEmpty() ? children.getKey() : path + "." + children.getKey(), prefix, result);
} }
} }
} }

View File

@ -0,0 +1,117 @@
package de.marhali.easyi18n.io.implementation;
import com.google.gson.*;
import com.intellij.openapi.application.*;
import com.intellij.openapi.project.*;
import com.intellij.openapi.vfs.*;
import de.marhali.easyi18n.io.*;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.util.*;
import org.jetbrains.annotations.*;
import thito.nodeflow.config.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
public class YamlTranslatorIO implements TranslatorIO {
@Override
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
Section section = Section.parseToMap(reader);
load(file.getNameWithoutExtension(), nodes, section);
}
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
private void load(String locale, LocalizedNode node, Section section) {
if (section instanceof MapSection) {
for (String key : section.getKeys()) {
LocalizedNode child = node.getChildren(key);
if (child == null) {
node.addChildren(child = new LocalizedNode(key, new ArrayList<>()));
}
LocalizedNode finalChild = child;
MapSection map = section.getMap(key).orElse(null);
if (map != null) {
load(locale, finalChild, map);
} else {
String value = section.getString(key).orElse(null);
if (value != null) {
child.getValue().put(locale, value);
}
}
}
}
}
private void save(LocalizedNode node, String locale, Section section, String path) {
if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
String value = node.getValue().get(locale);
if (value != null) {
section.set(path, value);
}
} else {
for (LocalizedNode child : node.getChildren()) {
save(child, locale, section, path == null ? child.getKey() : path + "." + child.getKey());
}
}
}
@Override
public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
Section section = new MapSection();
save(translations.getNodes(), locale, section, null);
String fullPath = directoryPath + "/" + locale + ".yml";
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
file.setBinaryContent(Section.toString(section).getBytes(file.getCharset()));
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
}

View File

@ -16,6 +16,7 @@ public class SettingsState {
private String localesPath; private String localesPath;
private String filePattern; private String filePattern;
private String previewLocale; private String previewLocale;
private String prefix;
private Boolean codeAssistance; private Boolean codeAssistance;
public SettingsState() {} public SettingsState() {}
@ -51,4 +52,12 @@ public class SettingsState {
public void setCodeAssistance(boolean codeAssistance) { public void setCodeAssistance(boolean codeAssistance) {
this.codeAssistance = codeAssistance; this.codeAssistance = codeAssistance;
} }
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
} }

View File

@ -3,9 +3,7 @@ package de.marhali.easyi18n.util;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.implementation.JsonTranslatorIO; import de.marhali.easyi18n.io.implementation.*;
import de.marhali.easyi18n.io.implementation.ModularizedJsonTranslatorIO;
import de.marhali.easyi18n.io.implementation.PropertiesTranslatorIO;
import de.marhali.easyi18n.io.TranslatorIO; import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.service.SettingsService;
@ -50,7 +48,8 @@ public class IOUtil {
case "properties": case "properties":
return new PropertiesTranslatorIO(); return new PropertiesTranslatorIO();
case "yml":
return new YamlTranslatorIO();
default: default:
throw new UnsupportedOperationException("Unsupported i18n locale file format: " + throw new UnsupportedOperationException("Unsupported i18n locale file format: " +
any.get().getFileType().getDefaultExtension()); any.get().getFileType().getDefaultExtension());

View File

@ -0,0 +1,227 @@
package thito.nodeflow.config;
import java.util.*;
import java.util.stream.*;
public class ListSection extends ArrayList<Object> implements Section {
private Section parent;
private String name;
public ListSection() {
super();
}
public ListSection(Collection<?> c) {
super();
addAll(c);
}
protected void setParent(Section parent, String name) {
this.parent = parent;
this.name = name;
}
@Override
public String getName() {
if (name == null && parent != null) {
if (parent instanceof ListSection) {
return String.valueOf(((ListSection) parent).indexOf(this));
}
if (parent instanceof MapSection) {
return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this))
.findAny().map(Map.Entry::getKey).orElse(null);
}
}
return name;
}
public Optional<Object> getObject(int index) {
return index >= 0 && index < size() ? Optional.ofNullable(get(index)) : Optional.empty();
}
@Override
public Section getParent() {
return parent;
}
@Override
public Set<String> getKeys() {
return IntStream.range(0, size()).mapToObj(String::valueOf).collect(Collectors.toSet());
}
@Override
public Optional<?> getInScope(String key) {
try {
return Optional.ofNullable(get(Integer.parseInt(key)));
} catch (Throwable t) {
}
return Optional.empty();
}
@Override
public void setInScope(String key, Object value) {
try {
set(Integer.parseInt(key), value);
} catch (Throwable t) {
}
}
@Override
public Object set(int index, Object element) {
element = Section.wrap(element);
if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element);
return super.set(index, element);
}
@Override
public boolean add(Object o) {
o = Section.wrap(o);
if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o);
return super.add(o);
}
@Override
public void add(int index, Object element) {
element = Section.wrap(element);
if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element);
super.add(index, element);
}
@Override
public boolean addAll(Collection<?> c) {
c.forEach(o -> add(o));
return !c.isEmpty();
}
@Override
public boolean addAll(int index, Collection<?> c) {
List<Object> wrapped = new ArrayList<>();
c.forEach(obj -> {
Object o = Section.wrap(obj);
if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o);
wrapped.add(o);
});
return super.addAll(index, wrapped);
}
@Override
public String toString() {
return Section.toString(this);
}
public <T extends Enum<T>> Optional<T> getEnum(int index, Class<T> clz) {
return getObject(index).map(o -> {
try {
return Enum.valueOf(clz, String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<String> getString(int index) {
return getObject(index).map(String::valueOf);
}
public Optional<Integer> getInteger(int index) {
return getObject(index).map(o -> {
try {
return Integer.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Double> getDouble(int index) {
return getObject(index).map(o -> {
try {
return Double.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Long> getLong(int index) {
return getObject(index).map(o -> {
try {
return Long.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Float> getFloat(int index) {
return getObject(index).map(o -> {
try {
return Float.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Short> getShort(int index) {
return getObject(index).map(o -> {
try {
return Short.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Byte> getByte(int index) {
return getObject(index).map(o -> {
try {
return Byte.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
public Optional<Character> getCharacter(int index) {
return getObject(index).map(o -> {
String text = String.valueOf(o);
return text.isEmpty() ? null : text.charAt(0);
});
}
public Optional<Boolean> getBoolean(int index) {
return getObject(index).map(o -> {
String text = String.valueOf(o);
return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null;
});
}
public Optional<MapSection> getMap(int index) {
return getObject(index).map(o -> {
if (o instanceof Map) {
if (o instanceof MapSection) return (MapSection) o;
MapSection mapSection = new MapSection((Map<?, ?>) o);
mapSection.setParent(this, null);
return mapSection;
}
return null;
});
}
public Optional<ListSection> getList(int index) {
return getObject(index).map(o -> {
if (o instanceof List) {
if (o instanceof ListSection) {
return (ListSection) o;
}
ListSection list = new ListSection((List<?>) o);
list.setParent(this, null);
return list;
}
ListSection list = new ListSection(Collections.singleton(o));
list.setParent(this, null);
return list;
});
}
}

View File

@ -0,0 +1,73 @@
package thito.nodeflow.config;
import java.util.*;
public class MapSection extends HashMap<String, Object> implements Section {
private Section parent;
private String name;
public MapSection() {
super();
}
public MapSection(Map<?, ?> m) {
super();
m.forEach((key, value) -> put(String.valueOf(key), value));
}
protected void setParent(Section parent, String name) {
this.parent = parent;
this.name = name;
}
@Override
public String getName() {
if (name == null && parent != null) {
if (parent instanceof ListSection) {
return String.valueOf(((ListSection) parent).indexOf(this));
}
if (parent instanceof MapSection) {
return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this))
.findAny().map(Entry::getKey).orElse(null);
}
}
return name;
}
@Override
public Section getParent() {
return parent;
}
@Override
public void setInScope(String key, Object value) {
put(key, value);
}
@Override
public Object put(String key, Object value) {
value = Section.wrap(value);
if (value instanceof Section) value = Section.wrapParent(this, key, (Section) value);
return super.put(key, value);
}
@Override
public void putAll(Map<? extends String, ?> m) {
m.forEach(this::put);
}
@Override
public Set<String> getKeys() {
return keySet();
}
@Override
public Optional<?> getInScope(String key) {
return Optional.ofNullable(get(key));
}
@Override
public String toString() {
return Section.toString(this);
}
}

View File

@ -0,0 +1,215 @@
package thito.nodeflow.config;
import org.yaml.snakeyaml.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
public interface Section {
String SEPARATOR = ".";
static Object wrap(Object o) {
if (o instanceof Section) return o;
if (o instanceof List) {
return new ListSection((List<?>) o);
}
if (o instanceof Map) {
return new MapSection((Map<?, ?>) o);
}
return o;
}
static String getName(String path) {
String[] split = path.split(Pattern.quote(SEPARATOR));
return split[split.length - 1];
}
static Section wrapParent(Section parent, String name, Section current) {
if (current.getParent() != null && current.getParent() != parent) {
if (current instanceof MapSection) {
MapSection mapSection = new MapSection((MapSection) current);
mapSection.setParent(parent, name);
return mapSection;
}
if (current instanceof ListSection) {
ListSection objects = new ListSection((ListSection) current);
objects.setParent(parent, name);
return objects;
}
} else {
if (current instanceof MapSection) {
((MapSection) current).setParent(parent, name);
}
if (current instanceof ListSection) {
((ListSection) current).setParent(parent, name);
}
}
return current;
}
static String toString(Section section) {
DumperOptions options = new DumperOptions();
options.setIndent(4);
options.setAllowUnicode(true);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
return yaml.dumpAsMap(section);
}
static MapSection parseToMap(Reader reader) {
Yaml yaml = new Yaml();
return new MapSection(yaml.loadAs(reader, Map.class));
}
Set<String> getKeys();
default Set<String> getPaths() {
Set<String> paths = new HashSet<>();
for (String k : getKeys()) {
Object lookup = getInScope(k).orElse(null);
if (lookup instanceof Section) {
for (String p : ((Section) lookup).getPaths()) {
paths.add(k + "." + p);
}
}
}
return paths;
}
String getName();
default String getPath() {
StringBuilder path = new StringBuilder(getName());
Section parent;
while ((parent = getParent()) != null) {
path.insert(0, parent.getName() + SEPARATOR);
}
return path.toString();
}
Section getParent();
Optional<?> getInScope(String key);
void setInScope(String key, Object value);
default void set(String path, Object value) {
String[] paths = path.split(Pattern.quote(SEPARATOR));
Object lookup = this;
for (int i = 0; i < paths.length - 1; i++) {
Section oldLookup = (Section) lookup;
lookup = oldLookup.getInScope(paths[i]).orElse(null);
if (!(lookup instanceof Section)) {
oldLookup.setInScope(paths[i], lookup = new MapSection());
}
}
if (paths.length > 0) {
((Section) lookup).setInScope(paths[paths.length - 1], value);
}
}
default Optional<?> getObject(String path) {
String[] paths = path.split(Pattern.quote(SEPARATOR));
Object lookup = this;
for (String s : paths) {
if (lookup instanceof Section) {
lookup = ((Section) lookup).getInScope(s).orElse(null);
} else {
return Optional.empty();
}
}
return Optional.ofNullable(lookup);
}
default <T extends Enum<T>> Optional<T> getEnum(String path, Class<T> clz) {
return getObject(path).map(o -> {
try {
return Enum.valueOf(clz, String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<String> getString(String path) {
return getObject(path).map(String::valueOf);
}
default Optional<Integer> getInteger(String path) {
return getObject(path).map(o -> {
try {
return Integer.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Double> getDouble(String path) {
return getObject(path).map(o -> {
try {
return Double.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Long> getLong(String path) {
return getObject(path).map(o -> {
try {
return Long.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Float> getFloat(String path) {
return getObject(path).map(o -> {
try {
return Float.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Short> getShort(String path) {
return getObject(path).map(o -> {
try {
return Short.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Byte> getByte(String path) {
return getObject(path).map(o -> {
try {
return Byte.valueOf(String.valueOf(o));
} catch (Throwable t) {
return null;
}
});
}
default Optional<Character> getCharacter(String path) {
return getObject(path).map(o -> {
String text = String.valueOf(o);
return text.isEmpty() ? null : text.charAt(0);
});
}
default Optional<Boolean> getBoolean(String path) {
return getObject(path).map(o -> {
String text = String.valueOf(o);
return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null;
});
}
default Optional<MapSection> getMap(String path) {
return getObject(path).map(o -> {
if (o instanceof Map) {
if (o instanceof MapSection) return (MapSection) o;
MapSection mapSection = new MapSection((Map<?, ?>) o);
mapSection.setParent(this, Section.getName(path));
return mapSection;
}
return null;
});
}
default Optional<ListSection> getList(String path) {
return getObject(path).map(o -> {
if (o instanceof List) {
if (o instanceof ListSection) {
return (ListSection) o;
}
ListSection list = new ListSection((List<?>) o);
list.setParent(this, Section.getName(path));
return list;
}
ListSection list = new ListSection(Collections.singleton(o));
list.setParent(this, Section.getName(path));
return list;
});
}
}

View File

@ -15,4 +15,5 @@ settings.path.title=Locales Directory
settings.path.text=Locales directory settings.path.text=Locales directory
settings.path.file-pattern=Translation file pattern settings.path.file-pattern=Translation file pattern
settings.preview=Preview locale settings.preview=Preview locale
settings.editor.assistance=I18n key completion and annotation inside editor settings.editor.assistance=I18n key completion and annotation inside editor
settings.path.prefix=Prefix