├── src ├── test │ ├── resources │ │ └── single │ │ │ ├── yaml │ │ │ ├── de.yaml │ │ │ └── en.yaml │ │ │ ├── properties │ │ │ ├── de.properties │ │ │ └── en.properties │ │ │ ├── json │ │ │ ├── en.json │ │ │ └── de.json │ │ │ └── json5 │ │ │ ├── de.json5 │ │ │ └── en.json5 │ └── java │ │ ├── de │ │ └── marhali │ │ │ └── easyi18n │ │ │ ├── e2e │ │ │ ├── single │ │ │ │ ├── SingleJsonTest.java │ │ │ │ ├── SingleYamlTest.java │ │ │ │ ├── SingleJson5Test.java │ │ │ │ └── SinglePropertiesTest.java │ │ │ ├── TestSettingsState.java │ │ │ └── EndToEndTestCase.java │ │ │ ├── mapper │ │ │ └── AbstractMapperTest.java │ │ │ ├── WildcardRegexMatcherTest.java │ │ │ └── settings │ │ │ ├── NamingConventionTest.java │ │ │ ├── ProjectSettingsServiceTest.java │ │ │ └── SettingsTestPreset.java │ │ └── util │ │ └── TranslationUtilTest.java └── main │ ├── resources │ ├── icons │ │ ├── translate13.svg │ │ └── translate13_dark.svg │ └── META-INF │ │ ├── de.marhali.easyi18n-php.xml │ │ ├── de.marhali.easyi18n-java.xml │ │ ├── de.marhali.easyi18n-kotlin.xml │ │ ├── de.marhali.easyi18n-xml.xml │ │ ├── de.marhali.easyi18n-javascript.xml │ │ ├── pluginIcon.svg │ │ └── plugin.xml │ └── java │ └── de │ └── marhali │ └── easyi18n │ ├── model │ ├── bus │ │ ├── ExpandAllListener.java │ │ ├── FilteredBusListener.java │ │ ├── FilterIncompleteListener.java │ │ ├── BusListener.java │ │ ├── FilterDuplicateListener.java │ │ ├── FocusKeyListener.java │ │ ├── UpdateDataListener.java │ │ └── SearchQueryListener.java │ ├── action │ │ ├── TranslationCreate.java │ │ ├── TranslationDelete.java │ │ └── TranslationUpdate.java │ ├── KeyPath.java │ ├── TranslationFile.java │ ├── Translation.java │ └── TranslationValue.java │ ├── exception │ ├── EmptyLocalesDirException.java │ └── SyntaxException.java │ ├── assistance │ ├── OptionalAssistance.java │ ├── completion │ │ ├── JavaCompletionContributor.java │ │ ├── JsCompletionContributor.java │ │ ├── XmlCompletionContributor.java │ │ ├── KtCompletionContributor.java │ │ ├── PhpCompletionContributor.java │ │ └── KeyCompletionProvider.java │ ├── intention │ │ ├── PhpTranslationIntention.java │ │ ├── KtTranslationIntention.java │ │ ├── JavaTranslationIntention.java │ │ ├── XmlTranslationIntention.java │ │ └── JsTranslationIntention.java │ ├── documentation │ │ └── CommonDocumentationProvider.java │ ├── folding │ │ ├── XmlFoldingBuilder.java │ │ ├── JsFoldingBuilder.java │ │ ├── PhpFoldingBuilder.java │ │ ├── JavaFoldingBuilder.java │ │ └── KtFoldingBuilder.java │ └── reference │ │ ├── JavaKeyReferenceContributor.java │ │ ├── PhpKeyReferenceContributor.java │ │ ├── XmlKeyReferenceContributor.java │ │ ├── JsKeyReferenceContributor.java │ │ ├── KtKeyReferenceContributor.java │ │ ├── AbstractKeyReferenceContributor.java │ │ └── PsiKeyReference.java │ ├── io │ ├── parser │ │ ├── yaml │ │ │ ├── YamlArrayMapper.java │ │ │ ├── YamlParserStrategy.java │ │ │ └── YamlMapper.java │ │ ├── properties │ │ │ ├── PropertiesArrayMapper.java │ │ │ ├── PropertiesMapper.java │ │ │ └── PropertiesParserStrategy.java │ │ ├── json │ │ │ ├── JsonArrayMapper.java │ │ │ ├── JsonParserStrategy.java │ │ │ └── JsonMapper.java │ │ ├── ParserStrategyType.java │ │ ├── json5 │ │ │ ├── Json5ArrayMapper.java │ │ │ ├── Json5ParserStrategy.java │ │ │ └── Json5Mapper.java │ │ ├── ArrayMapper.java │ │ └── ParserStrategy.java │ └── folder │ │ ├── FolderStrategyType.java │ │ ├── SingleFolderStrategy.java │ │ ├── ModularNamespaceFolderStrategy.java │ │ └── FolderStrategy.java │ ├── util │ ├── WildcardRegexMatcher.java │ ├── IntelliJBufferedWriter.java │ ├── UiUtil.java │ ├── TreeUtil.java │ ├── DocumentUtil.java │ ├── NotificationHelper.java │ └── TranslationUtil.java │ ├── listener │ ├── DeleteKeyListener.java │ ├── ReturnKeyListener.java │ └── PopupClickListener.java │ ├── action │ ├── ReloadAction.java │ ├── treeview │ │ ├── ExpandTreeViewAction.java │ │ └── CollapseTreeViewAction.java │ ├── SettingsAction.java │ ├── FilterIncompleteAction.java │ ├── FilterDuplicateAction.java │ ├── OpenFileAction.java │ ├── SearchAction.java │ └── AddAction.java │ ├── settings │ ├── presets │ │ ├── Preset.java │ │ ├── VueI18nPreset.java │ │ ├── ReactI18NextPreset.java │ │ ├── DefaultPreset.java │ │ └── NamingConvention.java │ ├── ProjectSettingsService.java │ ├── ProjectSettings.java │ └── ProjectSettingsConfigurable.java │ ├── dialog │ ├── descriptor │ │ └── DeleteActionDescriptor.java │ ├── EditDialog.java │ └── AddDialog.java │ ├── tabs │ ├── TableView.form │ ├── TreeView.form │ └── renderer │ │ ├── TreeRenderer.java │ │ └── TableRenderer.java │ ├── service │ ├── WindowManager.java │ ├── FileChangeListener.java │ └── TranslatorToolWindowFactory.java │ ├── DataBus.java │ └── InstanceManager.java ├── .gitignore ├── example ├── images │ ├── key-edit.PNG │ ├── settings.PNG │ ├── table-view.PNG │ ├── tree-view.PNG │ ├── key-annotation.PNG │ └── key-completion.PNG ├── modularized-json │ ├── locale-en │ │ ├── account.json │ │ ├── auth.json │ │ └── user.json │ └── locale-de │ │ ├── account.json │ │ ├── auth.json │ │ └── user.json ├── resource-bundle │ ├── locale_en.properties │ └── locale_de.properties ├── yaml │ ├── locale-en.yml │ └── locale-de.yml └── json │ ├── locale-en.json │ └── locale-de.json ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── codecov.yml ├── qodana.yml ├── .github ├── dependabot.yml └── workflows │ └── run-ui-tests.yml ├── LICENSE ├── .run ├── Run Tests.run.xml ├── Run Plugin.run.xml └── Run Verifications.run.xml ├── gradle.properties └── gradlew.bat /src/test/resources/single/yaml/de.yaml: -------------------------------------------------------------------------------- 1 | title: Titel 2 | nested: 3 | title: Titel 4 | -------------------------------------------------------------------------------- /src/test/resources/single/yaml/en.yaml: -------------------------------------------------------------------------------- 1 | title: Title 2 | nested: 3 | title: Title 4 | -------------------------------------------------------------------------------- /src/test/resources/single/properties/de.properties: -------------------------------------------------------------------------------- 1 | breakLine=eins\nzwei 2 | title=Titel 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .idea 4 | .intellijPlatform 5 | .kotlin 6 | .qodana 7 | build -------------------------------------------------------------------------------- /src/test/resources/single/properties/en.properties: -------------------------------------------------------------------------------- 1 | breakLine=first\nsecond 2 | title=Title 3 | -------------------------------------------------------------------------------- /example/images/key-edit.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/key-edit.PNG -------------------------------------------------------------------------------- /example/images/settings.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/settings.PNG -------------------------------------------------------------------------------- /example/images/table-view.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/table-view.PNG -------------------------------------------------------------------------------- /example/images/tree-view.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/tree-view.PNG -------------------------------------------------------------------------------- /example/images/key-annotation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/key-annotation.PNG -------------------------------------------------------------------------------- /example/images/key-completion.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/example/images/key-completion.PNG -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhali/easy-i18n/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/modularized-json/locale-en/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "subscription": "Subscription", 3 | "support": "Support", 4 | "delete": "Delete" 5 | } -------------------------------------------------------------------------------- /example/modularized-json/locale-de/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "subscription": "Abonnement", 3 | "support": "Unterstützung", 4 | "delete": "Löschen" 5 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "easy-i18n" 2 | 3 | plugins { 4 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 5 | } -------------------------------------------------------------------------------- /example/resource-bundle/locale_en.properties: -------------------------------------------------------------------------------- 1 | account.subscription=Subscription 2 | auth.login=Login 3 | auth.logout=Logout 4 | auth.register=Register 5 | user.email=Email Address 6 | user.username=Username -------------------------------------------------------------------------------- /src/test/resources/single/json/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Title", 3 | "number": -187, 4 | "object": { 5 | "title": "Title" 6 | }, 7 | "array": [ 8 | "item1", 9 | "item2" 10 | ] 11 | } -------------------------------------------------------------------------------- /example/modularized-json/locale-en/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": [ 3 | "Some", 4 | "array", 5 | "title" 6 | ], 7 | "login": "Login", 8 | "logout": "Logout", 9 | "register": "Register" 10 | } -------------------------------------------------------------------------------- /example/resource-bundle/locale_de.properties: -------------------------------------------------------------------------------- 1 | account.subscription=Abonnement 2 | auth.login=Einloggen 3 | auth.logout=Ausloggen 4 | auth.register=Registrieren 5 | user.email=Email-Adresse 6 | user.username=Benutzername -------------------------------------------------------------------------------- /src/test/resources/single/json/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Titel", 3 | "number": 187, 4 | "object": { 5 | "title": "Titel" 6 | }, 7 | "array": [ 8 | "element1", 9 | "element2" 10 | ] 11 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | threshold: 0% 7 | base: auto 8 | patch: 9 | default: 10 | informational: true -------------------------------------------------------------------------------- /example/modularized-json/locale-de/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": [ 3 | "Ein", 4 | "array", 5 | "Titel" 6 | ], 7 | "login": "Einloggen", 8 | "logout": "Ausloggen", 9 | "register": "Registrieren" 10 | } -------------------------------------------------------------------------------- /src/test/resources/single/json5/de.json5: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Titel", 3 | "number": 187, 4 | "hex": 0x187, 5 | "object": { 6 | "title": "Titel", 7 | }, 8 | "array": [ 9 | "element1", 10 | "element2", 11 | ], 12 | } -------------------------------------------------------------------------------- /src/test/resources/single/json5/en.json5: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Title", 3 | "number": -187, 4 | "hex": -0x187, 5 | "object": { 6 | "title": "Title", 7 | }, 8 | "array": [ 9 | "item1", 10 | "item2", 11 | ], 12 | } -------------------------------------------------------------------------------- /example/modularized-json/locale-en/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "Username", 3 | "email": "Email Address", 4 | "address": { 5 | "zip": "ZIP code", 6 | "city": "City", 7 | "street": "Street", 8 | "number": "House number" 9 | } 10 | } -------------------------------------------------------------------------------- /example/modularized-json/locale-de/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "Benutzername", 3 | "email": "Email-Adresse", 4 | "address": { 5 | "zip": "Postleitzahl", 6 | "city": "Ort", 7 | "street": "Straße", 8 | "number": "Hausnummer" 9 | } 10 | } -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: "1.0" 5 | linter: jetbrains/qodana-jvm-community:2024.3 6 | projectJDK: "21" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/icons/translate13.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/translate13_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/ExpandAllListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | /** 4 | * Single event listener. 5 | * @see #onExpandAll() 6 | * @author marhali 7 | */ 8 | public interface ExpandAllListener { 9 | /** 10 | * Action to expand all nodes 11 | */ 12 | void onExpandAll(); 13 | } -------------------------------------------------------------------------------- /example/yaml/locale-en.yml: -------------------------------------------------------------------------------- 1 | alpha: 2 | spacing: ' leading space' 3 | first: Example Translation 4 | beta: 5 | title: Title 6 | nested: 7 | title: some nested title 8 | gamma: 9 | array: 10 | escaped: 11 | - first;element 12 | - second element 13 | - third;element 14 | simple: 15 | - first element 16 | - second element 17 | title: gamma title 18 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/FilteredBusListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | import de.marhali.easyi18n.FilteredDataBus; 4 | 5 | /** 6 | * Interface to replicate the state of {@link FilteredDataBus} to underlying components. 7 | * @author marhali 8 | */ 9 | public interface FilteredBusListener extends UpdateDataListener, FocusKeyListener, ExpandAllListener {} 10 | -------------------------------------------------------------------------------- /example/yaml/locale-de.yml: -------------------------------------------------------------------------------- 1 | alpha: 2 | spacing: ' führendes Leerzeichen' 3 | first: Beispiel Übersetzung 4 | beta: 5 | title: Titel 6 | nested: 7 | title: Ein verschachtelter Titel 8 | gamma: 9 | array: 10 | escaped: 11 | - Erstes;Element 12 | - Zweites Element 13 | - Drittes;Element 14 | simple: 15 | - Erstes Element 16 | - Zweites Element 17 | title: Gamma Titel 18 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/exception/EmptyLocalesDirException.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.exception; 2 | 3 | /** 4 | * Indicates that the translation's directory has not been configured yet 5 | * @author marhali 6 | */ 7 | public class EmptyLocalesDirException extends IllegalArgumentException { 8 | public EmptyLocalesDirException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/FilterIncompleteListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | /** 4 | * Single event listener. 5 | * @author marhali 6 | */ 7 | public interface FilterIncompleteListener { 8 | /** 9 | * Toggles filter of missing translations 10 | * @param filter True if only translations with missing values should be shown 11 | */ 12 | void onFilterIncomplete(boolean filter); 13 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/BusListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | /** 4 | * Interface for communication of changes for participants of the data bus. 5 | * Every listener needs to be registered via {@link de.marhali.easyi18n.DataBus}. 6 | * 7 | * @author marhali 8 | */ 9 | public interface BusListener extends UpdateDataListener, FilterIncompleteListener, 10 | FilterDuplicateListener, SearchQueryListener, FocusKeyListener {} -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/FilterDuplicateListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | /** 4 | * Single event listener 5 | * @see #onFilterDuplicate(boolean) 6 | * @author marhali 7 | */ 8 | public interface FilterDuplicateListener { 9 | /** 10 | * Toggles filter of duplicated translation values 11 | * @param filter True if only translations with duplicates values should be shown 12 | */ 13 | void onFilterDuplicate(boolean filter); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/action/TranslationCreate.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.action; 2 | 3 | import de.marhali.easyi18n.model.Translation; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | /** 8 | * Represents update request to create a new translation. 9 | * @author marhali 10 | */ 11 | public class TranslationCreate extends TranslationUpdate { 12 | public TranslationCreate(@NotNull Translation translation) { 13 | super(null, translation); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/action/TranslationDelete.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.action; 2 | 3 | import de.marhali.easyi18n.model.Translation; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | /** 8 | * Represents update request to delete a existing translation. 9 | * @author marhali 10 | */ 11 | public class TranslationDelete extends TranslationUpdate { 12 | public TranslationDelete(@NotNull Translation translation) { 13 | super(translation, null); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/FocusKeyListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | import de.marhali.easyi18n.model.KeyPath; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | /** 8 | * Single event listener. 9 | * @author marhali 10 | */ 11 | public interface FocusKeyListener { 12 | /** 13 | * Move the specified translation key (full-key) into focus. 14 | * @param key Absolute translation key 15 | */ 16 | void onFocusKey(@NotNull KeyPath key); 17 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/UpdateDataListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | import de.marhali.easyi18n.model.TranslationData; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * Single event listener. 8 | * @author marhali 9 | */ 10 | public interface UpdateDataListener { 11 | /** 12 | * Update the underlying translation data set. 13 | * @param data Updated translations 14 | */ 15 | void onUpdateData(@NotNull TranslationData data); 16 | } -------------------------------------------------------------------------------- /example/json/locale-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": { 3 | "first": "example translation", 4 | "second": "another translation" 5 | }, 6 | "beta": { 7 | "title": "some title", 8 | "nested": { 9 | "title": "some nested title" 10 | } 11 | }, 12 | "gamma": { 13 | "title": "gamma title", 14 | "array": { 15 | "simple": [ 16 | "first element", 17 | "second element" 18 | ], 19 | "escaped": [ 20 | "first;element", 21 | "second element", 22 | "third;element" 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/OptionalAssistance.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance; 2 | 3 | import com.intellij.openapi.project.Project; 4 | 5 | import de.marhali.easyi18n.settings.ProjectSettingsService; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | /** 10 | * Used to define editor hooks as assistable. 11 | * @author marhali 12 | */ 13 | public interface OptionalAssistance { 14 | default boolean isAssistance(@NotNull Project project) { 15 | return ProjectSettingsService.get(project).getState().isAssistance(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/json/locale-de.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": { 3 | "first": "Beispiel Übersetzung", 4 | "second": "Andere Übersetzung" 5 | }, 6 | "beta": { 7 | "title": "Ein Titel", 8 | "nested": { 9 | "title": "Ein verschachtelter Titel" 10 | } 11 | }, 12 | "gamma": { 13 | "title": "Gamma Titel", 14 | "array": { 15 | "simple": [ 16 | "Erstes Element", 17 | "Zweites Element" 18 | ], 19 | "escaped": [ 20 | "Erstes;Element", 21 | "Zweites Element", 22 | "Drittes;Element" 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/bus/SearchQueryListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.bus; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | /** 6 | * Single event listener. 7 | * @author marhali 8 | */ 9 | public interface SearchQueryListener { 10 | /** 11 | * Filter the displayed data according to the search query. Supply 'null' to return to the normal state. 12 | * The keys and the content itself should be considered (full-text-search). 13 | * @param query Filter key or content 14 | */ 15 | void onSearchQuery(@Nullable String query); 16 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/yaml/YamlArrayMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.yaml; 2 | 3 | import de.marhali.easyi18n.io.parser.ArrayMapper; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class YamlArrayMapper extends ArrayMapper { 9 | public static String read(List list) { 10 | return read(list.iterator(), Object::toString); 11 | } 12 | 13 | public static List write(String concat) { 14 | List list = new ArrayList<>(); 15 | write(concat, list::add); 16 | return list; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/exception/SyntaxException.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.exception; 2 | 3 | import de.marhali.easyi18n.model.TranslationFile; 4 | 5 | /** 6 | * Indicates a syntax error in a managed translation file. 7 | * @author marhali 8 | */ 9 | public class SyntaxException extends RuntimeException { 10 | private final TranslationFile file; 11 | 12 | public SyntaxException(String message, TranslationFile file) { 13 | super(message); 14 | this.file = file; 15 | } 16 | 17 | public TranslationFile getFile() { 18 | return file; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/WildcardRegexMatcher.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import org.apache.commons.io.FilenameUtils; 4 | 5 | /** 6 | * Utilities for wildcard / regex matching. 7 | * @author marhali 8 | */ 9 | public class WildcardRegexMatcher { 10 | public static boolean matchWildcardRegex(String string, String pattern) { 11 | boolean wildcardMatch = FilenameUtils.wildcardMatchOnSystem(string, pattern); 12 | 13 | if(wildcardMatch) { 14 | return true; 15 | } 16 | 17 | try { 18 | return string.matches(pattern); 19 | } catch (Exception e) { 20 | return false; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/JavaCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.PsiLiteralExpression; 7 | 8 | /** 9 | * Java specific completion contributor. 10 | * @author marhali 11 | */ 12 | public class JavaCompletionContributor extends CompletionContributor { 13 | public JavaCompletionContributor() { 14 | extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralExpression.class), 15 | new KeyCompletionProvider()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/JsCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.lang.javascript.psi.JSLiteralExpression; 6 | import com.intellij.patterns.PlatformPatterns; 7 | 8 | /** 9 | * JavaScript specific completion contributor. 10 | * @author marhali 11 | */ 12 | public class JsCompletionContributor extends CompletionContributor { 13 | public JsCompletionContributor() { 14 | extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(JSLiteralExpression.class), 15 | new KeyCompletionProvider()); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/XmlCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl; 7 | 8 | /** 9 | * Xml specific completion contributor. 10 | * @author adeptius 11 | */ 12 | public class XmlCompletionContributor extends CompletionContributor { 13 | public XmlCompletionContributor() { 14 | extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(XmlAttributeValueImpl.class), 15 | new KeyCompletionProvider()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/single/SingleJsonTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e.single; 2 | 3 | import de.marhali.easyi18n.e2e.EndToEndTestCase; 4 | import de.marhali.easyi18n.e2e.TestSettingsState; 5 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 6 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 7 | 8 | /** 9 | * End-to-end tests for single directory json files. 10 | * @author marhali 11 | */ 12 | public class SingleJsonTest extends EndToEndTestCase { 13 | public SingleJsonTest() { 14 | super(new TestSettingsState( 15 | "src/test/resources/single/json", 16 | FolderStrategyType.SINGLE, 17 | ParserStrategyType.JSON) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/single/SingleYamlTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e.single; 2 | 3 | import de.marhali.easyi18n.e2e.EndToEndTestCase; 4 | import de.marhali.easyi18n.e2e.TestSettingsState; 5 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 6 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 7 | 8 | /** 9 | * End-to-ends tests for single directory yaml files. 10 | * @author marhali 11 | */ 12 | public class SingleYamlTest extends EndToEndTestCase { 13 | public SingleYamlTest() { 14 | super(new TestSettingsState( 15 | "src/test/resources/single/yaml", 16 | FolderStrategyType.SINGLE, 17 | ParserStrategyType.YML) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/KtCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry; 7 | 8 | /** 9 | * Kotlin specific completion contributor. 10 | * @author marhali 11 | */ 12 | public class KtCompletionContributor extends CompletionContributor { 13 | public KtCompletionContributor() { 14 | extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(KtLiteralStringTemplateEntry.class), 15 | new KeyCompletionProvider()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/PhpCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; 7 | 8 | /** 9 | * Php specific completion contributor. 10 | * @author marhali 11 | */ 12 | public class PhpCompletionContributor extends CompletionContributor { 13 | public PhpCompletionContributor() { 14 | extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(StringLiteralExpression.class), 15 | new KeyCompletionProvider()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/properties/PropertiesArrayMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.properties; 2 | 3 | import de.marhali.easyi18n.io.parser.ArrayMapper; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Map for 'properties' array values. 11 | * @author marhali 12 | */ 13 | public class PropertiesArrayMapper extends ArrayMapper { 14 | public static String read(String[] list) { 15 | return read(Arrays.stream(list).iterator(), Object::toString); 16 | } 17 | 18 | public static String[] write(String concat) { 19 | List list = new ArrayList<>(); 20 | write(concat, list::add); 21 | return list.toArray(new String[0]); 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/single/SingleJson5Test.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e.single; 2 | 3 | import de.marhali.easyi18n.e2e.EndToEndTestCase; 4 | import de.marhali.easyi18n.e2e.TestSettingsState; 5 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 6 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 7 | 8 | /** 9 | * @author marhali 10 | * End-to-end tests for single directory json5 files. 11 | */ 12 | public class SingleJson5Test extends EndToEndTestCase { 13 | public SingleJson5Test() { 14 | super(new TestSettingsState( 15 | "src/test/resources/single/json5", 16 | FolderStrategyType.SINGLE, 17 | ParserStrategyType.JSON5) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/single/SinglePropertiesTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e.single; 2 | 3 | import de.marhali.easyi18n.e2e.EndToEndTestCase; 4 | import de.marhali.easyi18n.e2e.TestSettingsState; 5 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 6 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 7 | 8 | /** 9 | * End-to-end tests for single directory .properties files. 10 | * @author marhali 11 | */ 12 | public class SinglePropertiesTest extends EndToEndTestCase { 13 | public SinglePropertiesTest() { 14 | super(new TestSettingsState( 15 | "src/test/resources/single/properties", 16 | FolderStrategyType.SINGLE, 17 | ParserStrategyType.PROPERTIES) 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/IntelliJBufferedWriter.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.BufferedWriter; 6 | import java.io.IOException; 7 | import java.io.Writer; 8 | 9 | /** 10 | * IntelliJ aware BufferedWriter implementation. 11 | * (Document PSI uses \n as line separator) 12 | * @author marhali 13 | */ 14 | public class IntelliJBufferedWriter extends BufferedWriter { 15 | public IntelliJBufferedWriter(@NotNull Writer out) { 16 | super(out); 17 | } 18 | 19 | public IntelliJBufferedWriter(@NotNull Writer out, int sz) { 20 | super(out, sz); 21 | } 22 | 23 | @Override 24 | public void newLine() throws IOException { 25 | write("\n"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/listener/DeleteKeyListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.listener; 2 | 3 | import java.awt.event.KeyEvent; 4 | import java.awt.event.KeyListener; 5 | 6 | /** 7 | * Delete (DEL) keystroke listener. 8 | * @author marhali 9 | */ 10 | public class DeleteKeyListener implements KeyListener { 11 | 12 | private final Runnable onActivate; 13 | 14 | public DeleteKeyListener(Runnable onActivate) { 15 | this.onActivate = onActivate; 16 | } 17 | 18 | @Override 19 | public void keyTyped(KeyEvent e) { 20 | if(e.getKeyChar() == KeyEvent.VK_DELETE) { 21 | this.onActivate.run(); 22 | } 23 | } 24 | 25 | @Override 26 | public void keyPressed(KeyEvent e) {} 27 | 28 | @Override 29 | public void keyReleased(KeyEvent e) {} 30 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/listener/ReturnKeyListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.listener; 2 | 3 | import java.awt.event.KeyEvent; 4 | import java.awt.event.KeyListener; 5 | 6 | /** 7 | * Return (\n) keystroke listener. 8 | * @author marhali 9 | */ 10 | public class ReturnKeyListener implements KeyListener { 11 | 12 | private final Runnable onActivate; 13 | 14 | public ReturnKeyListener(Runnable onActivate) { 15 | this.onActivate = onActivate; 16 | } 17 | 18 | @Override 19 | public void keyTyped(KeyEvent e) { 20 | if(e.getKeyChar() == KeyEvent.VK_ENTER) { 21 | this.onActivate.run(); 22 | } 23 | } 24 | 25 | @Override 26 | public void keyPressed(KeyEvent e) {} 27 | 28 | @Override 29 | public void keyReleased(KeyEvent e) {} 30 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/intention/PhpTranslationIntention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.intention; 2 | 3 | import com.intellij.openapi.util.TextRange; 4 | import com.intellij.psi.PsiElement; 5 | import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Php specific translation intention 12 | * @author marhali 13 | */ 14 | public class PhpTranslationIntention extends AbstractTranslationIntention { 15 | @Override 16 | protected @Nullable String extractText(@NotNull PsiElement element) { 17 | if(!(element.getParent() instanceof StringLiteralExpression)) { 18 | return null; 19 | } 20 | 21 | return ((StringLiteralExpression) element.getParent()).getContents(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/intention/KtTranslationIntention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.intention; 2 | 3 | import com.intellij.psi.PsiElement; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry; 8 | 9 | /** 10 | * Kotlin specific translation key intention. 11 | * @author marhali 12 | */ 13 | public class KtTranslationIntention extends AbstractTranslationIntention { 14 | @Override 15 | protected @Nullable String extractText(@NotNull PsiElement element) { 16 | if(!(element.getParent() instanceof KtLiteralStringTemplateEntry)) { 17 | return null; 18 | } 19 | 20 | KtLiteralStringTemplateEntry expression = (KtLiteralStringTemplateEntry) element.getParent(); 21 | return expression.getText(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/ReloadAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | 7 | import de.marhali.easyi18n.InstanceManager; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Objects; 12 | import java.util.ResourceBundle; 13 | 14 | /** 15 | * Reload translations action. 16 | * @author marhali 17 | */ 18 | public class ReloadAction extends AnAction { 19 | 20 | public ReloadAction() { 21 | super(ResourceBundle.getBundle("messages").getString("action.reload"), 22 | null, AllIcons.Actions.Refresh); 23 | } 24 | 25 | @Override 26 | public void actionPerformed(@NotNull AnActionEvent e) { 27 | InstanceManager.get(Objects.requireNonNull(e.getProject())).reload(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/treeview/ExpandTreeViewAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action.treeview; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.ResourceBundle; 9 | 10 | /** 11 | * Action to expand the entire tree (open all nodes with children). 12 | * @author marhali 13 | */ 14 | public class ExpandTreeViewAction extends AnAction { 15 | 16 | private final Runnable expandRunnable; 17 | 18 | public ExpandTreeViewAction(Runnable expandRunnable) { 19 | super(ResourceBundle.getBundle("messages").getString("view.tree.expand"), 20 | null, AllIcons.Actions.Expandall); 21 | 22 | this.expandRunnable = expandRunnable; 23 | } 24 | 25 | @Override 26 | public void actionPerformed(@NotNull AnActionEvent e) { 27 | expandRunnable.run(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/intention/JavaTranslationIntention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.intention; 2 | 3 | import com.intellij.openapi.util.TextRange; 4 | import com.intellij.psi.*; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * Java specific translation intention. 11 | * @author marhali 12 | */ 13 | public class JavaTranslationIntention extends AbstractTranslationIntention { 14 | @Override 15 | protected @Nullable String extractText(@NotNull PsiElement element) { 16 | if(!(element.getParent() instanceof PsiLiteralExpression)) { 17 | return null; 18 | } 19 | 20 | return String.valueOf(((PsiLiteralExpression) element.getParent()).getValue()); 21 | } 22 | 23 | @Override 24 | @NotNull TextRange convertRange(@NotNull TextRange input) { 25 | return new TextRange(input.getStartOffset() + 1, input.getEndOffset() - 1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/treeview/CollapseTreeViewAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action.treeview; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.ResourceBundle; 9 | 10 | /** 11 | * Action to collapse all tree nodes with children. 12 | * @author marhali 13 | */ 14 | public class CollapseTreeViewAction extends AnAction { 15 | 16 | private final Runnable collapseRunnable; 17 | 18 | public CollapseTreeViewAction(Runnable collapseRunnable) { 19 | super(ResourceBundle.getBundle("messages").getString("view.tree.collapse"), 20 | null, AllIcons.Actions.Collapseall); 21 | 22 | this.collapseRunnable = collapseRunnable; 23 | } 24 | 25 | @Override 26 | public void actionPerformed(@NotNull AnActionEvent e) { 27 | collapseRunnable.run(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/intention/XmlTranslationIntention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.intention; 2 | 3 | import com.intellij.openapi.util.TextRange; 4 | import com.intellij.psi.PsiElement; 5 | import com.intellij.psi.xml.XmlAttributeValue; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * Xml specific translation key intention. 11 | * @author adeptius 12 | */ 13 | public class XmlTranslationIntention extends AbstractTranslationIntention { 14 | @Override 15 | protected @Nullable String extractText(@NotNull PsiElement element) { 16 | if(!(element.getParent() instanceof XmlAttributeValue)) { 17 | return null; 18 | } 19 | 20 | return ((XmlAttributeValue) element.getParent()).getValue(); 21 | } 22 | 23 | @Override 24 | @NotNull TextRange convertRange(@NotNull TextRange input) { 25 | return new TextRange(input.getStartOffset() + 1, input.getEndOffset() - 1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/presets/Preset.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings.presets; 2 | 3 | import de.marhali.easyi18n.settings.ProjectSettings; 4 | 5 | /** 6 | * Enumeration of all available configuration presets. 7 | * Every preset needs to be registered here to be properly recognized. 8 | * @author marhali 9 | */ 10 | public enum Preset { 11 | DEFAULT(DefaultPreset.class), 12 | VUE_I18N(VueI18nPreset.class), 13 | REACT_I18NEXT(ReactI18NextPreset.class); 14 | 15 | private final Class clazz; 16 | 17 | Preset(Class clazz) { 18 | this.clazz = clazz; 19 | } 20 | 21 | public ProjectSettings config() { 22 | try { 23 | return this.clazz.getDeclaredConstructor().newInstance(); 24 | } catch (Exception e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return super.name().toLowerCase(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/dialog/descriptor/DeleteActionDescriptor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.dialog.descriptor; 2 | 3 | import com.intellij.openapi.ui.DialogWrapper; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionEvent; 8 | import java.util.ResourceBundle; 9 | 10 | /** 11 | * Delete action which represents the delete button on the edit translation dialog. 12 | * Action can be monitored using the exit code for the opened dialog. See EXIT_CODE. 13 | * @author marhali 14 | */ 15 | public class DeleteActionDescriptor extends AbstractAction { 16 | 17 | public static final int EXIT_CODE = 10; 18 | 19 | private final DialogWrapper dialog; 20 | 21 | public DeleteActionDescriptor(@NotNull DialogWrapper dialog) { 22 | super(ResourceBundle.getBundle("messages").getString("action.delete")); 23 | this.dialog = dialog; 24 | } 25 | 26 | @Override 27 | public void actionPerformed(ActionEvent e) { 28 | dialog.close(EXIT_CODE, false); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/intention/JsTranslationIntention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.intention; 2 | 3 | import com.intellij.lang.javascript.psi.JSLiteralExpression; 4 | import com.intellij.openapi.util.TextRange; 5 | import com.intellij.psi.PsiElement; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * JavaScript specific translation key intention. 12 | * @author marhali 13 | */ 14 | public class JsTranslationIntention extends AbstractTranslationIntention { 15 | @Override 16 | protected @Nullable String extractText(@NotNull PsiElement element) { 17 | if(!(element.getParent() instanceof JSLiteralExpression)) { 18 | return null; 19 | } 20 | 21 | return ((JSLiteralExpression) element.getParent()).getStringValue(); 22 | } 23 | 24 | @Override 25 | @NotNull TextRange convertRange(@NotNull TextRange input) { 26 | return new TextRange(input.getStartOffset() + 1, input.getEndOffset() - 1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/UiUtil.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | 6 | /** 7 | * User interface utilities. 8 | * @author marhali 9 | */ 10 | public class UiUtil { 11 | 12 | /** 13 | * Generates a html compliant string which shows all defined translations 14 | * @param messages Contains locales with desired translation 15 | * @return String with html format 16 | */ 17 | public static String generateHtmlTooltip(Set> messages) { 18 | StringBuilder builder = new StringBuilder(); 19 | 20 | builder.append(""); 21 | 22 | for(Map.Entry entry : messages) { 23 | builder.append(""); 24 | builder.append(entry.getKey()).append(":"); 25 | builder.append(" "); 26 | builder.append(entry.getValue()); 27 | builder.append("
"); 28 | } 29 | 30 | builder.append(""); 31 | 32 | return builder.toString(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/tabs/TableView.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/documentation/CommonDocumentationProvider.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.documentation; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import de.marhali.easyi18n.assistance.reference.PsiKeyReference; 5 | import org.jetbrains.annotations.Nls; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | /** 9 | * Language unspecific documentation provider. Every supported language should register an extension to this EP. 10 | * @author marhali 11 | */ 12 | public class CommonDocumentationProvider extends AbstractDocumentationProvider { 13 | 14 | @Override 15 | public @Nullable 16 | @Nls String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { 17 | if(!(element instanceof PsiKeyReference.TranslationReference)) { 18 | return null; 19 | } 20 | 21 | PsiKeyReference.TranslationReference keyReference = (PsiKeyReference.TranslationReference) element; 22 | String value = keyReference.getName(); 23 | 24 | return generateDoc(element.getProject(), value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/tabs/TreeView.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/SettingsAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.options.ShowSettingsUtil; 7 | 8 | import de.marhali.easyi18n.settings.ProjectSettingsConfigurable; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.ResourceBundle; 13 | 14 | /** 15 | * Plugin settings action. 16 | * @author marhali 17 | */ 18 | public class SettingsAction extends AnAction { 19 | 20 | public SettingsAction() { 21 | this(true); 22 | } 23 | 24 | public SettingsAction(boolean showIcon) { 25 | super(ResourceBundle.getBundle("messages").getString("action.settings"), 26 | null, showIcon ? AllIcons.General.Settings : null); 27 | } 28 | 29 | @Override 30 | public void actionPerformed(@NotNull AnActionEvent e) { 31 | ShowSettingsUtil.getInstance().showSettingsDialog(e.getProject(), ProjectSettingsConfigurable.class); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/de.marhali.easyi18n-php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | de.marhali.easyi18n.assistance.intention.PhpTranslationIntention 5 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json/JsonArrayMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json; 2 | 3 | import com.google.gson.*; 4 | 5 | import de.marhali.easyi18n.io.parser.ArrayMapper; 6 | 7 | /** 8 | * Map json array values. 9 | * @author marhali 10 | */ 11 | public class JsonArrayMapper extends ArrayMapper { 12 | 13 | private static final Gson GSON = new GsonBuilder().create(); 14 | 15 | public static String read(JsonArray array) { 16 | return read(array.iterator(), (jsonElement -> jsonElement.isJsonArray() || jsonElement.isJsonObject() 17 | ? "\\" + jsonElement 18 | : jsonElement.getAsString())); 19 | } 20 | 21 | public static JsonArray write(String concat) { 22 | JsonArray array = new JsonArray(); 23 | 24 | write(concat, (element) -> { 25 | if(element.startsWith("\\")) { 26 | array.add(GSON.fromJson(element.replace("\\", ""), JsonElement.class)); 27 | } else { 28 | array.add(element); 29 | } 30 | }); 31 | 32 | return array; 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/de.marhali.easyi18n-java.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | de.marhali.easyi18n.assistance.intention.JavaTranslationIntention 5 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/de.marhali.easyi18n-kotlin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | de.marhali.easyi18n.assistance.intention.KtTranslationIntention 5 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/de.marhali.easyi18n-xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 25 | de.marhali.easyi18n.assistance.intention.XmlTranslationIntention 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marcel Haßlinger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/java/util/TranslationUtilTest.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import de.marhali.easyi18n.model.TranslationData; 4 | import de.marhali.easyi18n.model.TranslationValue; 5 | import de.marhali.easyi18n.util.TranslationUtil; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | import java.util.Map; 10 | 11 | public class TranslationUtilTest { 12 | @Test 13 | public void isIncomplete() { 14 | TranslationData data = new TranslationData(true); 15 | 16 | data.addLocale("de"); 17 | data.addLocale("en"); 18 | 19 | TranslationValue complete = new TranslationValue(); 20 | complete.setLocaleValues(Map.of("de", "deValue", "en", "enValue")); 21 | Assert.assertFalse(TranslationUtil.isIncomplete(complete, data)); 22 | 23 | TranslationValue missingLocale = new TranslationValue("de", "deValue"); 24 | Assert.assertTrue(TranslationUtil.isIncomplete(missingLocale, data)); 25 | 26 | TranslationValue emptyLocaleValue = new TranslationValue(); 27 | emptyLocaleValue.setLocaleValues(Map.of("de", "deValue", "en", "")); 28 | Assert.assertTrue(TranslationUtil.isIncomplete(emptyLocaleValue, data)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/KeyPath.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * Represents the absolute key path for a desired translation. 11 | * The key could be based one or many sections. 12 | * Classes implementing this structure need to take care on how to layer translations paths. 13 | * @author marhali 14 | */ 15 | public class KeyPath extends ArrayList { 16 | 17 | public KeyPath() {} 18 | 19 | public KeyPath(@Nullable String... path) { 20 | super.addAll(List.of(path)); 21 | } 22 | 23 | public KeyPath(@NotNull List path) { 24 | super(path); 25 | } 26 | 27 | public KeyPath(@NotNull KeyPath path, @Nullable String... pathToAppend) { 28 | this(path); 29 | super.addAll(List.of(pathToAppend)); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | // Just a simple array view (e.g. [first, second]) - use KeyPathConverter to properly convert a key path 35 | return super.toString(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/folding/XmlFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.folding; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.openapi.util.Pair; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.util.PsiTreeUtil; 7 | import com.intellij.psi.xml.XmlAttributeValue; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | public class XmlFoldingBuilder extends AbstractFoldingBuilder { 16 | @Override 17 | @NotNull List> extractRegions(@NotNull PsiElement root) { 18 | return PsiTreeUtil.findChildrenOfType(root, XmlAttributeValue.class) 19 | .stream() 20 | .map((attributeValue -> Pair.pair(attributeValue.getValue(), (PsiElement) attributeValue))) 21 | .collect(Collectors.toList()); 22 | } 23 | 24 | @Override 25 | @Nullable String extractText(@NotNull ASTNode node) { 26 | XmlAttributeValue attributeValue = node.getPsi(XmlAttributeValue.class); 27 | return attributeValue.getValue(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | json5-java = "2.0.0" 4 | commons-lang3 = "3.18.0" 5 | commons-text = "1.14.0" 6 | junit = "4.13.2" 7 | opentest4j = "1.3.0" 8 | 9 | # plugins 10 | changelog = "2.4.0" 11 | intelliJPlatform = "2.7.2" 12 | kotlin = "2.2.0" 13 | kover = "0.9.1" 14 | qodana = "2025.1.1" 15 | 16 | [libraries] 17 | json5-java = { group = "de.marhali", name = "json5-java", version.ref = "json5-java" } 18 | commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" } 19 | commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" } 20 | junit = { group = "junit", name = "junit", version.ref = "junit" } 21 | opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" } 22 | 23 | [plugins] 24 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 25 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 26 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 27 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 28 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/listener/PopupClickListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.listener; 2 | 3 | import java.awt.event.MouseEvent; 4 | import java.awt.event.MouseListener; 5 | import java.util.function.Consumer; 6 | 7 | /** 8 | * Popup click listener for awt {@link MouseListener}. 9 | * Emits consumer defined in constructor on popup open action. 10 | * @author marhali 11 | */ 12 | public class PopupClickListener implements MouseListener { 13 | 14 | private final Consumer callback; 15 | 16 | public PopupClickListener(Consumer callback) { 17 | this.callback = callback; 18 | } 19 | 20 | @Override 21 | public void mouseClicked(MouseEvent e) {} 22 | 23 | @Override 24 | public void mousePressed(MouseEvent e) { 25 | if(e.isPopupTrigger()) { 26 | this.callback.accept(e); 27 | } 28 | } 29 | 30 | @Override 31 | public void mouseReleased(MouseEvent e) { 32 | if(e.isPopupTrigger()) { 33 | this.callback.accept(e); 34 | } 35 | } 36 | 37 | @Override 38 | public void mouseEntered(MouseEvent e) {} 39 | 40 | @Override 41 | public void mouseExited(MouseEvent e) {} 42 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/tabs/renderer/TreeRenderer.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.tabs.renderer; 2 | 3 | import com.intellij.ide.util.treeView.NodeRenderer; 4 | import com.intellij.navigation.ItemPresentation; 5 | import com.intellij.openapi.util.NlsSafe; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import javax.swing.*; 10 | 11 | /** 12 | * Similar to {@link NodeRenderer} but will will override {@link #getPresentation(Object)} to 13 | * make {@link ItemPresentation} visible. 14 | * @author marhali 15 | */ 16 | public class TreeRenderer extends NodeRenderer { 17 | 18 | @Override 19 | public void customizeCellRenderer(@NotNull JTree tree, @NlsSafe Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { 20 | super.customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus); 21 | } 22 | 23 | @Override 24 | protected @Nullable ItemPresentation getPresentation(Object node) { 25 | if(node instanceof ItemPresentation) { 26 | return (ItemPresentation) node; 27 | } else { 28 | return super.getPresentation(node); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/service/WindowManager.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.service; 2 | 3 | import com.intellij.openapi.wm.ToolWindow; 4 | 5 | import de.marhali.easyi18n.tabs.TableView; 6 | import de.marhali.easyi18n.tabs.TreeView; 7 | 8 | /** 9 | * Provides access to the plugin's own tool-window. 10 | * @author marhali 11 | */ 12 | public class WindowManager { 13 | 14 | private static WindowManager INSTANCE; 15 | 16 | private ToolWindow toolWindow; 17 | private TreeView treeView; 18 | private TableView tableView; 19 | 20 | public static WindowManager getInstance() { 21 | return INSTANCE == null ? INSTANCE = new WindowManager() : INSTANCE; 22 | } 23 | 24 | private WindowManager() {} 25 | 26 | public void initialize(ToolWindow toolWindow, TreeView treeView, TableView tableView) { 27 | this.toolWindow = toolWindow; 28 | this.treeView = treeView; 29 | this.tableView = tableView; 30 | } 31 | 32 | public ToolWindow getToolWindow() { 33 | return toolWindow; 34 | } 35 | 36 | public TreeView getTreeView() { 37 | return treeView; 38 | } 39 | 40 | public TableView getTableView() { 41 | return tableView; 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/TreeUtil.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import com.intellij.ide.projectView.PresentationData; 4 | import de.marhali.easyi18n.model.KeyPath; 5 | 6 | import javax.swing.tree.DefaultMutableTreeNode; 7 | import javax.swing.tree.TreePath; 8 | 9 | /** 10 | * Swing tree utility 11 | * @author marhali 12 | */ 13 | public class TreeUtil { 14 | 15 | /** 16 | * Constructs the full path for a given {@link TreePath} 17 | * @param treePath TreePath 18 | * @return Corresponding key path 19 | */ 20 | public static KeyPath getFullPath(TreePath treePath) { 21 | KeyPath keyPath = new KeyPath(); 22 | 23 | for (Object obj : treePath.getPath()) { 24 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj; 25 | Object value = node.getUserObject(); 26 | String section = value instanceof PresentationData ? 27 | ((PresentationData) value).getPresentableText() : String.valueOf(value); 28 | 29 | if(value == null) { // Skip empty sections 30 | continue; 31 | } 32 | 33 | keyPath.add(section); 34 | } 35 | 36 | return keyPath; 37 | } 38 | } -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/DocumentUtil.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import com.intellij.openapi.editor.Document; 4 | import com.intellij.openapi.fileEditor.FileDocumentManager; 5 | import com.intellij.openapi.fileTypes.FileType; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | 8 | public class DocumentUtil { 9 | protected Document document; 10 | private FileType fileType; 11 | 12 | public DocumentUtil(Document document) { 13 | setDocument(document); 14 | } 15 | 16 | public Document getDocument() { 17 | return document; 18 | } 19 | 20 | public void setDocument(Document document) { 21 | this.document = document; 22 | FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); 23 | VirtualFile virtualFile = fileDocumentManager.getFile(document); 24 | if (virtualFile != null) { 25 | fileType = virtualFile.getFileType(); 26 | } 27 | } 28 | 29 | public boolean isJsOrTs() { 30 | return (fileType.getDefaultExtension().contains("js") || fileType.getDescription().contains("ts")); 31 | } 32 | 33 | public boolean isVue() { 34 | return fileType.getDefaultExtension().contains("vue"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/FilterIncompleteAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.project.Project; 7 | 8 | import de.marhali.easyi18n.InstanceManager; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.Objects; 13 | import java.util.ResourceBundle; 14 | 15 | /** 16 | * Action which toggles translation filter on missing values. 17 | * @author marhali 18 | */ 19 | public class FilterIncompleteAction extends AnAction { 20 | public FilterIncompleteAction() { 21 | super(ResourceBundle.getBundle("messages").getString("action.filter.incomplete"), 22 | null, AllIcons.Actions.Words); 23 | } 24 | 25 | @Override 26 | public void actionPerformed(@NotNull AnActionEvent e) { 27 | Project project = Objects.requireNonNull(e.getProject()); 28 | boolean enable = e.getPresentation().getIcon() == AllIcons.Actions.Words; 29 | e.getPresentation().setIcon(enable ? AllIcons.Actions.WordsSelected : AllIcons.Actions.Words); 30 | InstanceManager.get(project).bus().propagate().onFilterIncomplete(enable); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/FilterDuplicateAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.project.Project; 7 | 8 | import de.marhali.easyi18n.InstanceManager; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.Objects; 13 | import java.util.ResourceBundle; 14 | 15 | /** 16 | * Action to toggle duplicate translation values filter. 17 | * @author marhali 18 | */ 19 | public class FilterDuplicateAction extends AnAction { 20 | public FilterDuplicateAction() { 21 | super(ResourceBundle.getBundle("messages").getString("action.filter.duplicate"), 22 | null, AllIcons.Actions.PreserveCase); 23 | } 24 | @Override 25 | public void actionPerformed(@NotNull AnActionEvent e) { 26 | Project project = Objects.requireNonNull(e.getProject()); 27 | boolean enable = e.getPresentation().getIcon() == AllIcons.Actions.PreserveCase; 28 | e.getPresentation().setIcon(enable ? AllIcons.Actions.PreserveCaseSelected : AllIcons.Actions.PreserveCase); 29 | InstanceManager.get(project).bus().propagate().onFilterDuplicate(enable); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/folding/JsFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.folding; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.lang.javascript.psi.JSLiteralExpression; 5 | import com.intellij.openapi.util.Pair; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.util.PsiTreeUtil; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * JavaScript specific translation key folding. 17 | * @author marhali 18 | */ 19 | public class JsFoldingBuilder extends AbstractFoldingBuilder { 20 | @Override 21 | @NotNull List> extractRegions(@NotNull PsiElement root) { 22 | return PsiTreeUtil.findChildrenOfType(root, JSLiteralExpression.class).stream().map(literalExpression -> 23 | Pair.pair(literalExpression.getStringValue(), (PsiElement) literalExpression)) 24 | .collect(Collectors.toList()); 25 | } 26 | 27 | @Override 28 | @Nullable String extractText(@NotNull ASTNode node) { 29 | JSLiteralExpression literalExpression = node.getPsi(JSLiteralExpression.class); 30 | return literalExpression.getStringValue(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/folding/PhpFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.folding; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.openapi.util.Pair; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.util.PsiTreeUtil; 7 | import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Php specific translation key folding. 16 | * @author marhali 17 | */ 18 | public class PhpFoldingBuilder extends AbstractFoldingBuilder { 19 | @Override 20 | @NotNull List> extractRegions(@NotNull PsiElement root) { 21 | return PsiTreeUtil.findChildrenOfType(root, StringLiteralExpression.class).stream().map(literalExpression -> 22 | Pair.pair(literalExpression.getContents(), (PsiElement) literalExpression)) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | @Override 27 | @Nullable String extractText(@NotNull ASTNode node) { 28 | StringLiteralExpression literalExpression = node.getPsi(StringLiteralExpression.class); 29 | return literalExpression.getContents(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/folding/JavaFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.folding; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.openapi.util.Pair; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiLiteralExpression; 7 | import com.intellij.psi.util.PsiTreeUtil; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * Java specific translation key folding. 17 | * @author marhali 18 | */ 19 | public class JavaFoldingBuilder extends AbstractFoldingBuilder { 20 | @Override 21 | @NotNull List> extractRegions(@NotNull PsiElement root) { 22 | return PsiTreeUtil.findChildrenOfType(root, PsiLiteralExpression.class).stream().map(literalExpression -> 23 | Pair.pair(String.valueOf(literalExpression.getValue()), (PsiElement) literalExpression)) 24 | .collect(Collectors.toList()); 25 | } 26 | 27 | @Override 28 | @Nullable String extractText(@NotNull ASTNode node) { 29 | PsiLiteralExpression literalExpression = node.getPsi(PsiLiteralExpression.class); 30 | return String.valueOf(literalExpression.getValue()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/OpenFileAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.fileEditor.FileEditorManager; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.vfs.VirtualFile; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.Objects; 13 | import java.util.ResourceBundle; 14 | 15 | /** 16 | * Plugin action to open a specific file. 17 | * @author marhali 18 | */ 19 | public class OpenFileAction extends AnAction { 20 | private final VirtualFile file; 21 | 22 | public OpenFileAction(VirtualFile file) { 23 | this(file, true); 24 | } 25 | 26 | public OpenFileAction(VirtualFile file, boolean showIcon) { 27 | super(ResourceBundle.getBundle("messages").getString("action.file"), 28 | null, showIcon ? AllIcons.FileTypes.Any_type : null); 29 | this.file = file; 30 | } 31 | 32 | @Override 33 | public void actionPerformed(@NotNull AnActionEvent e) { 34 | Project project = Objects.requireNonNull(e.getProject()); 35 | FileEditorManager.getInstance(project).openFile(file, true, true); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/mapper/AbstractMapperTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.mapper; 2 | 3 | import de.marhali.easyi18n.model.TranslationValue; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Defines test cases for {@link de.marhali.easyi18n.model.TranslationNode} mapping. 9 | * @author marhali 10 | */ 11 | public abstract class AbstractMapperTest { 12 | 13 | protected final String specialCharacters = "Special characters: äü@Öä€/$§;.-?+~#```'' end"; 14 | protected final String arraySimple = "!arr[first;second]"; 15 | protected final String arrayEscaped = "!arr[first\\;element;second element;third\\;element]"; 16 | protected final String leadingSpace = " leading space"; 17 | 18 | @Test 19 | public abstract void testNonSorting(); 20 | 21 | @Test 22 | public abstract void testSorting(); 23 | 24 | @Test 25 | public abstract void testArrays(); 26 | 27 | @Test 28 | public abstract void testSpecialCharacters(); 29 | 30 | @Test 31 | public abstract void testNestedKeys(); 32 | 33 | @Test 34 | public abstract void testNonNestedKeys(); 35 | 36 | @Test 37 | public abstract void testLeadingSpace(); 38 | 39 | @Test 40 | public abstract void testNumbers(); 41 | 42 | protected TranslationValue create(String content) { 43 | return new TranslationValue("en", content); 44 | } 45 | } -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/WildcardRegexMatcherTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n; 2 | 3 | import de.marhali.easyi18n.util.WildcardRegexMatcher; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | /** 9 | * Unit tests for {@link WildcardRegexMatcher}. 10 | * @author marhali 11 | */ 12 | public class WildcardRegexMatcherTest extends WildcardRegexMatcher { 13 | @Test 14 | public void testWildcard() { 15 | Assert.assertTrue(matchWildcardRegex("en.json", "*.json")); 16 | Assert.assertTrue(matchWildcardRegex("de.json", "*.json")); 17 | Assert.assertFalse(matchWildcardRegex("index.html", "*.json")); 18 | 19 | Assert.assertTrue(matchWildcardRegex("en.json", "*.*")); 20 | Assert.assertFalse(matchWildcardRegex("file", "*.*")); 21 | 22 | Assert.assertTrue(matchWildcardRegex("en.txt", "*.???")); 23 | Assert.assertFalse(matchWildcardRegex("en.json", "*.???")); 24 | } 25 | 26 | @Test 27 | public void testRegex() { 28 | Assert.assertTrue(matchWildcardRegex("en.json", "^(en|de)\\.json")); 29 | Assert.assertFalse(matchWildcardRegex("gb.json", "^(en|de)\\.json")); 30 | 31 | Assert.assertTrue(matchWildcardRegex("en.jpg", "^.*\\.(jpg|JPG|gif|GIF)$")); 32 | Assert.assertFalse(matchWildcardRegex("en.json", "^.*\\.(jpg|JPG|gif|GIF)$")); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/TranslationFile.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | /** 9 | * Represents an existing translation file in a context a specific folder strategy. 10 | * @author marhali 11 | */ 12 | public class TranslationFile { 13 | 14 | private final @NotNull VirtualFile virtualFile; 15 | private final @NotNull String locale; 16 | private final @Nullable KeyPath namespace; 17 | 18 | public TranslationFile(@NotNull VirtualFile virtualFile, @NotNull String locale, @Nullable KeyPath namespace) { 19 | this.virtualFile = virtualFile; 20 | this.locale = locale; 21 | this.namespace = namespace; 22 | } 23 | 24 | public @NotNull VirtualFile getVirtualFile() { 25 | return virtualFile; 26 | } 27 | 28 | public @NotNull String getLocale() { 29 | return locale; 30 | } 31 | 32 | public @Nullable KeyPath getNamespace() { 33 | return namespace; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "TranslationFile{" + 39 | "virtualFile=" + virtualFile + 40 | ", locale='" + locale + '\'' + 41 | ", namespace='" + namespace + '\'' + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/action/TranslationUpdate.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model.action; 2 | 3 | import de.marhali.easyi18n.model.Translation; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * Represents an update for a translated i18n key. 8 | * Supports translation creation, manipulation and deletion. 9 | * 10 | * @author marhali 11 | */ 12 | public class TranslationUpdate { 13 | 14 | private final @Nullable Translation origin; 15 | private final @Nullable Translation change; 16 | 17 | public TranslationUpdate(@Nullable Translation origin, @Nullable Translation change) { 18 | this.origin = origin; 19 | this.change = change; 20 | } 21 | 22 | public @Nullable Translation getOrigin() { 23 | return origin; 24 | } 25 | 26 | public @Nullable Translation getChange() { 27 | return change; 28 | } 29 | 30 | public boolean isCreation() { 31 | return this.origin == null; 32 | } 33 | 34 | public boolean isDeletion() { 35 | return this.change == null; 36 | } 37 | 38 | public boolean isKeyChange() { 39 | return this.origin != null && this.change != null && !this.origin.getKey().equals(this.change.getKey()); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "TranslationUpdate{" + 45 | "origin=" + origin + 46 | ", change=" + change + 47 | '}'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/Translation.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * Represents a translation with defined key and locale values. 8 | * 9 | * @author marhali 10 | */ 11 | public class Translation { 12 | 13 | private final @NotNull KeyPath key; 14 | private @Nullable TranslationValue value; 15 | 16 | /** 17 | * Constructs a new translation instance. 18 | * @param key Absolute key path 19 | * @param value Values to set - nullable to indicate removal 20 | */ 21 | public Translation(@NotNull KeyPath key, @Nullable TranslationValue value) { 22 | this.key = key; 23 | this.value = value; 24 | } 25 | 26 | /** 27 | * @return Absolute key path 28 | */ 29 | public @NotNull KeyPath getKey() { 30 | return key; 31 | } 32 | 33 | /** 34 | * @return values - nullable to indicate removal 35 | */ 36 | public @Nullable TranslationValue getValue() { 37 | return value; 38 | } 39 | 40 | /** 41 | * @param value Values to set - nullable to indicate removal 42 | */ 43 | public void setValue(@Nullable TranslationValue value) { 44 | this.value = value; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "Translation{" + 50 | "key=" + key + 51 | ", value=" + value + 52 | '}'; 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/folder/FolderStrategyType.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.folder; 2 | 3 | /** 4 | * Represents all supported folder strategies. 5 | * @author marhali 6 | */ 7 | public enum FolderStrategyType { 8 | SINGLE(SingleFolderStrategy.class, false), 9 | MODULARIZED_LOCALE(ModularLocaleFolderStrategy.class, true), 10 | MODULARIZED_NAMESPACE(ModularNamespaceFolderStrategy.class, true); 11 | 12 | private final Class strategy; 13 | private final boolean namespaceMode; 14 | 15 | /** 16 | * @param strategy Strategy implementation 17 | * @param namespaceMode Does this strategy use namespaces? 18 | */ 19 | FolderStrategyType(Class strategy, boolean namespaceMode) { 20 | this.strategy = strategy; 21 | this.namespaceMode = namespaceMode; 22 | } 23 | 24 | public Class getStrategy() { 25 | return strategy; 26 | } 27 | 28 | public int toIndex() { 29 | int index = 0; 30 | 31 | for(FolderStrategyType strategy : values()) { 32 | if(strategy == this) { 33 | return index; 34 | } 35 | 36 | index++; 37 | } 38 | 39 | throw new NullPointerException(); 40 | } 41 | 42 | public boolean isNamespaceMode() { 43 | return namespaceMode; 44 | } 45 | 46 | public static FolderStrategyType fromIndex(int index) { 47 | return values()[index]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/ProjectSettingsService.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent; 4 | import com.intellij.openapi.components.State; 5 | import com.intellij.openapi.components.Storage; 6 | import com.intellij.openapi.project.Project; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | /** 11 | * Persistent storage for project-specific settings. 12 | * @author marhali 13 | */ 14 | @State( 15 | name = "ProjectSettingsService", 16 | storages = @Storage("easy-i18n.xml") 17 | ) 18 | public class ProjectSettingsService implements PersistentStateComponent { 19 | 20 | public static @NotNull ProjectSettingsService get(@NotNull Project project) { 21 | return project.getService(ProjectSettingsService.class); 22 | } 23 | 24 | private ProjectSettingsState state; 25 | 26 | public ProjectSettingsService() { 27 | this.state = new ProjectSettingsState(); 28 | } 29 | 30 | /** 31 | * Sets the provided configuration and invalidates the merged state. 32 | * @param state New configuration 33 | */ 34 | public void setState(@NotNull ProjectSettingsState state) { 35 | this.state = state; 36 | } 37 | 38 | @Override 39 | public @NotNull ProjectSettingsState getState() { 40 | return state; 41 | } 42 | 43 | @Override 44 | public void loadState(@NotNull ProjectSettingsState state) { 45 | this.state = state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/ProjectSettings.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 4 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 5 | 6 | import de.marhali.easyi18n.settings.presets.NamingConvention; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * API to access the project-specific configuration for this plugin. 12 | * 13 | * @author marhaliu 14 | */ 15 | public interface ProjectSettings { 16 | // Resource Configuration 17 | @Nullable 18 | String getLocalesDirectory(); 19 | 20 | @NotNull 21 | FolderStrategyType getFolderStrategy(); 22 | 23 | @NotNull 24 | ParserStrategyType getParserStrategy(); 25 | 26 | @NotNull 27 | String getFilePattern(); 28 | 29 | boolean isIncludeSubDirs(); 30 | 31 | boolean isSorting(); 32 | 33 | // Editor Configuration 34 | @Nullable 35 | String getNamespaceDelimiter(); 36 | 37 | @NotNull 38 | String getSectionDelimiter(); 39 | 40 | @Nullable 41 | String getContextDelimiter(); 42 | 43 | @Nullable 44 | String getPluralDelimiter(); 45 | 46 | @Nullable 47 | String getDefaultNamespace(); 48 | 49 | @NotNull 50 | String getPreviewLocale(); 51 | 52 | boolean isNestedKeys(); 53 | 54 | boolean isAssistance(); 55 | 56 | // Experimental Configuration 57 | boolean isAlwaysFold(); 58 | 59 | String getFlavorTemplate(); 60 | 61 | @NotNull 62 | NamingConvention getCaseFormat(); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/JavaKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.patterns.PlatformPatterns; 5 | import com.intellij.psi.*; 6 | 7 | 8 | import com.intellij.util.ProcessingContext; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | /** 13 | * Java specific key reference binding. 14 | * @author marhali 15 | */ 16 | public class JavaKeyReferenceContributor extends AbstractKeyReferenceContributor { 17 | 18 | @Override 19 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 20 | registrar.registerReferenceProvider( 21 | PlatformPatterns.psiElement(PsiLiteralExpression.class), 22 | getProvider()); 23 | } 24 | 25 | private PsiReferenceProvider getProvider() { 26 | return new PsiReferenceProvider() { 27 | @Override 28 | public PsiReference @NotNull [] getReferencesByElement( 29 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 30 | 31 | Project project = element.getProject(); 32 | PsiLiteralExpression literalExpression = (PsiLiteralExpression) element; 33 | String value = literalExpression.getValue() instanceof String 34 | ? (String) literalExpression.getValue() 35 | : null; 36 | 37 | return getReferences(project, element, value); 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/PhpKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.patterns.PlatformPatterns; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiReference; 7 | import com.intellij.psi.PsiReferenceProvider; 8 | import com.intellij.psi.PsiReferenceRegistrar; 9 | import com.intellij.util.ProcessingContext; 10 | import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; 11 | 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | /** 15 | * Php specific key reference binding 16 | */ 17 | public class PhpKeyReferenceContributor extends AbstractKeyReferenceContributor { 18 | @Override 19 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 20 | registrar.registerReferenceProvider( 21 | PlatformPatterns.psiElement(StringLiteralExpression.class), 22 | getProvider()); 23 | } 24 | 25 | private PsiReferenceProvider getProvider() { 26 | return new PsiReferenceProvider() { 27 | @Override 28 | public PsiReference @NotNull [] getReferencesByElement( 29 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 30 | 31 | Project project = element.getProject(); 32 | StringLiteralExpression literalExpression = (StringLiteralExpression) element; 33 | return getReferences(project, element, literalExpression.getContents()); 34 | } 35 | }; 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/TestSettingsState.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e; 2 | 3 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 4 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 5 | import de.marhali.easyi18n.settings.presets.DefaultPreset; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Settings base for end-to-end tests. 12 | * @author marhali 13 | */ 14 | public class TestSettingsState extends DefaultPreset { 15 | 16 | private final String localesDirectory; 17 | private final FolderStrategyType folderStrategy; 18 | private final ParserStrategyType parserStrategy; 19 | 20 | public TestSettingsState(String localesDirectory, FolderStrategyType folderStrategy, ParserStrategyType parserStrategy) { 21 | this.localesDirectory = localesDirectory; 22 | this.folderStrategy = folderStrategy; 23 | this.parserStrategy = parserStrategy; 24 | } 25 | 26 | 27 | @Override 28 | public @Nullable String getLocalesDirectory() { 29 | return localesDirectory; 30 | } 31 | 32 | @Override 33 | public @NotNull FolderStrategyType getFolderStrategy() { 34 | return folderStrategy; 35 | } 36 | 37 | @Override 38 | public @NotNull ParserStrategyType getParserStrategy() { 39 | return parserStrategy; 40 | } 41 | 42 | @Override 43 | public @NotNull String getFilePattern() { 44 | return "*.*"; 45 | } 46 | 47 | @Override 48 | public boolean isSorting() { 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/XmlKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.patterns.PlatformPatterns; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiReference; 7 | import com.intellij.psi.PsiReferenceProvider; 8 | import com.intellij.psi.PsiReferenceRegistrar; 9 | import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl; 10 | import com.intellij.util.ProcessingContext; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | /** 14 | * Xml specific key reference binding 15 | * @author adeptius 16 | */ 17 | public class XmlKeyReferenceContributor extends AbstractKeyReferenceContributor { 18 | @Override 19 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 20 | registrar.registerReferenceProvider( 21 | PlatformPatterns.psiElement(XmlAttributeValueImpl.class), 22 | getProvider()); 23 | } 24 | 25 | private PsiReferenceProvider getProvider() { 26 | return new PsiReferenceProvider() { 27 | @Override 28 | public PsiReference @NotNull [] getReferencesByElement( 29 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 30 | 31 | Project project = element.getProject(); 32 | XmlAttributeValueImpl literalExpression = (XmlAttributeValueImpl) element; 33 | String value = literalExpression.getValue(); 34 | return getReferences(project, element, value); 35 | } 36 | }; 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/JsKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.lang.javascript.psi.JSLiteralExpression; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiReference; 8 | import com.intellij.psi.PsiReferenceProvider; 9 | import com.intellij.psi.PsiReferenceRegistrar; 10 | import com.intellij.util.ProcessingContext; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | /** 14 | * JavaScript specific translation-key reference binding. 15 | * @author marhali 16 | */ 17 | public class JsKeyReferenceContributor extends AbstractKeyReferenceContributor { 18 | @Override 19 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 20 | registrar.registerReferenceProvider( 21 | PlatformPatterns.psiElement(JSLiteralExpression.class), 22 | getProvider()); 23 | } 24 | 25 | private PsiReferenceProvider getProvider() { 26 | return new PsiReferenceProvider() { 27 | @Override 28 | public PsiReference @NotNull [] getReferencesByElement( 29 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 30 | 31 | Project project = element.getProject(); 32 | JSLiteralExpression literalExpression = (JSLiteralExpression) element; 33 | String value = literalExpression.getStringValue(); 34 | 35 | return getReferences(project, element, value); 36 | } 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = de.marhali.easyi18n 4 | pluginName = easy-i18n 5 | pluginRepositoryUrl = https://github.com/marhali/easy-i18n 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 4.9.0 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 242 11 | 12 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 13 | platformType = IU 14 | platformVersion = 2025.1.5 15 | 16 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 17 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 18 | platformPlugins = com.jetbrains.php:242.23726.16 19 | # Example: platformBundledPlugins = com.intellij.java 20 | platformBundledPlugins = org.jetbrains.kotlin, JavaScript 21 | # Example: platformBundledModules = intellij.spellchecker 22 | platformBundledModules = 23 | 24 | # Gradle Releases -> https://github.com/gradle/gradle/releases 25 | gradleVersion = 9.0.0 26 | 27 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 28 | kotlin.stdlib.default.dependency = false 29 | 30 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 31 | org.gradle.configuration-cache = true 32 | 33 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 34 | org.gradle.caching = true -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/folding/KtFoldingBuilder.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.folding; 2 | 3 | import com.intellij.lang.ASTNode; 4 | import com.intellij.openapi.util.Pair; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.util.PsiTreeUtil; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | import org.jetbrains.kotlin.psi.KtStringTemplateEntry; 11 | import org.jetbrains.kotlin.psi.KtStringTemplateExpression; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Kotlin specific translation-key folding. 18 | * @author marhali 19 | */ 20 | public class KtFoldingBuilder extends AbstractFoldingBuilder { 21 | @Override 22 | @NotNull List> extractRegions(@NotNull PsiElement root) { 23 | List> regions = new ArrayList<>(); 24 | 25 | for (KtStringTemplateExpression templateExpression : PsiTreeUtil.findChildrenOfType(root, KtStringTemplateExpression.class)) { 26 | for (KtStringTemplateEntry entry : templateExpression.getEntries()) { 27 | regions.add(Pair.pair(entry.getText(), templateExpression)); 28 | break; 29 | } 30 | } 31 | 32 | return regions; 33 | } 34 | 35 | @Override 36 | @Nullable String extractText(@NotNull ASTNode node) { 37 | KtStringTemplateExpression templateExpression = node.getPsi(KtStringTemplateExpression.class); 38 | 39 | for (KtStringTemplateEntry entry : templateExpression.getEntries()) { 40 | return entry.getText(); 41 | } 42 | 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/dialog/EditDialog.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.dialog; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.ui.DialogWrapper; 5 | 6 | import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor; 7 | import de.marhali.easyi18n.model.action.TranslationDelete; 8 | import de.marhali.easyi18n.model.action.TranslationUpdate; 9 | import de.marhali.easyi18n.model.Translation; 10 | 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import javax.swing.*; 15 | 16 | /** 17 | * Dialog to edit or delete an existing translation. 18 | * @author marhali 19 | */ 20 | public class EditDialog extends TranslationDialog { 21 | 22 | /** 23 | * Constructs a new edit dialog with the provided translation 24 | * @param project Opened project 25 | * @param origin Translation to edit 26 | */ 27 | public EditDialog(@NotNull Project project, @NotNull Translation origin) { 28 | super(project, origin); 29 | 30 | setTitle(bundle.getString("action.edit")); 31 | } 32 | 33 | @Override 34 | protected Action @NotNull [] createLeftSideActions() { 35 | return new Action[]{ new DeleteActionDescriptor(this) }; 36 | } 37 | 38 | @Override 39 | protected @Nullable TranslationUpdate handleExit(int exitCode) { 40 | switch (exitCode) { 41 | case DialogWrapper.OK_EXIT_CODE: 42 | return new TranslationUpdate(origin, getState()); 43 | case DeleteActionDescriptor.EXIT_CODE: 44 | return new TranslationDelete(origin); 45 | default: 46 | return null; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/ParserStrategyType.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser; 2 | 3 | import de.marhali.easyi18n.io.parser.json.JsonParserStrategy; 4 | import de.marhali.easyi18n.io.parser.json5.Json5ParserStrategy; 5 | import de.marhali.easyi18n.io.parser.properties.PropertiesParserStrategy; 6 | import de.marhali.easyi18n.io.parser.yaml.YamlParserStrategy; 7 | 8 | /** 9 | * Represents all supported file parser strategies. 10 | * @author marhali 11 | */ 12 | public enum ParserStrategyType { 13 | JSON(JsonParserStrategy.class), 14 | JSON5(Json5ParserStrategy.class), 15 | YAML(YamlParserStrategy.class), 16 | YML(YamlParserStrategy.class), 17 | PROPERTIES(PropertiesParserStrategy.class), 18 | ARB(JsonParserStrategy.class); 19 | 20 | private final Class strategy; 21 | 22 | ParserStrategyType(Class strategy) { 23 | this.strategy = strategy; 24 | } 25 | 26 | public Class getStrategy() { 27 | return strategy; 28 | } 29 | 30 | public String getFileExtension() { 31 | return toString().toLowerCase(); 32 | } 33 | 34 | public String getExampleFilePattern() { 35 | return "*." + this.getFileExtension(); 36 | } 37 | 38 | public int toIndex() { 39 | int index = 0; 40 | 41 | for(ParserStrategyType strategy : values()) { 42 | if(strategy == this) { 43 | return index; 44 | } 45 | 46 | index++; 47 | } 48 | 49 | throw new NullPointerException(); 50 | } 51 | 52 | public static ParserStrategyType fromIndex(int index) { 53 | return values()[index]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/de.marhali.easyi18n-javascript.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 17 | 18 | 22 | 23 | 27 | 28 | 32 | 33 | 34 | de.marhali.easyi18n.assistance.intention.JsTranslationIntention 35 | 36 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/ProjectSettingsConfigurable.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import com.intellij.openapi.project.Project; 5 | 6 | import de.marhali.easyi18n.InstanceManager; 7 | 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | 12 | /** 13 | * IDE settings panel for this plugin 14 | * @author marhali 15 | */ 16 | public class ProjectSettingsConfigurable implements Configurable { 17 | 18 | private final Project project; 19 | 20 | private ProjectSettingsComponent component; 21 | 22 | public ProjectSettingsConfigurable(Project project) { 23 | this.project = project; 24 | } 25 | 26 | @Override 27 | public String getDisplayName() { 28 | return "Easy I18n"; 29 | } 30 | 31 | @Override 32 | public @Nullable JComponent createComponent() { 33 | component = new ProjectSettingsComponent(project); 34 | component.setState(ProjectSettingsService.get(project).getState()); 35 | return component.getMainPanel(); 36 | } 37 | 38 | @Override 39 | public boolean isModified() { 40 | ProjectSettingsState originState = ProjectSettingsService.get(project).getState(); 41 | return !originState.equals(component.getState()); 42 | } 43 | 44 | @Override 45 | public void apply() { 46 | ProjectSettingsService.get(project).setState(component.getState()); 47 | InstanceManager.get(project).reload(); 48 | } 49 | 50 | @Override 51 | public void reset() { 52 | component.setState(ProjectSettingsService.get(project).getState()); 53 | } 54 | 55 | @Override 56 | public void disposeUIResources() { 57 | component = null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/settings/NamingConventionTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase; 4 | import de.marhali.easyi18n.settings.presets.NamingConvention; 5 | 6 | import java.io.IOException; 7 | 8 | public class NamingConventionTest extends BasePlatformTestCase { 9 | @Override 10 | protected void setUp() throws Exception { 11 | super.setUp(); 12 | } 13 | 14 | public void testConvertToNamingConvention() throws IOException { 15 | assertEquals("helloWorld", NamingConvention.convertKeyToConvention("Hello World", NamingConvention.CAMEL_CASE)); 16 | assertEquals("hello_world", NamingConvention.convertKeyToConvention("Hello World", NamingConvention.SNAKE_CASE)); 17 | assertEquals("HelloWorld", NamingConvention.convertKeyToConvention("Hello World", NamingConvention.PASCAL_CASE)); 18 | assertEquals("HELLO_WORLD", NamingConvention.convertKeyToConvention("Hello World", NamingConvention.SNAKE_CASE_UPPERCASE)); 19 | } 20 | 21 | public void testGetEnumNames() throws Exception { 22 | String[] expected = {"Camel Case", "Pascal Case", "Snake Case", "Snake Case (Uppercase)"}; 23 | String[] actual = NamingConvention.getEnumNames(); 24 | assertEquals(expected.length, actual.length); 25 | } 26 | 27 | 28 | public void testFromString() { 29 | assertEquals(NamingConvention.CAMEL_CASE, NamingConvention.fromString("Camel Case")); 30 | assertEquals(NamingConvention.PASCAL_CASE, NamingConvention.fromString("Pascal Case")); 31 | assertEquals(NamingConvention.SNAKE_CASE, NamingConvention.fromString("Snake Case")); 32 | assertEquals(NamingConvention.SNAKE_CASE_UPPERCASE, NamingConvention.fromString("Snake Case (Uppercase)")); 33 | assertEquals(NamingConvention.CAMEL_CASE, NamingConvention.fromString("Invalid Input")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/KtKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.patterns.PlatformPatterns; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiReference; 7 | import com.intellij.psi.PsiReferenceProvider; 8 | import com.intellij.psi.PsiReferenceRegistrar; 9 | import com.intellij.util.ProcessingContext; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry; 12 | import org.jetbrains.kotlin.psi.KtStringTemplateExpression; 13 | 14 | import java.util.Arrays; 15 | import java.util.Optional; 16 | 17 | /** 18 | * Kotlin specific translation-key reference binding. 19 | * @author marhali 20 | */ 21 | public class KtKeyReferenceContributor extends AbstractKeyReferenceContributor { 22 | @Override 23 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 24 | registrar.registerReferenceProvider( 25 | PlatformPatterns.psiElement().inside(KtStringTemplateExpression.class), 26 | getProvider()); 27 | } 28 | 29 | private PsiReferenceProvider getProvider() { 30 | return new PsiReferenceProvider() { 31 | @Override 32 | public PsiReference @NotNull [] getReferencesByElement( 33 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 34 | 35 | Optional targetElement = Arrays.stream(element.getChildren()).filter(child -> 36 | child instanceof KtLiteralStringTemplateEntry).findAny(); 37 | 38 | if(targetElement.isEmpty()) { 39 | return PsiReference.EMPTY_ARRAY; 40 | } 41 | 42 | return getReferences(element.getProject(), element, targetElement.get().getText()); 43 | } 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json5/Json5ArrayMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json5; 2 | 3 | import de.marhali.easyi18n.io.parser.ArrayMapper; 4 | import de.marhali.easyi18n.util.StringUtil; 5 | import de.marhali.json5.Json5; 6 | import de.marhali.json5.Json5Array; 7 | import de.marhali.json5.Json5Primitive; 8 | 9 | import org.apache.commons.lang3.math.NumberUtils; 10 | 11 | import java.io.IOException; 12 | 13 | /** 14 | * Map json5 array values. 15 | * @author marhali 16 | */ 17 | public class Json5ArrayMapper extends ArrayMapper { 18 | 19 | private static final Json5 JSON5 = Json5.builder(builder -> 20 | builder.allowInvalidSurrogate().quoteSingle().indentFactor(0).build()); 21 | 22 | public static String read(Json5Array array) { 23 | return read(array.iterator(), (jsonElement -> { 24 | try { 25 | return jsonElement.isJson5Array() || jsonElement.isJson5Object() 26 | ? "\\" + JSON5.serialize(jsonElement) 27 | : jsonElement.getAsString(); 28 | } catch (IOException e) { 29 | throw new AssertionError(e.getMessage(), e.getCause()); 30 | } 31 | })); 32 | } 33 | 34 | public static Json5Array write(String concat) { 35 | Json5Array array = new Json5Array(); 36 | 37 | write(concat, (element) -> { 38 | if(element.startsWith("\\")) { 39 | array.add(JSON5.parse(element.replace("\\", ""))); 40 | } else { 41 | if(StringUtil.isHexString(element)) { 42 | array.add(Json5Primitive.of(element, true)); 43 | } else if(NumberUtils.isCreatable(element)) { 44 | array.add(Json5Primitive.of(NumberUtils.createNumber(element))); 45 | } else { 46 | array.add(Json5Primitive.of(element)); 47 | } 48 | } 49 | }); 50 | 51 | return array; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/folder/SingleFolderStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.folder; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 6 | import de.marhali.easyi18n.model.TranslationData; 7 | import de.marhali.easyi18n.model.TranslationFile; 8 | import de.marhali.easyi18n.settings.ProjectSettings; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.io.IOException; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Single directory translation folder strategy. 18 | * Every child is recognized as a file for a specific language. 19 | * Directory => en.file, de.file, fr.file 20 | * 21 | * @author marhali 22 | */ 23 | public class SingleFolderStrategy extends FolderStrategy { 24 | 25 | public SingleFolderStrategy(@NotNull ProjectSettings settings) { 26 | super(settings); 27 | } 28 | 29 | @Override 30 | public @NotNull List analyzeFolderStructure(@NotNull VirtualFile localesDirectory) { 31 | List files = new ArrayList<>(); 32 | 33 | for (VirtualFile file : localesDirectory.getChildren()) { 34 | if (super.isFileRelevant(file)) { 35 | files.add(new TranslationFile(file, file.getNameWithoutExtension(), null)); 36 | } 37 | } 38 | 39 | return files; 40 | } 41 | 42 | @Override 43 | public @NotNull List constructFolderStructure( 44 | @NotNull String localesPath, @NotNull ParserStrategyType type, 45 | @NotNull TranslationData data) throws IOException { 46 | 47 | List files = new ArrayList<>(); 48 | 49 | for (String locale : data.getLocales()) { 50 | VirtualFile vf = super.constructFile(localesPath, 51 | locale + "." + type.getFileExtension()); 52 | 53 | files.add(new TranslationFile(vf, locale, null)); 54 | } 55 | 56 | return files; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out the current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v4 37 | 38 | # Set up the Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: zulu 43 | java-version: 21 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/actions/setup-gradle@v4 48 | with: 49 | cache-read-only: true 50 | 51 | # Run IDEA prepared for UI testing 52 | - name: Run IDE 53 | run: ${{ matrix.runIde }} 54 | 55 | # Wait for IDEA to be started 56 | - name: Health Check 57 | uses: jtalk/url-health-check-action@v4 58 | with: 59 | url: http://127.0.0.1:8082 60 | max-attempts: 15 61 | retry-delay: 30s 62 | 63 | # Run tests 64 | - name: Tests 65 | run: ./gradlew test -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/model/TranslationValue.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.model; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | /** 12 | * Represents the set values behind a specific translation. 13 | * @author marhali 14 | */ 15 | public class TranslationValue { 16 | 17 | private @NotNull Map localeValues; 18 | 19 | public TranslationValue() { 20 | this.localeValues = new HashMap<>(); 21 | } 22 | 23 | public TranslationValue(@NotNull String locale, @NotNull String content) { 24 | this(); 25 | localeValues.put(locale, content); 26 | } 27 | 28 | public Set> getEntries() { 29 | return this.localeValues.entrySet(); 30 | } 31 | 32 | public Collection getLocaleContents() { 33 | return this.localeValues.values(); 34 | } 35 | 36 | public void setLocaleValues(@NotNull Map localeValues) { 37 | this.localeValues = localeValues; 38 | } 39 | 40 | public @Nullable String get(@NotNull String locale) { 41 | return this.localeValues.get(locale); 42 | } 43 | 44 | public void put(@NotNull String locale, @NotNull String content) { 45 | this.localeValues.put(locale, content); 46 | } 47 | 48 | public void remove(@NotNull String locale) { 49 | this.localeValues.remove(locale); 50 | } 51 | 52 | public boolean containsLocale(@NotNull String locale) { 53 | return this.localeValues.containsKey(locale); 54 | } 55 | 56 | public int size() { 57 | return this.localeValues.size(); 58 | } 59 | 60 | public void clear() { 61 | this.localeValues.clear(); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "TranslationValue{" + 67 | "localeValues=" + localeValues + 68 | '}'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/settings/ProjectSettingsServiceTest.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase; 4 | import com.intellij.util.xmlb.XmlSerializerUtil; 5 | 6 | import de.marhali.easyi18n.settings.presets.DefaultPreset; 7 | import de.marhali.easyi18n.settings.presets.NamingConvention; 8 | 9 | /** 10 | * Tests for the project settings service itself. 11 | * 12 | * @author marhali 13 | */ 14 | public class ProjectSettingsServiceTest extends BasePlatformTestCase { 15 | 16 | @Override 17 | protected void setUp() throws Exception { 18 | super.setUp(); 19 | ProjectSettingsService.get(getProject()).setState(new ProjectSettingsState()); 20 | } 21 | 22 | public void testSettingsDefaultPreset() { 23 | ProjectSettingsState state = ProjectSettingsService.get(getProject()).getState(); 24 | assertEquals(new ProjectSettingsState(new DefaultPreset()), state); 25 | } 26 | 27 | public void testPersistenceState() { 28 | ProjectSettingsState previous = new ProjectSettingsState(new SettingsTestPreset()); 29 | ProjectSettingsState after = XmlSerializerUtil.createCopy(previous); 30 | assertEquals(previous, after); 31 | } 32 | 33 | public void testPersistenceSingle() { 34 | ProjectSettingsState previous = new ProjectSettingsState(); 35 | previous.setLocalesDirectory("mySinglePropTest"); 36 | 37 | ProjectSettingsState after = XmlSerializerUtil.createCopy(previous); 38 | assertEquals("mySinglePropTest", after.getLocalesDirectory()); 39 | } 40 | 41 | public void testPersistenceFormatCase() { 42 | ProjectSettingsState previous = new ProjectSettingsState(); 43 | assertEquals(previous.getCaseFormat(), NamingConvention.CAMEL_CASE); 44 | previous.setCaseFormat(NamingConvention.SNAKE_CASE); 45 | ProjectSettingsState after = XmlSerializerUtil.createCopy(previous); 46 | assertEquals(after.getCaseFormat(), NamingConvention.SNAKE_CASE); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/ArrayMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser; 2 | 3 | import de.marhali.easyi18n.util.StringUtil; 4 | 5 | import org.apache.commons.text.StringEscapeUtils; 6 | 7 | import java.text.MessageFormat; 8 | import java.util.Iterator; 9 | import java.util.function.Consumer; 10 | import java.util.function.Function; 11 | import java.util.regex.Pattern; 12 | 13 | /** 14 | * Simple array support for translation values. 15 | * Some i18n systems allows the user to define array values for some translations. 16 | * We support array values by wrapping them into: '!arr[valueA;valueB]'. 17 | * 18 | * @author marhali 19 | */ 20 | public abstract class ArrayMapper { 21 | static final String PREFIX = "!arr["; 22 | static final String SUFFIX = "]"; 23 | static final char DELIMITER = ';'; 24 | 25 | public static final String SPLITERATOR_REGEX = 26 | MessageFormat.format("(? String read(Iterator elements, Function stringFactory) { 29 | StringBuilder builder = new StringBuilder(PREFIX); 30 | 31 | int i = 0; 32 | while(elements.hasNext()) { 33 | if(i > 0) { 34 | builder.append(DELIMITER); 35 | } 36 | 37 | String value = stringFactory.apply(elements.next()); 38 | 39 | builder.append(StringUtil.escapeControls( 40 | value.replace(String.valueOf(DELIMITER), "\\" + DELIMITER), true)); 41 | 42 | i++; 43 | } 44 | 45 | builder.append(SUFFIX); 46 | return builder.toString(); 47 | } 48 | 49 | protected static void write(String concat, Consumer writeElement) { 50 | concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length()); 51 | 52 | for(String element : concat.split(SPLITERATOR_REGEX)) { 53 | element = element.replace("\\" + DELIMITER, String.valueOf(DELIMITER)); 54 | writeElement.accept(StringEscapeUtils.unescapeJava(element)); 55 | } 56 | } 57 | 58 | public static boolean isArray(String concat) { 59 | return concat != null && concat.startsWith(PREFIX) && concat.endsWith(SUFFIX); 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Layer 1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/DataBus.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n; 2 | 3 | import de.marhali.easyi18n.model.bus.BusListener; 4 | import de.marhali.easyi18n.model.TranslationData; 5 | import de.marhali.easyi18n.model.KeyPath; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | /** 14 | * Eventbus which is mandatory to distribute changes to the participating components. 15 | * For user interface related components the {@link FilteredDataBus} has a builtin solution to apply all relevant filters. 16 | * @author marhali 17 | */ 18 | public class DataBus { 19 | 20 | private final Set listener; 21 | 22 | protected DataBus() { 23 | this.listener = new HashSet<>(); 24 | } 25 | 26 | /** 27 | * Adds a participant to the event bus. Every participant needs to be added manually. 28 | * @param listener Bus listener 29 | */ 30 | public void addListener(BusListener listener) { 31 | this.listener.add(listener); 32 | } 33 | 34 | /** 35 | * Fires the called events on the returned prototype. 36 | * The event will be distributed to all participants which were registered at execution time. 37 | * @return Listener prototype 38 | */ 39 | public BusListener propagate() { 40 | return new BusListener() { 41 | @Override 42 | public void onFilterDuplicate(boolean filter) { 43 | listener.forEach(li -> li.onFilterDuplicate(filter)); 44 | } 45 | 46 | @Override 47 | public void onFilterIncomplete(boolean filter) { 48 | listener.forEach(li -> li.onFilterIncomplete(filter)); 49 | } 50 | 51 | @Override 52 | public void onFocusKey(@NotNull KeyPath key) { 53 | listener.forEach(li -> li.onFocusKey(key)); 54 | } 55 | 56 | @Override 57 | public void onSearchQuery(@Nullable String query) { 58 | listener.forEach(li -> li.onSearchQuery(query)); 59 | } 60 | 61 | @Override 62 | public void onUpdateData(@NotNull TranslationData data) { 63 | listener.forEach(li -> li.onUpdateData(data)); 64 | } 65 | }; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json/JsonParserStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonObject; 6 | 7 | import com.google.gson.JsonSyntaxException; 8 | import com.intellij.openapi.vfs.VirtualFile; 9 | 10 | import de.marhali.easyi18n.exception.SyntaxException; 11 | import de.marhali.easyi18n.io.parser.ParserStrategy; 12 | import de.marhali.easyi18n.model.*; 13 | import de.marhali.easyi18n.settings.ProjectSettings; 14 | 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.io.InputStreamReader; 18 | import java.io.Reader; 19 | import java.util.Objects; 20 | 21 | /** 22 | * Json file format parser strategy. 23 | * @author marhali 24 | */ 25 | public class JsonParserStrategy extends ParserStrategy { 26 | 27 | private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); 28 | 29 | public JsonParserStrategy(@NotNull ProjectSettings settings) { 30 | super(settings); 31 | } 32 | 33 | @Override 34 | public void read(@NotNull TranslationFile file, @NotNull TranslationData data) throws Exception { 35 | data.addLocale(file.getLocale()); 36 | 37 | VirtualFile vf = file.getVirtualFile(); 38 | TranslationNode targetNode = super.getOrCreateTargetNode(file, data); 39 | 40 | try(Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) { 41 | JsonObject input; 42 | 43 | try { 44 | input = GSON.fromJson(reader, JsonObject.class); 45 | } catch (JsonSyntaxException ex) { 46 | throw new SyntaxException(ex.getMessage(), file); 47 | } 48 | 49 | if(input != null) { // @input is null if file is completely empty 50 | JsonMapper.read(file.getLocale(), input, targetNode); 51 | } 52 | } 53 | } 54 | 55 | @Override 56 | public @NotNull String write(@NotNull TranslationData data, @NotNull TranslationFile file) throws Exception { 57 | TranslationNode targetNode = super.getTargetNode(data, file); 58 | 59 | JsonObject output = new JsonObject(); 60 | JsonMapper.write(file.getLocale(), output, Objects.requireNonNull(targetNode)); 61 | 62 | return GSON.toJson(output); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/AbstractKeyReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.psi.PsiElement; 5 | import com.intellij.psi.PsiReference; 6 | import com.intellij.psi.PsiReferenceContributor; 7 | import de.marhali.easyi18n.InstanceManager; 8 | import de.marhali.easyi18n.assistance.OptionalAssistance; 9 | import de.marhali.easyi18n.model.KeyPath; 10 | import de.marhali.easyi18n.model.Translation; 11 | import de.marhali.easyi18n.model.TranslationValue; 12 | import de.marhali.easyi18n.settings.ProjectSettings; 13 | import de.marhali.easyi18n.settings.ProjectSettingsService; 14 | import de.marhali.easyi18n.util.KeyPathConverter; 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | /** 19 | * Language specific translation key reference contributor. 20 | * @author marhali 21 | */ 22 | abstract class AbstractKeyReferenceContributor extends PsiReferenceContributor implements OptionalAssistance { 23 | /** 24 | * Searches for relevant translation-key references 25 | * @param project Opened project 26 | * @param element Targeted element 27 | * @param text Designated translation key 28 | * @return Matched translation-key reference(s) 29 | */ 30 | protected @NotNull PsiReference[] getReferences( 31 | @NotNull Project project, @NotNull PsiElement element, @Nullable String text) { 32 | 33 | if(text == null || text.isEmpty() || !isAssistance(project)) { 34 | return PsiReference.EMPTY_ARRAY; 35 | } 36 | 37 | ProjectSettings settings = ProjectSettingsService.get(project).getState(); 38 | KeyPathConverter converter = new KeyPathConverter(settings); 39 | 40 | // TODO: We should provide multiple references if not a leaf node was provided (contextual / plurals support) 41 | 42 | KeyPath path = converter.fromString(text); 43 | TranslationValue values = InstanceManager.get(project).store().getData().getTranslation(path); 44 | 45 | if(values == null) { // We only reference valid and existing translations 46 | return PsiReference.EMPTY_ARRAY; 47 | } 48 | 49 | return new PsiReference[] { 50 | new PsiKeyReference(converter, new Translation(path, values), element) 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/tabs/renderer/TableRenderer.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.tabs.renderer; 2 | 3 | import com.intellij.ui.JBColor; 4 | 5 | import javax.swing.*; 6 | import javax.swing.table.DefaultTableCellRenderer; 7 | import java.awt.*; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | /** 12 | * Similar to {@link DefaultTableCellRenderer} but will mark the first column red if any column is empty. 13 | * @author marhali 14 | */ 15 | public class TableRenderer extends DefaultTableCellRenderer { 16 | 17 | @Override 18 | public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 19 | Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 20 | 21 | // Always reset color 22 | component.setForeground(null); 23 | 24 | if(column != 0) { 25 | return component; 26 | } 27 | 28 | if(missesValues(row, table)) { 29 | component.setForeground(JBColor.RED); 30 | } else if(hasDuplicates(row, table)) { 31 | component.setForeground(JBColor.ORANGE); 32 | } 33 | 34 | return component; 35 | } 36 | 37 | private boolean missesValues(int row, JTable table) { 38 | int columns = table.getColumnCount(); 39 | 40 | for(int i = 1; i < columns; i++) { 41 | Object value = table.getValueAt(row, i); 42 | 43 | if(value == null || value.toString().isEmpty()) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | private boolean hasDuplicates(int checkRow, JTable table) { 52 | int columns = table.getColumnCount(); 53 | int rows = table.getRowCount(); 54 | 55 | Set contents = new HashSet<>(); 56 | for(int column = 1; column < columns; column++) { 57 | contents.add(String.valueOf(table.getValueAt(checkRow, column))); 58 | } 59 | 60 | for(int row = 1; row < rows; row++) { 61 | if(row == checkRow) { 62 | continue; 63 | } 64 | 65 | for(int column = 1; column < columns; column++) { 66 | if(contents.contains(String.valueOf(table.getValueAt(row, column)))) { 67 | return true; 68 | } 69 | } 70 | } 71 | 72 | return false; 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/presets/VueI18nPreset.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings.presets; 2 | 3 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 4 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 5 | import de.marhali.easyi18n.settings.ProjectSettings; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * Preset for Vue.js - vue-i18n 11 | * @author marhali 12 | */ 13 | public class VueI18nPreset implements ProjectSettings { 14 | @Override 15 | public @Nullable String getLocalesDirectory() { 16 | return null; 17 | } 18 | 19 | @Override 20 | public @NotNull FolderStrategyType getFolderStrategy() { 21 | return FolderStrategyType.SINGLE; 22 | } 23 | 24 | @Override 25 | public @NotNull ParserStrategyType getParserStrategy() { 26 | return ParserStrategyType.JSON; 27 | } 28 | 29 | @Override 30 | public @NotNull String getFilePattern() { 31 | return "*.json"; 32 | } 33 | 34 | @Override 35 | public boolean isIncludeSubDirs() { 36 | return false; 37 | } 38 | 39 | @Override 40 | public boolean isSorting() { 41 | return true; 42 | } 43 | 44 | @Override 45 | public @Nullable String getNamespaceDelimiter() { 46 | return null; 47 | } 48 | 49 | @Override 50 | public @NotNull String getSectionDelimiter() { 51 | return "."; 52 | } 53 | 54 | @Override 55 | public @Nullable String getContextDelimiter() { 56 | return null; 57 | } 58 | 59 | @Override 60 | public @Nullable String getPluralDelimiter() { 61 | return null; 62 | } 63 | 64 | @Override 65 | public @Nullable String getDefaultNamespace() { 66 | return null; 67 | } 68 | 69 | @Override 70 | public @NotNull String getPreviewLocale() { 71 | return "en"; 72 | } 73 | 74 | @Override 75 | public boolean isNestedKeys() { 76 | return true; 77 | } 78 | 79 | @Override 80 | public boolean isAssistance() { 81 | return true; 82 | } 83 | 84 | @Override 85 | public boolean isAlwaysFold() { 86 | return false; 87 | } 88 | 89 | @Override 90 | public String getFlavorTemplate() { 91 | return "$i18n.t"; 92 | } 93 | 94 | @Override 95 | public @NotNull NamingConvention getCaseFormat() { 96 | return NamingConvention.CAMEL_CASE; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/SearchAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.actionSystem.Presentation; 6 | import com.intellij.openapi.actionSystem.ex.CustomComponentAction; 7 | import com.intellij.ui.components.JBTextField; 8 | import com.intellij.util.ui.JBUI; 9 | 10 | import org.jdesktop.swingx.prompt.PromptSupport; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import javax.swing.*; 14 | import java.awt.*; 15 | import java.awt.event.KeyAdapter; 16 | import java.awt.event.KeyEvent; 17 | import java.util.ResourceBundle; 18 | import java.util.function.Consumer; 19 | 20 | /** 21 | * Search translations by key action. 22 | * @author marhali 23 | */ 24 | public class SearchAction extends AnAction implements CustomComponentAction { 25 | 26 | private final Consumer searchCallback; 27 | private JBTextField textField; 28 | 29 | public SearchAction(@NotNull Consumer searchCallback) { 30 | super(ResourceBundle.getBundle("messages").getString("action.search")); 31 | this.searchCallback = searchCallback; 32 | } 33 | 34 | @Override 35 | public void actionPerformed(@NotNull AnActionEvent e) {} // Should never be called 36 | 37 | public void actionPerformed() { 38 | searchCallback.accept(textField == null ? "" : textField.getText()); 39 | } 40 | 41 | @Override 42 | public @NotNull JComponent createCustomComponent(@NotNull Presentation presentation, @NotNull String place) { 43 | textField = new JBTextField(); 44 | textField.setPreferredSize(new Dimension(160, 25)); 45 | PromptSupport.setPrompt(ResourceBundle.getBundle("messages").getString("action.search"), textField); 46 | 47 | textField.addKeyListener(handleKeyListener()); 48 | textField.setBorder(JBUI.Borders.empty()); 49 | 50 | JPanel panel = new JPanel(new BorderLayout()); 51 | panel.add(textField, BorderLayout.CENTER); 52 | 53 | return panel; 54 | } 55 | 56 | private KeyAdapter handleKeyListener() { 57 | return new KeyAdapter() { 58 | @Override 59 | public void keyPressed(KeyEvent e) { 60 | if(e.getKeyCode() == KeyEvent.VK_ENTER) { 61 | e.consume(); 62 | actionPerformed(); 63 | } 64 | } 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/presets/ReactI18NextPreset.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings.presets; 2 | 3 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 4 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 5 | import de.marhali.easyi18n.settings.ProjectSettings; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Preset for React - i18n-next 12 | * @author marhali 13 | */ 14 | public class ReactI18NextPreset implements ProjectSettings { 15 | @Override 16 | public @Nullable String getLocalesDirectory() { 17 | return null; 18 | } 19 | 20 | @Override 21 | public @NotNull FolderStrategyType getFolderStrategy() { 22 | return FolderStrategyType.MODULARIZED_NAMESPACE; 23 | } 24 | 25 | @Override 26 | public @NotNull ParserStrategyType getParserStrategy() { 27 | return ParserStrategyType.JSON; 28 | } 29 | 30 | @Override 31 | public @NotNull String getFilePattern() { 32 | return "*.json"; 33 | } 34 | 35 | @Override 36 | public boolean isIncludeSubDirs() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public boolean isSorting() { 42 | return true; 43 | } 44 | 45 | @Override 46 | public @Nullable String getNamespaceDelimiter() { 47 | return ":"; 48 | } 49 | 50 | @Override 51 | public @NotNull String getSectionDelimiter() { 52 | return "."; 53 | } 54 | 55 | @Override 56 | public @Nullable String getContextDelimiter() { 57 | return "_"; 58 | } 59 | 60 | @Override 61 | public @Nullable String getPluralDelimiter() { 62 | return "_"; 63 | } 64 | 65 | @Override 66 | public @Nullable String getDefaultNamespace() { 67 | return "common"; 68 | } 69 | 70 | @Override 71 | public @NotNull String getPreviewLocale() { 72 | return "en"; 73 | } 74 | 75 | @Override 76 | public boolean isNestedKeys() { 77 | return false; 78 | } 79 | 80 | @Override 81 | public boolean isAssistance() { 82 | return true; 83 | } 84 | 85 | @Override 86 | public boolean isAlwaysFold() { 87 | return false; 88 | } 89 | 90 | @Override 91 | public String getFlavorTemplate() { 92 | return "$i18n.t"; 93 | } 94 | @Override 95 | public @NotNull NamingConvention getCaseFormat() { 96 | return NamingConvention.CAMEL_CASE; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/presets/DefaultPreset.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings.presets; 2 | 3 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 4 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 5 | import de.marhali.easyi18n.settings.ProjectSettings; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Default preset. Used if none has been defined. 12 | * @author marhali 13 | */ 14 | public class DefaultPreset implements ProjectSettings { 15 | @Override 16 | public String getLocalesDirectory() { 17 | return null; 18 | } 19 | 20 | @Override 21 | public @NotNull FolderStrategyType getFolderStrategy() { 22 | return FolderStrategyType.SINGLE; 23 | } 24 | 25 | @Override 26 | public @NotNull ParserStrategyType getParserStrategy() { 27 | return ParserStrategyType.JSON; 28 | } 29 | 30 | @Override 31 | public @NotNull String getFilePattern() { 32 | return "*.*"; 33 | } 34 | 35 | @Override 36 | public boolean isIncludeSubDirs() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public boolean isSorting() { 42 | return true; 43 | } 44 | 45 | @Override 46 | public String getNamespaceDelimiter() { 47 | return ":"; 48 | } 49 | 50 | @Override 51 | public @NotNull String getSectionDelimiter() { 52 | return "."; 53 | } 54 | 55 | @Override 56 | public String getContextDelimiter() { 57 | return "_"; 58 | } 59 | 60 | @Override 61 | public String getPluralDelimiter() { 62 | return "_"; 63 | } 64 | 65 | @Override 66 | public @Nullable String getDefaultNamespace() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public @NotNull String getPreviewLocale() { 72 | return "en"; 73 | } 74 | 75 | @Override 76 | public boolean isNestedKeys() { 77 | return true; 78 | } 79 | 80 | @Override 81 | public boolean isAssistance() { 82 | return true; 83 | } 84 | 85 | @Override 86 | public boolean isAlwaysFold() { 87 | return false; 88 | } 89 | 90 | @Override 91 | public String getFlavorTemplate() { 92 | return "$i18n.t"; 93 | } 94 | 95 | @Override 96 | public @NotNull NamingConvention getCaseFormat() { 97 | return NamingConvention.CAMEL_CASE; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/NotificationHelper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import com.intellij.notification.*; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import com.intellij.openapi.project.Project; 6 | import de.marhali.easyi18n.action.OpenFileAction; 7 | import de.marhali.easyi18n.action.SettingsAction; 8 | import de.marhali.easyi18n.exception.SyntaxException; 9 | import de.marhali.easyi18n.io.IOHandler; 10 | import de.marhali.easyi18n.settings.ProjectSettings; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.text.MessageFormat; 14 | import java.util.ResourceBundle; 15 | 16 | /** 17 | * Utility tool to support creating notifications with detailed information like exception traces. 18 | * @author marhali 19 | */ 20 | public class NotificationHelper { 21 | private static final String NOTIFICATION_GROUP = "Easy I18n Notification Group"; 22 | 23 | public static void createIOError(@NotNull ProjectSettings state, Exception ex) { 24 | ResourceBundle bundle = ResourceBundle.getBundle("messages"); 25 | 26 | String message = MessageFormat.format(bundle.getString("error.io"), 27 | state.getFolderStrategy(), state.getParserStrategy(), state.getFilePattern(), state.getLocalesDirectory()); 28 | 29 | Logger.getInstance(IOHandler.class).error(message, ex); 30 | } 31 | 32 | public static void createBadSyntaxNotification(Project project, SyntaxException ex) { 33 | ResourceBundle bundle = ResourceBundle.getBundle("messages"); 34 | 35 | Notification notification = new Notification( 36 | NOTIFICATION_GROUP, 37 | bundle.getString("warning.bad-syntax"), 38 | ex.getMessage(), 39 | NotificationType.ERROR 40 | ); 41 | 42 | notification.addAction(new OpenFileAction(ex.getFile().getVirtualFile(), false)); 43 | notification.addAction(new SettingsAction(false)); 44 | 45 | Notifications.Bus.notify(notification, project); 46 | } 47 | 48 | public static void createEmptyLocalesDirNotification(Project project) { 49 | ResourceBundle bundle = ResourceBundle.getBundle("messages"); 50 | 51 | Notification notification = new Notification( 52 | NOTIFICATION_GROUP, 53 | "Easy I18n", 54 | bundle.getString("warning.missing-config"), 55 | NotificationType.WARNING); 56 | 57 | notification.addAction(new SettingsAction()); 58 | 59 | Notifications.Bus.notify(notification, project); 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/reference/PsiKeyReference.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.reference; 2 | 3 | import com.intellij.navigation.ItemPresentation; 4 | import com.intellij.openapi.util.TextRange; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiReferenceBase; 7 | import com.intellij.psi.SyntheticElement; 8 | import com.intellij.psi.impl.FakePsiElement; 9 | 10 | import de.marhali.easyi18n.dialog.AddDialog; 11 | import de.marhali.easyi18n.dialog.EditDialog; 12 | import de.marhali.easyi18n.model.Translation; 13 | import de.marhali.easyi18n.util.KeyPathConverter; 14 | 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | /** 19 | * References translation keys inside editor with corresponding {@link EditDialog} / {@link AddDialog}. 20 | * @author marhali 21 | */ 22 | public class PsiKeyReference extends PsiReferenceBase { 23 | 24 | private final @NotNull Translation translation; 25 | private final @NotNull KeyPathConverter converter; 26 | 27 | protected PsiKeyReference( 28 | @NotNull KeyPathConverter converter, @NotNull Translation translation, @NotNull PsiElement element) { 29 | 30 | super(element, true); 31 | this.translation = translation; 32 | this.converter = converter; 33 | } 34 | 35 | public @NotNull String getKey() { 36 | return converter.toString(translation.getKey()); 37 | } 38 | 39 | @Override 40 | public @Nullable PsiElement resolve() { 41 | return new TranslationReference(); 42 | } 43 | 44 | public class TranslationReference extends FakePsiElement implements SyntheticElement { 45 | @Override 46 | public PsiElement getParent() { 47 | return myElement; 48 | } 49 | 50 | @Override 51 | public void navigate(boolean requestFocus) { 52 | new EditDialog(getProject(), translation).showAndHandle(); 53 | } 54 | 55 | @Override 56 | public String getPresentableText() { 57 | return getKey(); 58 | } 59 | 60 | @Override 61 | public String getName() { 62 | return getKey(); 63 | } 64 | 65 | @Override 66 | public @Nullable TextRange getTextRange() { 67 | TextRange rangeInElement = getRangeInElement(); 68 | TextRange elementRange = myElement.getTextRange(); 69 | return elementRange != null ? rangeInElement.shiftRight(elementRange.getStartOffset()) : rangeInElement; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/properties/PropertiesMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.properties; 2 | 3 | import de.marhali.easyi18n.model.TranslationData; 4 | import de.marhali.easyi18n.model.KeyPath; 5 | import de.marhali.easyi18n.model.TranslationValue; 6 | import de.marhali.easyi18n.util.KeyPathConverter; 7 | import de.marhali.easyi18n.util.StringUtil; 8 | 9 | import org.apache.commons.lang3.math.NumberUtils; 10 | import org.apache.commons.text.StringEscapeUtils; 11 | 12 | import java.util.Map; 13 | 14 | /** 15 | * Mapper for mapping properties files into translation nodes and backwards. 16 | * @author marhali 17 | */ 18 | public class PropertiesMapper { 19 | 20 | public static void read(String locale, SortableProperties properties, 21 | TranslationData data, KeyPathConverter converter) { 22 | 23 | for(Map.Entry entry : properties.entrySet()) { 24 | KeyPath key = converter.fromString(String.valueOf(entry.getKey())); 25 | Object value = entry.getValue(); 26 | 27 | TranslationValue translation = data.getTranslation(key); 28 | 29 | if(translation == null) { 30 | translation = new TranslationValue(); 31 | } 32 | 33 | String content = value instanceof String[] 34 | ? PropertiesArrayMapper.read((String[]) value) 35 | : StringUtil.escapeControls(String.valueOf(value), true); 36 | 37 | translation.put(locale, content); 38 | data.setTranslation(key, translation); 39 | } 40 | } 41 | 42 | public static void write(String locale, SortableProperties properties, 43 | TranslationData data, KeyPathConverter converter) { 44 | 45 | for(KeyPath key : data.getFullKeys()) { 46 | TranslationValue translation = data.getTranslation(key); 47 | 48 | if(translation != null && translation.containsLocale(locale)) { 49 | String simpleKey = converter.toString(key); 50 | String content = translation.get(locale); 51 | 52 | if(PropertiesArrayMapper.isArray(content)) { 53 | properties.put(simpleKey, PropertiesArrayMapper.write(content)); 54 | } else if(NumberUtils.isCreatable(content)) { 55 | properties.put(simpleKey, NumberUtils.createNumber(content)); 56 | } else { 57 | properties.put(simpleKey, StringEscapeUtils.unescapeJava(content)); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/yaml/YamlParserStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.yaml; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import de.marhali.easyi18n.exception.SyntaxException; 6 | import de.marhali.easyi18n.io.parser.ParserStrategy; 7 | import de.marhali.easyi18n.model.TranslationData; 8 | import de.marhali.easyi18n.model.TranslationFile; 9 | import de.marhali.easyi18n.model.TranslationNode; 10 | import de.marhali.easyi18n.settings.ProjectSettings; 11 | 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | import org.yaml.snakeyaml.DumperOptions; 16 | import org.yaml.snakeyaml.Yaml; 17 | import org.yaml.snakeyaml.error.YAMLException; 18 | 19 | import java.io.InputStreamReader; 20 | import java.io.Reader; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | public class YamlParserStrategy extends ParserStrategy { 25 | 26 | private static DumperOptions dumperOptions() { 27 | DumperOptions options = new DumperOptions(); 28 | 29 | options.setIndent(2); 30 | options.setAllowUnicode(true); 31 | options.setPrettyFlow(true); 32 | options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 33 | 34 | return options; 35 | } 36 | 37 | private static final Yaml YAML = new Yaml(dumperOptions()); 38 | 39 | public YamlParserStrategy(@NotNull ProjectSettings settings) { 40 | super(settings); 41 | } 42 | 43 | @Override 44 | public void read(@NotNull TranslationFile file, @NotNull TranslationData data) throws Exception { 45 | data.addLocale(file.getLocale()); 46 | 47 | VirtualFile vf = file.getVirtualFile(); 48 | TranslationNode targetNode = super.getOrCreateTargetNode(file, data); 49 | 50 | try(Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) { 51 | Map input; 52 | 53 | try { 54 | input = YAML.load(reader); 55 | } catch(YAMLException ex) { 56 | throw new SyntaxException(ex.getMessage(), file); 57 | } 58 | 59 | YamlMapper.read(file.getLocale(), input, targetNode); 60 | } 61 | } 62 | 63 | @Override 64 | public @Nullable String write(@NotNull TranslationData data, @NotNull TranslationFile file) throws Exception { 65 | TranslationNode targetNode = super.getTargetNode(data, file); 66 | 67 | Map output = new HashMap<>(); 68 | YamlMapper.write(file.getLocale(), output, targetNode); 69 | 70 | return YAML.dumpAsMap(output); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/service/FileChangeListener.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.service; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.AsyncFileListener; 7 | import com.intellij.openapi.vfs.LocalFileSystem; 8 | import com.intellij.openapi.vfs.VirtualFile; 9 | import com.intellij.openapi.vfs.newvfs.events.VFileEvent; 10 | 11 | import de.marhali.easyi18n.InstanceManager; 12 | 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.io.File; 17 | import java.util.List; 18 | 19 | /** 20 | * Listens for file changes inside configured @localesPath. See {@link AsyncFileListener}. 21 | * Will trigger the reload function of the i18n instance if a relevant file was changed. 22 | * @author marhali 23 | */ 24 | public class FileChangeListener implements AsyncFileListener { 25 | 26 | private static final Logger logger = Logger.getInstance(FileChangeListener.class); 27 | 28 | private final @NotNull Project project; 29 | private @Nullable String localesPath; 30 | 31 | public FileChangeListener(@NotNull Project project) { 32 | this.project = project; 33 | this.localesPath = null; // Wait for any update before listening to file changes 34 | } 35 | 36 | public void updateLocalesPath(@Nullable String localesPath) { 37 | if(localesPath != null && !localesPath.isEmpty()) { 38 | VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath)); 39 | 40 | if(file != null && file.isDirectory()) { 41 | this.localesPath = file.getPath(); 42 | return; 43 | } 44 | } 45 | 46 | this.localesPath = null; 47 | } 48 | 49 | @Override 50 | public ChangeApplier prepareChange(@NotNull List events) { 51 | return new ChangeApplier() { 52 | @Override 53 | public void afterVfsChange() { 54 | if(localesPath != null) { 55 | events.forEach((e) -> { 56 | if(e.getPath().contains(localesPath)) { // Perform reload 57 | logger.debug("Detected file change. Reloading instance..."); 58 | ApplicationManager.getApplication().invokeLater(() -> 59 | InstanceManager.get(project).reload() 60 | ); 61 | } 62 | }); 63 | } 64 | } 65 | }; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json5/Json5ParserStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json5; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import de.marhali.easyi18n.exception.SyntaxException; 6 | import de.marhali.easyi18n.io.parser.ParserStrategy; 7 | import de.marhali.easyi18n.model.TranslationData; 8 | import de.marhali.easyi18n.model.TranslationFile; 9 | import de.marhali.easyi18n.model.TranslationNode; 10 | import de.marhali.easyi18n.settings.ProjectSettings; 11 | import de.marhali.json5.Json5; 12 | import de.marhali.json5.Json5Element; 13 | import de.marhali.json5.Json5Object; 14 | 15 | import de.marhali.json5.exception.Json5Exception; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.io.InputStreamReader; 19 | import java.io.Reader; 20 | import java.util.Objects; 21 | 22 | /** 23 | * Json5 file format parser strategy 24 | * @author marhali 25 | */ 26 | public class Json5ParserStrategy extends ParserStrategy { 27 | 28 | private static final Json5 JSON5 = Json5.builder(builder -> 29 | builder.allowInvalidSurrogate().trailingComma().indentFactor(2).build()); 30 | 31 | public Json5ParserStrategy(@NotNull ProjectSettings settings) { 32 | super(settings); 33 | } 34 | 35 | @Override 36 | public void read(@NotNull TranslationFile file, @NotNull TranslationData data) throws Exception { 37 | data.addLocale(file.getLocale()); 38 | 39 | VirtualFile vf = file.getVirtualFile(); 40 | TranslationNode targetNode = super.getOrCreateTargetNode(file, data); 41 | 42 | try (Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) { 43 | Json5Element input; 44 | 45 | try { 46 | input = JSON5.parse(reader); 47 | if(input != null && !input.isJson5Object()) { 48 | throw new SyntaxException("Expected a json5 object as root node", file); 49 | } 50 | } catch (Json5Exception ex) { 51 | throw new SyntaxException(ex.getMessage(), file); 52 | } 53 | 54 | if(input != null) { 55 | Json5Mapper.read(file.getLocale(), input.getAsJson5Object(), targetNode); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public @NotNull String write(@NotNull TranslationData data, @NotNull TranslationFile file) throws Exception { 62 | TranslationNode targetNode = super.getTargetNode(data, file); 63 | 64 | Json5Object output = new Json5Object(); 65 | Json5Mapper.write(file.getLocale(), output, Objects.requireNonNull(targetNode)); 66 | 67 | return JSON5.serialize(output); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | de.marhali.easyi18n 4 | Easy I18n 5 | marhali 6 | 7 | 8 | 9 | com.intellij.modules.platform 10 | com.intellij.modules.lang 11 | 12 | org.jetbrains.kotlin 13 | JavaScript 14 | com.intellij.modules.xml 15 | com.intellij.java 16 | com.jetbrains.php 17 | 18 | 19 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/assistance/completion/KeyCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.assistance.completion; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.CompletionProvider; 5 | import com.intellij.codeInsight.completion.CompletionResultSet; 6 | import com.intellij.codeInsight.lookup.LookupElement; 7 | import com.intellij.codeInsight.lookup.LookupElementBuilder; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.util.IconLoader; 10 | import com.intellij.util.ProcessingContext; 11 | 12 | import de.marhali.easyi18n.InstanceManager; 13 | import de.marhali.easyi18n.assistance.OptionalAssistance; 14 | import de.marhali.easyi18n.model.KeyPath; 15 | import de.marhali.easyi18n.model.Translation; 16 | import de.marhali.easyi18n.model.TranslationData; 17 | import de.marhali.easyi18n.settings.ProjectSettings; 18 | import de.marhali.easyi18n.settings.ProjectSettingsService; 19 | import de.marhali.easyi18n.util.KeyPathConverter; 20 | 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import javax.swing.*; 24 | import java.util.Set; 25 | 26 | /** 27 | * Provides existing translation keys for code completion. 28 | * @author marhali 29 | */ 30 | class KeyCompletionProvider extends CompletionProvider implements OptionalAssistance { 31 | 32 | private static final Icon icon = IconLoader.getIcon("/icons/translate13.svg", KeyCompletionProvider.class); 33 | 34 | @Override 35 | protected void addCompletions(@NotNull CompletionParameters parameters, 36 | @NotNull ProcessingContext context, @NotNull CompletionResultSet result) { 37 | Project project = parameters.getOriginalFile().getProject(); 38 | 39 | if(!isAssistance(project)) { 40 | return; 41 | } 42 | 43 | ProjectSettings settings = ProjectSettingsService.get(project).getState(); 44 | TranslationData data = InstanceManager.get(project).store().getData(); 45 | Set fullKeys = data.getFullKeys(); 46 | 47 | for (KeyPath key : fullKeys) { 48 | result.addElement(constructLookup(new Translation(key, data.getTranslation(key)), settings)); 49 | } 50 | } 51 | 52 | private LookupElement constructLookup(Translation translation, ProjectSettings settings) { 53 | KeyPathConverter converter = new KeyPathConverter(settings); 54 | 55 | return LookupElementBuilder 56 | .create(converter.toString(translation.getKey())) 57 | .withTailText(" " + translation.getValue().get(settings.getPreviewLocale()), true) 58 | .withIcon(icon); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/properties/PropertiesParserStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.properties; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import de.marhali.easyi18n.exception.SyntaxException; 6 | import de.marhali.easyi18n.io.parser.ParserStrategy; 7 | import de.marhali.easyi18n.model.TranslationData; 8 | import de.marhali.easyi18n.model.TranslationFile; 9 | import de.marhali.easyi18n.model.TranslationNode; 10 | import de.marhali.easyi18n.settings.ProjectSettings; 11 | import de.marhali.easyi18n.util.KeyPathConverter; 12 | 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStreamReader; 17 | import java.io.Reader; 18 | import java.io.StringWriter; 19 | 20 | /** 21 | * Properties file format parser strategy. 22 | * @author marhali 23 | */ 24 | public class PropertiesParserStrategy extends ParserStrategy { 25 | 26 | private final @NotNull KeyPathConverter converter; 27 | 28 | public PropertiesParserStrategy(@NotNull ProjectSettings settings) { 29 | super(settings); 30 | this.converter = new KeyPathConverter(settings); 31 | } 32 | 33 | @Override 34 | public void read(@NotNull TranslationFile file, @NotNull TranslationData data) throws Exception { 35 | data.addLocale(file.getLocale()); 36 | 37 | VirtualFile vf = file.getVirtualFile(); 38 | TranslationNode targetNode = super.getOrCreateTargetNode(file, data); 39 | TranslationData targetData = new TranslationData(data.getLocales(), targetNode); 40 | 41 | try(Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) { 42 | SortableProperties input = new SortableProperties(this.settings.isSorting()); 43 | 44 | try { 45 | input.load(reader); 46 | } catch(IOException ex) { 47 | throw new SyntaxException(ex.getMessage(), file); 48 | } 49 | 50 | PropertiesMapper.read(file.getLocale(), input, targetData, converter); 51 | } 52 | } 53 | 54 | @Override 55 | public @NotNull String write(@NotNull TranslationData data, @NotNull TranslationFile file) throws Exception { 56 | TranslationNode targetNode = super.getTargetNode(data, file); 57 | TranslationData targetData = new TranslationData(data.getLocales(), targetNode); 58 | 59 | SortableProperties output = new SortableProperties(this.settings.isSorting()); 60 | PropertiesMapper.write(file.getLocale(), output, targetData, converter); 61 | 62 | try(StringWriter writer = new StringWriter()) { 63 | output.store(writer); 64 | return writer.toString(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/ParserStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser; 2 | 3 | import de.marhali.easyi18n.model.*; 4 | import de.marhali.easyi18n.settings.ProjectSettings; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.Objects; 10 | 11 | /** 12 | * Represents a parser for a specific file format. 13 | * @author marhali 14 | */ 15 | public abstract class ParserStrategy { 16 | 17 | protected final @NotNull ProjectSettings settings; 18 | 19 | public ParserStrategy(@NotNull ProjectSettings settings) { 20 | this.settings = settings; 21 | } 22 | 23 | /** 24 | * Reads the translation file into the translation data object (consider namespace and locale) 25 | * @param file File to read from 26 | * @param data Target translation data to save the parsed data 27 | */ 28 | public abstract void read(@NotNull TranslationFile file, @NotNull TranslationData data) throws Exception; 29 | 30 | /** 31 | * Constructs the relevant data to represents the specified translation file. (consider namespace and locale) 32 | * @param data Translation data cache 33 | * @param file Target translation file 34 | * @return String representing target translation file. 35 | * Can be null to indicate that the file is not necessary and could be deleted 36 | */ 37 | public abstract @Nullable String write(@NotNull TranslationData data, @NotNull TranslationFile file) throws Exception; 38 | 39 | /** 40 | * Determines translation node to use for parsing 41 | * @param file Translation file to parse 42 | * @param data Translations 43 | * @return TranslationNode to use 44 | */ 45 | protected @NotNull TranslationNode getOrCreateTargetNode( 46 | @NotNull TranslationFile file, @NotNull TranslationData data) { 47 | 48 | TranslationNode targetNode = data.getRootNode(); 49 | 50 | if(file.getNamespace() != null) { 51 | targetNode = data.getOrCreateNoe(file.getNamespace()); 52 | } 53 | 54 | return targetNode; 55 | } 56 | 57 | /** 58 | * Determines translation node to use for writing 59 | * @param data Translations 60 | * @param file Translation file to update 61 | * @return TranslationNode to use 62 | */ 63 | protected @NotNull TranslationNode getTargetNode(@NotNull TranslationData data, @NotNull TranslationFile file) { 64 | TranslationNode targetNode = data.getRootNode(); 65 | 66 | if(file.getNamespace() != null) { 67 | targetNode = data.getNode(new KeyPath(file.getNamespace())); 68 | } 69 | 70 | return Objects.requireNonNull(targetNode); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/action/AddAction.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.action; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.ui.content.Content; 8 | 9 | import de.marhali.easyi18n.dialog.AddDialog; 10 | import de.marhali.easyi18n.model.KeyPath; 11 | import de.marhali.easyi18n.service.WindowManager; 12 | import de.marhali.easyi18n.util.KeyPathConverter; 13 | import de.marhali.easyi18n.util.TreeUtil; 14 | 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import javax.swing.tree.TreePath; 19 | import java.util.Objects; 20 | import java.util.ResourceBundle; 21 | 22 | /** 23 | * Add translation action. 24 | * @author marhai 25 | */ 26 | public class AddAction extends AnAction { 27 | 28 | public AddAction() { 29 | super(ResourceBundle.getBundle("messages").getString("action.add"), 30 | null, AllIcons.General.Add); 31 | } 32 | 33 | @Override 34 | public void actionPerformed(@NotNull AnActionEvent e) { 35 | Project project = Objects.requireNonNull(e.getProject()); 36 | new AddDialog(project, detectPreKey(project), null).showAndHandle(); 37 | } 38 | 39 | /** 40 | * Detects a selected translation key in our tool-window. 41 | * @param project Opened project 42 | * @return Found key to prefill translation key or null if not applicable 43 | */ 44 | private @Nullable KeyPath detectPreKey(@NotNull Project project) { 45 | KeyPathConverter converter = new KeyPathConverter(project); 46 | WindowManager window = WindowManager.getInstance(); 47 | 48 | if(window.getToolWindow() == null) { 49 | return null; 50 | } 51 | 52 | Content manager = window.getToolWindow().getContentManager().getSelectedContent(); 53 | 54 | if(manager == null) { 55 | return null; 56 | } 57 | 58 | if(manager.getDisplayName().equals( 59 | ResourceBundle.getBundle("messages").getString("view.tree.title"))) { // Tree View 60 | 61 | TreePath path = window.getTreeView().getTree().getSelectionPath(); 62 | 63 | if(path != null) { 64 | return TreeUtil.getFullPath(path); 65 | } 66 | 67 | } else { // Table View 68 | int row = window.getTableView().getTable().getSelectedRow(); 69 | 70 | if(row >= 0) { 71 | String path = String.valueOf(window.getTableView().getTable().getValueAt(row, 0)); 72 | return converter.fromString(path); 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | } -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/settings/SettingsTestPreset.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings; 2 | 3 | import de.marhali.easyi18n.io.folder.FolderStrategyType; 4 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 5 | 6 | import de.marhali.easyi18n.settings.presets.NamingConvention; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Settings preset to test the functionality of the settings service. 12 | * 13 | * @author marhali 14 | */ 15 | public class SettingsTestPreset implements ProjectSettings { 16 | @Override 17 | public @Nullable String getLocalesDirectory() { 18 | return "myCustomLocalesDirectory"; 19 | } 20 | 21 | @Override 22 | public @NotNull FolderStrategyType getFolderStrategy() { 23 | return FolderStrategyType.MODULARIZED_NAMESPACE; 24 | } 25 | 26 | @Override 27 | public @NotNull ParserStrategyType getParserStrategy() { 28 | return ParserStrategyType.JSON5; 29 | } 30 | 31 | @Override 32 | public @NotNull String getFilePattern() { 33 | return "*.testfile.json5"; 34 | } 35 | 36 | @Override 37 | public boolean isIncludeSubDirs() { 38 | return true; 39 | } 40 | 41 | @Override 42 | public boolean isSorting() { 43 | return false; 44 | } 45 | 46 | @Override 47 | public @Nullable String getNamespaceDelimiter() { 48 | return "nsDelim"; 49 | } 50 | 51 | @Override 52 | public @NotNull String getSectionDelimiter() { 53 | return "sctDelim"; 54 | } 55 | 56 | @Override 57 | public @Nullable String getContextDelimiter() { 58 | return "ctxDelim"; 59 | } 60 | 61 | @Override 62 | public @Nullable String getPluralDelimiter() { 63 | return "plDelim"; 64 | } 65 | 66 | @Override 67 | public @Nullable String getDefaultNamespace() { 68 | return "defNs"; 69 | } 70 | 71 | @Override 72 | public @NotNull String getPreviewLocale() { 73 | return "prevLocale"; 74 | } 75 | 76 | @Override 77 | public boolean isNestedKeys() { 78 | return true; 79 | } 80 | 81 | @Override 82 | public boolean isAssistance() { 83 | return false; 84 | } 85 | 86 | @Override 87 | public boolean isAlwaysFold() { 88 | return false; 89 | } 90 | 91 | @Override 92 | public String getFlavorTemplate() { 93 | return "t"; 94 | } 95 | 96 | @Override 97 | public @NotNull NamingConvention getCaseFormat() { 98 | return NamingConvention.CAMEL_CASE; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/yaml/YamlMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.yaml; 2 | 3 | import de.marhali.easyi18n.model.TranslationNode; 4 | import de.marhali.easyi18n.model.TranslationValue; 5 | import de.marhali.easyi18n.util.StringUtil; 6 | 7 | import org.apache.commons.lang3.math.NumberUtils; 8 | import org.apache.commons.text.StringEscapeUtils; 9 | 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class YamlMapper { 15 | @SuppressWarnings("unchecked") 16 | public static void read(String locale, Map section, TranslationNode node) { 17 | for(String key : section.keySet()) { 18 | Object value = section.get(key); 19 | TranslationNode childNode = node.getOrCreateChildren(key); 20 | 21 | if(value instanceof Map) { 22 | // Nested element run recursively 23 | read(locale, (Map) value, childNode); 24 | } else { 25 | TranslationValue translation = childNode.getValue(); 26 | 27 | String content = value instanceof List 28 | ? YamlArrayMapper.read((List) value) 29 | : StringUtil.escapeControls(String.valueOf(value), true); 30 | 31 | translation.put(locale, content); 32 | childNode.setValue(translation); 33 | } 34 | } 35 | } 36 | 37 | public static void write(String locale, Map section, TranslationNode node) { 38 | for(Map.Entry entry : node.getChildren().entrySet()) { 39 | String key = entry.getKey(); 40 | TranslationNode childNode = entry.getValue(); 41 | 42 | if(!childNode.isLeaf()) { 43 | // Nested node - run recursively 44 | Map childSection = new HashMap<>(); 45 | write(locale, childSection, childNode); 46 | if(!childSection.isEmpty()) { 47 | section.put(key, childSection); 48 | } 49 | } else { 50 | TranslationValue translation = childNode.getValue(); 51 | String content = translation.get(locale); 52 | 53 | if(content != null) { 54 | if(YamlArrayMapper.isArray(content)) { 55 | section.put(key, YamlArrayMapper.write(content)); 56 | } else if(NumberUtils.isCreatable(content)) { 57 | section.put(key, NumberUtils.createNumber(content)); 58 | } else { 59 | section.put(key, StringEscapeUtils.unescapeJava(content)); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/service/TranslatorToolWindowFactory.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.service; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.project.DumbAware; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.wm.ToolWindow; 7 | import com.intellij.openapi.wm.ToolWindowFactory; 8 | import com.intellij.ui.content.Content; 9 | import com.intellij.ui.content.ContentFactory; 10 | 11 | import de.marhali.easyi18n.InstanceManager; 12 | import de.marhali.easyi18n.action.*; 13 | import de.marhali.easyi18n.tabs.TableView; 14 | import de.marhali.easyi18n.tabs.TreeView; 15 | 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.ResourceBundle; 21 | 22 | /** 23 | * Tool window factory which will represent the entire ui for this plugin. 24 | * @author marhali 25 | */ 26 | public class TranslatorToolWindowFactory implements ToolWindowFactory, DumbAware { 27 | 28 | @Override 29 | public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { 30 | InstanceManager manager = InstanceManager.get(project); 31 | ContentFactory contentFactory = ContentFactory.getInstance(); 32 | 33 | // Translations tree view 34 | TreeView treeView = new TreeView(project); 35 | Content treeContent = contentFactory.createContent(treeView.getRootPanel(), 36 | ResourceBundle.getBundle("messages").getString("view.tree.title"), false); 37 | 38 | toolWindow.getContentManager().addContent(treeContent); 39 | 40 | // Translations table view 41 | TableView tableView = new TableView(project); 42 | Content tableContent = contentFactory.createContent(tableView.getRootPanel(), 43 | ResourceBundle.getBundle("messages").getString("view.table.title"), false); 44 | 45 | toolWindow.getContentManager().addContent(tableContent); 46 | 47 | // ToolWindow Actions (Can be used for every view) 48 | List actions = new ArrayList<>(); 49 | actions.add(new AddAction()); 50 | actions.add(new FilterIncompleteAction()); 51 | actions.add(new FilterDuplicateAction()); 52 | actions.add(new ReloadAction()); 53 | actions.add(new SettingsAction()); 54 | actions.add(new SearchAction((query) -> manager.bus().propagate().onSearchQuery(query))); 55 | toolWindow.setTitleActions(actions); 56 | 57 | // Initialize Window Manager 58 | WindowManager.getInstance().initialize(toolWindow, treeView, tableView); 59 | 60 | // Synchronize ui with underlying data 61 | manager.uiBus().addListener(treeView); 62 | manager.uiBus().addListener(tableView); 63 | manager.bus().propagate().onUpdateData(manager.store().getData()); 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/dialog/AddDialog.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.dialog; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.ui.DialogWrapper; 5 | 6 | import de.marhali.easyi18n.model.action.TranslationCreate; 7 | import de.marhali.easyi18n.model.KeyPath; 8 | import de.marhali.easyi18n.model.Translation; 9 | import de.marhali.easyi18n.model.TranslationValue; 10 | import de.marhali.easyi18n.model.action.TranslationUpdate; 11 | import de.marhali.easyi18n.settings.ProjectSettingsService; 12 | 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.util.function.Consumer; 17 | 18 | /** 19 | * Dialog to create a new translation with all associated locale values. 20 | * Supports optional prefill technique for translation key or locale value. 21 | * @author marhali 22 | */ 23 | public class AddDialog extends TranslationDialog { 24 | 25 | private Consumer onCreated; 26 | 27 | /** 28 | * Constructs a new create dialog with prefilled fields 29 | * @param project Opened project 30 | * @param prefillKey Prefill translation key 31 | * @param prefillLocale Prefill preview locale value 32 | */ 33 | public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale) { 34 | super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(), 35 | prefillLocale != null 36 | ? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale) 37 | : null) 38 | ); 39 | 40 | setTitle(bundle.getString("action.add")); 41 | } 42 | public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale,Consumer onCreated) { 43 | super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(), 44 | prefillLocale != null 45 | ? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale) 46 | : null) 47 | ); 48 | 49 | this.onCreated = onCreated; 50 | setTitle(bundle.getString("action.add")); 51 | } 52 | 53 | /** 54 | * Constructs a new create dialog without prefilled fields. 55 | * @param project Opened project 56 | */ 57 | public AddDialog(@NotNull Project project) { 58 | this(project, new KeyPath(), ""); 59 | } 60 | 61 | @Override 62 | protected @Nullable TranslationUpdate handleExit(int exitCode) { 63 | if(exitCode == DialogWrapper.OK_EXIT_CODE) { 64 | if(onCreated != null) { 65 | onCreated.accept(this.getKeyField().getText()); 66 | } 67 | 68 | return new TranslationCreate(getState()); 69 | } 70 | 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json/JsonMapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonPrimitive; 6 | 7 | import de.marhali.easyi18n.model.TranslationNode; 8 | import de.marhali.easyi18n.model.TranslationValue; 9 | import de.marhali.easyi18n.util.StringUtil; 10 | 11 | import org.apache.commons.lang3.math.NumberUtils; 12 | import org.apache.commons.text.StringEscapeUtils; 13 | 14 | import java.util.Map; 15 | 16 | /** 17 | * Mapper for mapping json objects into translation nodes and backwards. 18 | * @author marhali 19 | */ 20 | public class JsonMapper { 21 | 22 | public static void read(String locale, JsonObject json, TranslationNode node) { 23 | for(Map.Entry entry : json.entrySet()) { 24 | String key = entry.getKey(); 25 | JsonElement value = entry.getValue(); 26 | 27 | TranslationNode childNode = node.getOrCreateChildren(key); 28 | 29 | if(value.isJsonObject()) { 30 | // Nested element - run recursively 31 | read(locale, value.getAsJsonObject(), childNode); 32 | } else { 33 | TranslationValue translation = childNode.getValue(); 34 | 35 | String content = entry.getValue().isJsonArray() 36 | ? JsonArrayMapper.read(value.getAsJsonArray()) 37 | : StringUtil.escapeControls(value.getAsString(), true); 38 | 39 | translation.put(locale, content); 40 | childNode.setValue(translation); 41 | } 42 | } 43 | } 44 | 45 | public static void write(String locale, JsonObject json, TranslationNode node) { 46 | for(Map.Entry entry : node.getChildren().entrySet()) { 47 | String key = entry.getKey(); 48 | TranslationNode childNode = entry.getValue(); 49 | 50 | if(!childNode.isLeaf()) { 51 | // Nested node - run recursively 52 | JsonObject childJson = new JsonObject(); 53 | write(locale, childJson, childNode); 54 | if(childJson.size() > 0) { 55 | json.add(key, childJson); 56 | } 57 | } else { 58 | TranslationValue translation = childNode.getValue(); 59 | String content = translation.get(locale); 60 | 61 | if(content != null) { 62 | if(JsonArrayMapper.isArray(content)) { 63 | json.add(key, JsonArrayMapper.write(content)); 64 | } else if(NumberUtils.isCreatable(content)) { 65 | json.add(key, new JsonPrimitive(NumberUtils.createNumber(content))); 66 | } else { 67 | json.add(key, new JsonPrimitive(StringEscapeUtils.unescapeJava(content))); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/parser/json5/Json5Mapper.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.parser.json5; 2 | 3 | import de.marhali.easyi18n.model.TranslationNode; 4 | import de.marhali.easyi18n.model.TranslationValue; 5 | import de.marhali.easyi18n.util.StringUtil; 6 | import de.marhali.json5.Json5Element; 7 | import de.marhali.json5.Json5Object; 8 | import de.marhali.json5.Json5Primitive; 9 | 10 | import org.apache.commons.lang3.math.NumberUtils; 11 | import org.apache.commons.text.StringEscapeUtils; 12 | 13 | import java.util.Map; 14 | 15 | /** 16 | * Mapper for mapping json5 objects into translation nodes and backwards. 17 | * @author marhali 18 | */ 19 | public class Json5Mapper { 20 | public static void read(String locale, Json5Object json, TranslationNode node) { 21 | for(Map.Entry entry : json.entrySet()) { 22 | String key = entry.getKey(); 23 | Json5Element value = entry.getValue(); 24 | 25 | TranslationNode childNode = node.getOrCreateChildren(key); 26 | 27 | if(value.isJson5Object()) { 28 | // Nested element - run recursively 29 | read(locale, value.getAsJson5Object(), childNode); 30 | } else { 31 | TranslationValue translation = childNode.getValue(); 32 | 33 | String content = value.isJson5Array() 34 | ? Json5ArrayMapper.read(value.getAsJson5Array()) 35 | : StringUtil.escapeControls(value.getAsString(), true); 36 | 37 | translation.put(locale, content); 38 | childNode.setValue(translation); 39 | } 40 | } 41 | } 42 | 43 | public static void write(String locale, Json5Object json, TranslationNode node) { 44 | for(Map.Entry entry : node.getChildren().entrySet()) { 45 | String key = entry.getKey(); 46 | TranslationNode childNode = entry.getValue(); 47 | 48 | if(!childNode.isLeaf()) { 49 | // Nested node - run recursively 50 | Json5Object childJson = new Json5Object(); 51 | write(locale, childJson, childNode); 52 | if(childJson.size() > 0) { 53 | json.add(key, childJson); 54 | } 55 | 56 | } else { 57 | TranslationValue translation = childNode.getValue(); 58 | String content = translation.get(locale); 59 | if(content != null) { 60 | if(Json5ArrayMapper.isArray(content)) { 61 | json.add(key, Json5ArrayMapper.write(content)); 62 | } else if(StringUtil.isHexString(content)) { 63 | json.add(key, Json5Primitive.of(content, true)); 64 | } else if(NumberUtils.isCreatable(content)) { 65 | json.add(key, Json5Primitive.of(NumberUtils.createNumber(content))); 66 | } else { 67 | json.add(key, Json5Primitive.of(StringEscapeUtils.unescapeJava(content))); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/util/TranslationUtil.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.util; 2 | 3 | import de.marhali.easyi18n.model.KeyPath; 4 | import de.marhali.easyi18n.model.Translation; 5 | import de.marhali.easyi18n.model.TranslationData; 6 | import de.marhali.easyi18n.model.TranslationValue; 7 | import de.marhali.easyi18n.settings.ProjectSettingsState; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Collection; 12 | import java.util.Objects; 13 | 14 | /** 15 | * Utilities for translations 16 | * @author marhali 17 | */ 18 | public class TranslationUtil { 19 | /** 20 | * Check whether a given translation has duplicated values. 21 | * @param translation The translation to check 22 | * @param data Translation data cache 23 | * @return true if duplicates were found otherwise false 24 | */ 25 | public static boolean hasDuplicates(@NotNull Translation translation, @NotNull TranslationData data) { 26 | assert translation.getValue() != null; 27 | Collection contents = translation.getValue().getLocaleContents(); 28 | 29 | for (KeyPath key : data.getFullKeys()) { 30 | if(translation.getKey().equals(key)) { // Only consider other translations 31 | continue; 32 | } 33 | 34 | for (String localeContent : Objects.requireNonNull(data.getTranslation(key)).getLocaleContents()) { 35 | if(contents.contains(localeContent)) { 36 | return true; 37 | } 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | /** 45 | * Check whether a given translation has missing locale values. 46 | * @param value The translation to check 47 | * @param data Translation data cache 48 | * @return true if missing values were found otherwise false 49 | */ 50 | public static boolean isIncomplete(@NotNull TranslationValue value, @NotNull TranslationData data) { 51 | return value.getLocaleContents().size() != data.getLocales().size() 52 | || value.getLocaleContents().stream().anyMatch(String::isEmpty); 53 | } 54 | 55 | /** 56 | * Check whether a given translation falls under a specified search query. 57 | * @param settings Project specific settings 58 | * @param translation The translation to check 59 | * @param searchQuery Full-text search term 60 | * @return true if translations is applicable otherwise false 61 | */ 62 | public static boolean isSearched(@NotNull ProjectSettingsState settings, 63 | @NotNull Translation translation, @NotNull String searchQuery) { 64 | 65 | String concatKey = new KeyPathConverter(settings).toString(translation.getKey()).toLowerCase(); 66 | 67 | if(searchQuery.contains(concatKey) || concatKey.contains(searchQuery)) { 68 | return true; 69 | } 70 | 71 | assert translation.getValue() != null; 72 | for (String localeContent : translation.getValue().getLocaleContents()) { 73 | if(localeContent.toLowerCase().contains(searchQuery)) { 74 | return true; 75 | } 76 | } 77 | 78 | return false; 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/settings/presets/NamingConvention.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.settings.presets; 2 | 3 | import com.google.common.base.CaseFormat; 4 | 5 | import java.util.Arrays; 6 | 7 | /** 8 | * Enum representing different naming conventions. 9 | * Provides utility methods to convert keys to the specified convention. 10 | */ 11 | public enum NamingConvention { 12 | CAMEL_CASE("Camel Case"), 13 | PASCAL_CASE("Pascal Case"), 14 | SNAKE_CASE("Snake Case"), 15 | SNAKE_CASE_UPPERCASE("Snake Case (Uppercase)"); 16 | 17 | private final String name; 18 | 19 | private NamingConvention(String name) { 20 | this.name = name; 21 | } 22 | 23 | /** 24 | * Retrieves the name of the current instance of the class. 25 | * 26 | * @return the name of the current instance 27 | */ 28 | public String getName() { 29 | return this.name; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return super.name().toLowerCase(); 35 | } 36 | 37 | /** 38 | * Converts a string representation of a naming convention to the corresponding NamingConvention enum value. 39 | * 40 | * @param name the string representation of the naming convention 41 | * @return the corresponding NamingConvention enum value 42 | */ 43 | static public NamingConvention fromString(String name) { 44 | for (NamingConvention value : NamingConvention.values()) { 45 | if (value.getName().equals(name)) 46 | return value; 47 | } 48 | return NamingConvention.CAMEL_CASE; 49 | } 50 | 51 | /** 52 | * Returns an array of strings representing the names of the enum values in the {@link NamingConvention} enum. 53 | * 54 | * @return an array of strings representing the enum names 55 | */ 56 | static public String[] getEnumNames() { 57 | return Arrays.stream(NamingConvention.values()) 58 | .map(NamingConvention::getName) 59 | .toArray(String[]::new); 60 | } 61 | 62 | /** 63 | * Converts a given key to the specified naming convention. 64 | * 65 | * @param key the key to convert 66 | * @param convention the naming convention to convert the key to 67 | * @return the converted key 68 | */ 69 | static public String convertKeyToConvention(String key, NamingConvention convention) { 70 | String newKey = key.toLowerCase(); 71 | newKey = newKey.replaceAll("\\s+", "_"); 72 | return switch (convention) { 73 | case SNAKE_CASE: 74 | yield formatToSnakeCase(newKey, false); 75 | case SNAKE_CASE_UPPERCASE: 76 | yield formatToSnakeCase(newKey, true); 77 | case CAMEL_CASE: 78 | yield formatToCamelCase(newKey, false); 79 | case PASCAL_CASE: 80 | yield formatToCamelCase(newKey, true); 81 | 82 | }; 83 | } 84 | 85 | static private String formatToCamelCase(String key, boolean capitalized) { 86 | return CaseFormat.LOWER_UNDERSCORE.to(capitalized ? CaseFormat.UPPER_CAMEL : CaseFormat.LOWER_CAMEL, key); 87 | } 88 | 89 | static private String formatToSnakeCase(String key, boolean capitalized) { 90 | return CaseFormat.LOWER_UNDERSCORE.to(capitalized ? CaseFormat.UPPER_UNDERSCORE : CaseFormat.LOWER_UNDERSCORE, key); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/de/marhali/easyi18n/e2e/EndToEndTestCase.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.e2e; 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase; 4 | 5 | import de.marhali.easyi18n.InstanceManager; 6 | import de.marhali.easyi18n.settings.ProjectSettings; 7 | import de.marhali.easyi18n.settings.ProjectSettingsService; 8 | import de.marhali.easyi18n.settings.ProjectSettingsState; 9 | 10 | import org.apache.commons.io.FileUtils; 11 | import org.apache.commons.io.filefilter.IOFileFilter; 12 | import org.apache.commons.io.filefilter.TrueFileFilter; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.charset.Charset; 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.util.Arrays; 21 | import java.util.Objects; 22 | 23 | /** 24 | * End-to-end test case. 25 | * 26 | * @author marhali 27 | */ 28 | public abstract class EndToEndTestCase extends BasePlatformTestCase { 29 | 30 | private static final Charset CHARSET = StandardCharsets.UTF_8; 31 | 32 | private final ProjectSettings settings; 33 | private Path tempPath; 34 | 35 | public EndToEndTestCase(ProjectSettings settings) { 36 | this.settings = settings; 37 | } 38 | 39 | @Override 40 | protected void setUp() throws Exception { 41 | super.setUp(); 42 | ProjectSettingsService.get(getProject()).setState(new ProjectSettingsState(settings)); 43 | tempPath = Files.createTempDirectory("tests-easyi18n-"); 44 | } 45 | 46 | @Override 47 | protected void tearDown() throws Exception { 48 | FileUtils.deleteDirectory(tempPath.toFile()); 49 | super.tearDown(); 50 | } 51 | 52 | public void testParseAndSerialize() throws IOException { 53 | // Read translation files based on the provided settings 54 | InstanceManager.get(getProject()).store().loadFromPersistenceLayer(success -> {}); 55 | 56 | // Save the cached translation data to a temporary output directory 57 | ProjectSettingsState out = new ProjectSettingsState(settings); 58 | out.setLocalesDirectory(tempPath.toString()); 59 | ProjectSettingsService.get(getProject()).setState(out); 60 | 61 | InstanceManager.get(getProject()).store().saveToPersistenceLayer(success -> { 62 | }); 63 | 64 | // Compare file structure and contents 65 | IOFileFilter fileFilter = TrueFileFilter.INSTANCE; 66 | 67 | File originalDirectory = new File(Objects.requireNonNull(settings.getLocalesDirectory())); 68 | File[] originalFiles = FileUtils.listFiles(originalDirectory, fileFilter, fileFilter).toArray(new File[0]); 69 | 70 | File outputDirectory = tempPath.toFile(); 71 | File[] outputFiles = FileUtils.listFiles(outputDirectory, fileFilter, fileFilter).toArray(new File[0]); 72 | 73 | Arrays.sort(originalFiles); 74 | Arrays.sort(outputFiles); 75 | 76 | assertEquals(originalFiles.length, outputFiles.length); 77 | 78 | for (int i = 0; i < originalFiles.length; i++) { 79 | File originalFile = originalFiles[i]; 80 | File outputFile = outputFiles[i]; 81 | 82 | // Replace originalFile with os-dependent line-separators 83 | assertEquals(FileUtils.readFileToString(originalFile, CHARSET).replace("\n", System.lineSeparator()), 84 | FileUtils.readFileToString(outputFile, CHARSET)); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/folder/ModularNamespaceFolderStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.folder; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | 5 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 6 | import de.marhali.easyi18n.model.KeyPath; 7 | import de.marhali.easyi18n.model.TranslationData; 8 | import de.marhali.easyi18n.model.TranslationFile; 9 | import de.marhali.easyi18n.model.TranslationNode; 10 | import de.marhali.easyi18n.settings.ProjectSettings; 11 | 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | 20 | /** 21 | * Modular translation folder strategy by namespace. 22 | * Directory => user dir => en.file / de.file 23 | * @author marhali 24 | */ 25 | public class ModularNamespaceFolderStrategy extends FolderStrategy { 26 | 27 | public ModularNamespaceFolderStrategy(@NotNull ProjectSettings settings) { 28 | super(settings); 29 | } 30 | 31 | @Override 32 | public @NotNull List analyzeFolderStructure(@NotNull VirtualFile localesDirectory) { 33 | return new ArrayList<>(findLocaleFiles(new KeyPath(), localesDirectory)); 34 | } 35 | 36 | private List findLocaleFiles(KeyPath ns, VirtualFile dir) { 37 | List files = new ArrayList<>(); 38 | 39 | for (VirtualFile localeFile : dir.getChildren()) { 40 | if(localeFile.isDirectory()) { 41 | if(ns.isEmpty() || settings.isIncludeSubDirs()) { 42 | files.addAll(findLocaleFiles(new KeyPath(ns, localeFile.getName()), localeFile)); 43 | } 44 | continue; 45 | } 46 | 47 | if(super.isFileRelevant(localeFile)) { 48 | files.add(new TranslationFile(localeFile, localeFile.getNameWithoutExtension(), ns)); 49 | } 50 | } 51 | 52 | return files; 53 | } 54 | 55 | @Override 56 | public @NotNull List constructFolderStructure( 57 | @NotNull String localesPath, @NotNull ParserStrategyType type, 58 | @NotNull TranslationData data) throws IOException { 59 | 60 | return new ArrayList<>(this.createLocaleFiles( 61 | localesPath, data.getLocales(), new KeyPath(), type, data.getRootNode())); 62 | } 63 | 64 | private List createLocaleFiles(String localesPath, Set locales, KeyPath path, ParserStrategyType type, TranslationNode node) throws IOException { 65 | List files = new ArrayList<>(); 66 | 67 | for (Map.Entry entry : node.getChildren().entrySet()) { 68 | String parentPath = localesPath + "/" + String.join("/", path); 69 | 70 | // Root-Node or is directory(includeSubDirs) 71 | if(path.isEmpty() || super.exists(parentPath, entry.getKey())) { 72 | files.addAll(createLocaleFiles(localesPath, locales, new KeyPath(path, entry.getKey()), type, entry.getValue())); 73 | continue; 74 | } 75 | 76 | for (String locale : locales) { 77 | VirtualFile vf = super.constructFile(parentPath, locale + "." + type.getFileExtension()); 78 | files.add(new TranslationFile(vf, locale, path)); 79 | } 80 | } 81 | 82 | return files; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/InstanceManager.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.project.Project; 5 | 6 | import de.marhali.easyi18n.model.action.TranslationUpdate; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.Map; 11 | import java.util.WeakHashMap; 12 | 13 | /** 14 | * Central singleton component for managing an easy-i18n instance for a specific project. 15 | * @author marhali 16 | */ 17 | public class InstanceManager { 18 | 19 | private static final Map INSTANCES = new WeakHashMap<>(); 20 | 21 | private final DataStore store; 22 | private final DataBus bus; 23 | private final FilteredDataBus uiBus; 24 | 25 | public static InstanceManager get(@NotNull Project project) { 26 | InstanceManager instance = INSTANCES.get(project); 27 | 28 | if(instance == null){ 29 | instance = new InstanceManager(project); 30 | INSTANCES.put(project, instance); 31 | } 32 | 33 | return instance; 34 | } 35 | 36 | private InstanceManager(@NotNull Project project) { 37 | this.store = new DataStore(project); 38 | this.bus = new DataBus(); 39 | this.uiBus = new FilteredDataBus(project); 40 | 41 | // Register ui eventbus on top of the normal eventbus 42 | this.bus.addListener(this.uiBus); 43 | 44 | // Load data after first initialization 45 | ApplicationManager.getApplication().invokeLater(() -> { 46 | this.store.loadFromPersistenceLayer((success) -> { 47 | this.bus.propagate().onUpdateData(this.store.getData()); 48 | }); 49 | }); 50 | } 51 | 52 | public DataStore store() { 53 | return this.store; 54 | } 55 | 56 | /** 57 | * Primary eventbus. 58 | */ 59 | public DataBus bus() { 60 | return this.bus; 61 | } 62 | 63 | /** 64 | * UI optimized eventbus with builtin filter logic. 65 | */ 66 | public FilteredDataBus uiBus() { 67 | return this.uiBus; 68 | } 69 | 70 | /** 71 | * Reloads the plugin instance. Unsaved cached data will be deleted. 72 | * Fetches data from persistence layer and notifies all endpoints via {@link DataBus}. 73 | */ 74 | public void reload() { 75 | store.loadFromPersistenceLayer((success) -> 76 | bus.propagate().onUpdateData(store.getData())); 77 | } 78 | 79 | public void processUpdate(TranslationUpdate update) { 80 | if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation 81 | this.store.getData().setTranslation(update.getOrigin().getKey(), null); 82 | } 83 | 84 | if(!update.isDeletion()) { // Create or re-create translation with changed data 85 | this.store.getData().setTranslation(update.getChange().getKey(), update.getChange().getValue()); 86 | } 87 | 88 | this.store.saveToPersistenceLayer(success -> { 89 | if(success) { 90 | this.bus.propagate().onUpdateData(this.store.getData()); 91 | 92 | if(!update.isDeletion()) { 93 | this.bus.propagate().onFocusKey(update.getChange().getKey()); 94 | } else { 95 | this.bus.propagate().onFocusKey(update.getOrigin().getKey()); 96 | } 97 | } 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/de/marhali/easyi18n/io/folder/FolderStrategy.java: -------------------------------------------------------------------------------- 1 | package de.marhali.easyi18n.io.folder; 2 | 3 | import com.intellij.openapi.vfs.LocalFileSystem; 4 | import com.intellij.openapi.vfs.VirtualFile; 5 | 6 | import de.marhali.easyi18n.io.parser.ParserStrategyType; 7 | import de.marhali.easyi18n.model.TranslationData; 8 | import de.marhali.easyi18n.model.TranslationFile; 9 | import de.marhali.easyi18n.settings.ProjectSettings; 10 | import de.marhali.easyi18n.util.WildcardRegexMatcher; 11 | 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.List; 17 | import java.util.Objects; 18 | 19 | /** 20 | * Represents a specific translation file directory structure. 21 | * @author marhali 22 | */ 23 | public abstract class FolderStrategy { 24 | 25 | protected final @NotNull ProjectSettings settings; 26 | 27 | public FolderStrategy(@NotNull ProjectSettings settings) { 28 | this.settings = settings; 29 | } 30 | 31 | /** 32 | * Searches the translation folder for matching files based on the implementing strategy. 33 | * The provided directory is already checked as a directory and can be used to query child items. 34 | * @param localesDirectory Configured translation file directory 35 | * @return translation files which matches the strategy 36 | */ 37 | public abstract @NotNull List analyzeFolderStructure(@NotNull VirtualFile localesDirectory); 38 | 39 | /** 40 | * Analyzes the provided translation data and returns the directory structure based on the implementing strategy 41 | * @param localesPath Configured locales path 42 | * @param data Translation data to use for write action 43 | * @return translation file structure 44 | */ 45 | public abstract @NotNull List constructFolderStructure(@NotNull String localesPath, 46 | @NotNull ParserStrategyType type, @NotNull TranslationData data) throws IOException; 47 | 48 | /** 49 | * Checks if the provided file is not a directory and matches the configured file pattern 50 | * @param file File to check 51 | * @return true if file matches and should be processed 52 | */ 53 | protected boolean isFileRelevant(@NotNull VirtualFile file) { 54 | return !file.isDirectory() 55 | && WildcardRegexMatcher.matchWildcardRegex(file.getName(), settings.getFilePattern()); 56 | } 57 | 58 | /** 59 | * 60 | * @param parent Directory path 61 | * @param child File name with extension 62 | * @return IntelliJ {@link VirtualFile} 63 | * @throws IOException Could not access file 64 | */ 65 | protected @NotNull VirtualFile constructFile(@NotNull String parent, @NotNull String child) throws IOException { 66 | File file = new File(parent, child); 67 | file.getParentFile().mkdirs(); 68 | boolean exists = file.createNewFile(); 69 | 70 | VirtualFile vf = exists 71 | ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) 72 | : LocalFileSystem.getInstance().findFileByIoFile(file); 73 | 74 | return Objects.requireNonNull(vf); 75 | } 76 | 77 | /** 78 | * Checks whether a given file or directory exists 79 | * @param parent Parent path 80 | * @param child File / Directory name 81 | * @return true if file is existing otherwise false 82 | */ 83 | protected boolean exists(@NotNull String parent, @NotNull String child) { 84 | return LocalFileSystem.getInstance().findFileByIoFile(new File(parent, child)) != null; 85 | } 86 | } 87 | --------------------------------------------------------------------------------