diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 70d86ad..f6b0d11 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,7 +3,15 @@
version: 2
updates:
+ # Maintain dependencies for Gradle dependencies
- package-ecosystem: "gradle"
directory: "/"
+ target-branch: "next"
schedule:
interval: "daily"
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ target-branch: "next"
+ schedule:
+ interval: "daily"
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f9ada39..47d5a19 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,111 +1,53 @@
# GitHub Actions Workflow created for testing and preparing the plugin release in following steps:
# - validate Gradle Wrapper,
-# - run test and verifyPlugin tasks,
-# - run buildPlugin task and prepare artifact for the further tests,
-# - run IntelliJ Plugin Verifier,
+# - run 'test' and 'verifyPlugin' tasks,
+# - run Qodana inspections,
+# - run 'buildPlugin' task and prepare artifact for the further tests,
+# - run 'runPluginVerifier' task,
# - create a draft release.
#
# Workflow is triggered on push and pull_request events.
#
-# Docs:
-# - GitHub Actions: https://help.github.com/en/actions
-# - IntelliJ Plugin Verifier GitHub Action: https://github.com/ChrisCarini/intellij-platform-plugin-verifier-action
+# GitHub Actions reference: https://help.github.com/en/actions
#
## JBIJPPTPL
name: Build
-on: [push, pull_request]
+on:
+ # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests)
+ push:
+ branches: [main]
+ # Trigger the workflow on any pull request
+ pull_request:
+
jobs:
# Run Gradle Wrapper Validation Action to verify the wrapper's checksum
- gradleValidation:
- name: Gradle Wrapper
+ # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks
+ # Build plugin and provide the artifact for the next workflow jobs
+ build:
+ name: Build
runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.properties.outputs.version }}
+ changelog: ${{ steps.properties.outputs.changelog }}
steps:
# Check out current repository
- name: Fetch Sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v2.4.0
# Validate wrapper
- name: Gradle Wrapper Validation
- uses: gradle/wrapper-validation-action@v1.0.3
+ uses: gradle/wrapper-validation-action@v1.0.4
- # Run verifyPlugin and test Gradle tasks
- test:
- name: Test
- needs: gradleValidation
- runs-on: ubuntu-latest
- steps:
-
- # Setup Java 1.8 environment for the next steps
+ # Setup Java 11 environment for the next steps
- name: Setup Java
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v2
with:
+ distribution: zulu
java-version: 11
-
- # Check out current repository
- - name: Fetch Sources
- uses: actions/checkout@v2
-
- # Cache Gradle dependencies
- - name: Setup Gradle Dependencies Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/caches
- key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
-
- # Cache Gradle Wrapper
- - name: Setup Gradle Wrapper Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
-
- # Run detekt, ktlint and tests
- - name: Run Linters and Test
- run: ./gradlew check
-
- # Run verifyPlugin Gradle task
- - name: Verify Plugin
- run: ./gradlew verifyPlugin
-
- # Build plugin with buildPlugin Gradle task and provide the artifact for the next workflow jobs
- # Requires test job to be passed
- build:
- name: Build
- needs: test
- runs-on: ubuntu-latest
- outputs:
- name: ${{ steps.properties.outputs.name }}
- version: ${{ steps.properties.outputs.version }}
- changelog: ${{ steps.properties.outputs.changelog }}
- artifact: ${{ steps.properties.outputs.artifact }}
- steps:
-
- # Setup Java 1.8 environment for the next steps
- - name: Setup Java
- uses: actions/setup-java@v1
- with:
- java-version: 11
-
- # Check out current repository
- - name: Fetch Sources
- uses: actions/checkout@v2
-
- # Cache Gradle Dependencies
- - name: Setup Gradle Dependencies Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/caches
- key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
-
- # Cache Gradle Wrapper
- - name: Setup Gradle Wrapper Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+ cache: gradle
# Set environment variables
- name: Export Properties
@@ -119,128 +61,99 @@ jobs:
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
- ARTIFACT="${NAME}-${VERSION}.zip"
-
echo "::set-output name=version::$VERSION"
echo "::set-output name=name::$NAME"
echo "::set-output name=changelog::$CHANGELOG"
- echo "::set-output name=artifact::$ARTIFACT"
-
- # Build artifact using buildPlugin Gradle task
- - name: Build Plugin
- run: ./gradlew buildPlugin
-
- # Upload plugin artifact to make it available in the next jobs
- - name: Upload artifact
- uses: actions/upload-artifact@v1
- with:
- name: plugin-artifact
- path: ./build/distributions/${{ steps.properties.outputs.artifact }}
-
- # Verify built plugin using IntelliJ Plugin Verifier tool
- # Requires build job to be passed
- verify:
- name: Verify
- needs: build
- runs-on: ubuntu-latest
- steps:
-
- # Setup Java 1.8 environment for the next steps
- - name: Setup Java
- uses: actions/setup-java@v1
- with:
- java-version: 11
-
- # Check out current repository
- - name: Fetch Sources
- uses: actions/checkout@v2
-
- # Cache Gradle Dependencies
- - name: Setup Gradle Dependencies Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/caches
- key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
-
- # Cache Gradle Wrapper
- - name: Setup Gradle Wrapper Cache
- uses: actions/cache@v2
- with:
- path: ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
-
- # Set environment variables
- - name: Export Properties
- id: properties
- shell: bash
- run: |
- PROPERTIES="$(./gradlew properties --console=plain -q)"
- IDE_VERSIONS="$(echo "$PROPERTIES" | grep "^pluginVerifierIdeVersions:" | base64)"
-
- echo "::set-output name=ideVersions::$IDE_VERSIONS"
echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier"
+ ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier
+ # Run tests
+ - name: Run Tests
+ run: ./gradlew test
+
+ # Collect Tests Result of failed tests
+ - name: Collect Tests Result
+ if: ${{ failure() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: tests-result
+ path: ${{ github.workspace }}/build/reports/tests
# Cache Plugin Verifier IDEs
- name: Setup Plugin Verifier IDEs Cache
- uses: actions/cache@v2
+ uses: actions/cache@v2.1.6
with:
path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides
- key: ${{ runner.os }}-plugin-verifier-${{ steps.properties.outputs.ideVersions }}
+ key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
- # Run IntelliJ Plugin Verifier action using GitHub Action
- - name: Verify Plugin
+ # Run Verify Plugin task and IntelliJ Plugin Verifier tool
+ - name: Run Plugin Verification tasks
run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }}
+ # Collect Plugin Verifier Result
+ - name: Collect Plugin Verifier Result
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: pluginVerifier-result
+ path: ${{ github.workspace }}/build/reports/pluginVerifier
+
+ # Run Qodana inspections
+ - name: Qodana - Code Inspection
+ uses: JetBrains/qodana-action@v2.1-eap
+
+ # Collect Qodana Result
+ - name: Collect Qodana Result
+ uses: actions/upload-artifact@v2
+ with:
+ name: qodana-result
+ path: ${{ github.workspace }}/qodana
+
+ # Prepare plugin archive content for creating artifact
+ - name: Prepare Plugin Artifact
+ id: artifact
+ shell: bash
+ run: |
+ cd ${{ github.workspace }}/build/distributions
+ FILENAME=`ls *.zip`
+ unzip "$FILENAME" -d content
+ echo "::set-output name=filename::$FILENAME"
+ # Store already-built plugin as an artifact for downloading
+ - name: Upload artifact
+ uses: actions/upload-artifact@v2.2.4
+ with:
+ name: ${{ steps.artifact.outputs.filename }}
+ path: ./build/distributions/content/*/*
+
# Prepare a draft release for GitHub Releases page for the manual verification
# If accepted and published, release workflow would be triggered
releaseDraft:
name: Release Draft
if: github.event_name != 'pull_request'
- needs: [build, verify]
+ needs: build
runs-on: ubuntu-latest
steps:
# Check out current repository
- name: Fetch Sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v2.4.0
# Remove old release drafts by using the curl request for the available releases with draft flag
- name: Remove Old Release Drafts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases \
- | tr '\r\n' ' ' \
- | jq '.[] | select(.draft == true) | .id' \
- | xargs -I '{}' \
- curl -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases/{}
-
+ gh api repos/{owner}/{repo}/releases \
+ --jq '.[] | select(.draft == true) | .id' \
+ | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{}
# Create new release draft - which is not publicly visible and requires manual acceptance
- name: Create Release Draft
- id: createDraft
- uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: v${{ needs.build.outputs.version }}
- release_name: v${{ needs.build.outputs.version }}
- body: ${{ needs.build.outputs.changelog }}
- draft: true
-
- # Download plugin artifact provided by the previous job
- - name: Download Artifact
- uses: actions/download-artifact@v2
- with:
- name: plugin-artifact
-
- # Upload artifact as a release asset
- - name: Upload Release Asset
- id: upload-release-asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.createDraft.outputs.upload_url }}
- asset_path: ./${{ needs.build.outputs.artifact }}
- asset_name: ${{ needs.build.outputs.artifact }}
- asset_content_type: application/zip
+ run: |
+ gh release create v${{ needs.build.outputs.version }} \
+ --draft \
+ --title "v${{ needs.build.outputs.version }}" \
+ --notes "$(cat << 'EOM'
+ ${{ needs.build.outputs.changelog }}
+ EOM
+ )"
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a7afcbe..fff3941 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,57 +14,66 @@ jobs:
runs-on: ubuntu-latest
steps:
- # Setup Java 1.8 environment for the next steps
- - name: Setup Java
- uses: actions/setup-java@v1
- with:
- java-version: 11
-
# Check out current repository
- name: Fetch Sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v2.4.0
with:
ref: ${{ github.event.release.tag_name }}
+ # Setup Java 11 environment for the next steps
+ - name: Setup Java
+ uses: actions/setup-java@v2
+ with:
+ distribution: zulu
+ java-version: 11
+ cache: gradle
+
+ # Set environment variables
+ - name: Export Properties
+ id: properties
+ shell: bash
+ run: |
+ CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
+ ${{ github.event.release.body }}
+ EOM
+ )"
+
+ echo "::set-output name=changelog::$CHANGELOG"
+ # Update Unreleased section with the current release note
+ - name: Patch Changelog
+ if: ${{ steps.properties.outputs.changelog != '' }}
+ run: |
+ ./gradlew patchChangelog --release-note "$(cat << 'EOM'
+ ${{ steps.properties.outputs.changelog }}
+ EOM
+ )"
# Publish the plugin to the Marketplace
- name: Publish Plugin
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: ./gradlew publishPlugin
- # Patch changelog, commit and push to the current repository
- changelog:
- name: Update Changelog
- needs: release
- runs-on: ubuntu-latest
- steps:
+ # Upload artifact as a release asset
+ - name: Upload Release Asset
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
- # Setup Java 1.8 environment for the next steps
- - name: Setup Java
- uses: actions/setup-java@v1
- with:
- java-version: 11
-
- # Check out current repository
- - name: Fetch Sources
- uses: actions/checkout@v2
- with:
- ref: ${{ github.event.release.tag_name }}
-
- # Update Unreleased section with the current version
- - name: Patch Changelog
- run: ./gradlew patchChangelog
-
- # Commit patched Changelog
- - name: Commit files
+ # Create pull request
+ - name: Create Pull Request
+ if: ${{ steps.properties.outputs.changelog != '' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- git config --local user.email "action@github.com"
- git config --local user.name "GitHub Action"
- git commit -m "Update changelog" -a
-
- # Push changes
- - name: Push changes
- uses: ad-m/github-push-action@master
- with:
- branch: main
- github_token: ${{ secrets.GITHUB_TOKEN }}
+ VERSION="${{ github.event.release.tag_name }}"
+ BRANCH="changelog-update-$VERSION"
+ git config user.email "action@github.com"
+ git config user.name "GitHub Action"
+ git checkout -b $BRANCH
+ git commit -am "Changelog update - $VERSION"
+ git push --set-upstream origin $BRANCH
+ gh pr create \
+ --title "Changelog update - \`$VERSION\`" \
+ --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
+ --base main \
+ --head $BRANCH
\ No newline at end of file
diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml
new file mode 100644
index 0000000..1027273
--- /dev/null
+++ b/.github/workflows/run-ui-tests.yml
@@ -0,0 +1,60 @@
+# GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps:
+# - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI
+# - wait for IDE to start
+# - run UI tests with separate Gradle task
+#
+# Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform
+#
+# Workflow is triggered manually.
+
+name: Run UI Tests
+on:
+ workflow_dispatch
+
+jobs:
+
+ testUI:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ runIde: |
+ export DISPLAY=:99.0
+ Xvfb -ac :99 -screen 0 1920x1080x16 &
+ gradle runIdeForUiTests &
+ - os: windows-latest
+ runIde: start gradlew.bat runIdeForUiTests
+ - os: macos-latest
+ runIde: ./gradlew runIdeForUiTests &
+
+ steps:
+
+ # Check out current repository
+ - name: Fetch Sources
+ uses: actions/checkout@v2.4.0
+
+ # Setup Java 11 environment for the next steps
+ - name: Setup Java
+ uses: actions/setup-java@v2
+ with:
+ distribution: zulu
+ java-version: 11
+ cache: gradle
+
+ # Run IDEA prepared for UI testing
+ - name: Run IDE
+ run: ${{ matrix.runIde }}
+
+ # Wait for IDEA to be started
+ - name: Health Check
+ uses: jtalk/url-health-check-action@v2
+ with:
+ url: http://127.0.0.1:8082
+ max-attempts: 15
+ retry-delay: 30s
+
+ # Run tests
+ - name: Tests
+ run: ./gradlew test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e0d53d8..61032ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.gradle
.idea
-build
+.qodana
+build
\ No newline at end of file
diff --git a/.run/Run IDE for UI Tests.run.xml b/.run/Run IDE for UI Tests.run.xml
new file mode 100644
index 0000000..5b76189
--- /dev/null
+++ b/.run/Run IDE for UI Tests.run.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/Run IDE with Plugin.run.xml b/.run/Run IDE with Plugin.run.xml
index d15ff68..f42721a 100644
--- a/.run/Run IDE with Plugin.run.xml
+++ b/.run/Run IDE with Plugin.run.xml
@@ -1,24 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
\ No newline at end of file
diff --git a/.run/Run Plugin Tests.run.xml b/.run/Run Plugin Tests.run.xml
index 03d0287..5e7d02e 100644
--- a/.run/Run Plugin Tests.run.xml
+++ b/.run/Run Plugin Tests.run.xml
@@ -1,24 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
\ No newline at end of file
diff --git a/.run/Run Plugin Verification.run.xml b/.run/Run Plugin Verification.run.xml
index 3a8d688..1c1be17 100644
--- a/.run/Run Plugin Verification.run.xml
+++ b/.run/Run Plugin Verification.run.xml
@@ -1,26 +1,26 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
+
\ No newline at end of file
diff --git a/.run/Run Qodana.run.xml b/.run/Run Qodana.run.xml
new file mode 100644
index 0000000..17b041a
--- /dev/null
+++ b/.run/Run Qodana.run.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f6d85c..059334c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,20 @@
# easy-i18n Changelog
## [Unreleased]
+### Added
+- The search function now supports full-text-search
+- Automatically reload translation data on file system change
+- Sorting of translation keys can now be disabled via configuration
+- Key section nesting can be disabled via configuration
+- Numbers will be stored as number type whenever possible
+- Code signing of plugin source
+
+### Changed
+- Better focus keys in tree-view after edit
+- Optimized internal data structure (io, cache, events)
+- Adjusted compatibility matrix to 2020.3 - 2021.3
+- Updated dependencies and improved README file
+
## [1.5.1]
### Fixed
- Exception on key annotation if path-prefix is undefined
diff --git a/README.md b/README.md
index 5dd8911..d4676bb 100644
--- a/README.md
+++ b/README.md
@@ -6,29 +6,40 @@
[](https://paypal.me/marhalide)
-This is an easy plugin to manage internationalization for JSON or Resource-Bundle(Properties) based locale files.
-Most common use case is for translating Webapps or simple Java Applications. Translating large scale projects was never that easy with your favourite IDE!
+This is a plugin for easier management of translation files of projects that need to be translated into different languages. Translating large projects has never been so easy with your favorite IDE!
## Use Cases
-- Webapps: For example [Vue](https://vuejs.org/) with [vue-i18n](https://kazupon.github.io/vue-i18n/) or any other JSON translation file based technology
-- Java based Resource-Bundle
+- Webapps: [Vue](https://vuejs.org/) with [vue-i18n](https://kazupon.github.io/vue-i18n/), [React](https://reactjs.org/) or any other json based technology
+- Java projects based on Resource-Bundle's
+- Projects that uses yaml, json or properties as locale file base for internationalization
## Features
-- UI Tool Window with Table- and Tree-View representation
+- UI Tool Window which supports tree- or table-view
- Easily Add / Edit / Delete translations
-- Filter / Search function to hide irrelevant keys
-- Key completion and annotation inside editor
-- Configurable locales directory & preferred locale for ui presentation
-- Supports modularized (splitted) json files
-- Translation keys with missing definition for any locale will be displayed red
-- Quick edit any translation by right-click (IntelliJ Popup Action)
-- Quick delete any translation via DEL-Key
+- Filter function with full-text-search support
+- Editor Assistance: Key completion, annotation and referencing
+- Key sorting and nesting can be configured
+- Configurable locales directory & preferred locale for ui presentation
+- Missing language translations will be indicated red
+- Quick actions: right-click or DEL to edit or delete a translation
+- Automatically reloads translation data if any locale file was changed
## Screenshots
-
-
-
+
+
+
+
+
+
+
+## Supported IO Strategies (locale files)
+- Json: json files inside locales directory
+- Namespaced Json: Multiple json files per locale directory
+- Yaml: yml or yaml files inside locales directory
+- Properties: properties files inside locales directory
+
+If there are any files in the locales folder that should not be processed, they can be ignored with the Translation file pattern option.
## Installation
- Using IDE built-in plugin system:
@@ -45,8 +56,8 @@ Most common use case is for translating Webapps or simple Java Applications. Tra
- Install plugin. See **Installation** section
- Create a directory which will hold the locale files
- Create a file for each required locale (e.g de.json, en.json) Note: Each json file must at least define an empty section (e.g. **{}**)
-- Click on the **Settings** Action inside the Easy I18n Tool Window
-- Select the created directory (optional: define the preferred locale to view) and press Ok
+- Click on the **Settings** Action inside the EasyI18n Tool Window
+- Select the created directory (optional: define the preferred locale to view) and press **Ok**
- Translations can now be created / edited or deleted
Examples for the configuration can be found in the [/example](https://github.com/marhali/easy-i18n/tree/main/example) folder.
diff --git a/build.gradle.kts b/build.gradle.kts
index e817fcc..800cf61 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,3 @@
-import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -8,15 +7,13 @@ plugins {
// Java support
id("java")
// Kotlin support
- id("org.jetbrains.kotlin.jvm") version "1.5.10"
- // gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
- id("org.jetbrains.intellij") version "1.0"
- // gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
- id("org.jetbrains.changelog") version "1.1.2"
- // detekt linter - read more: https://detekt.github.io/detekt/gradle.html
- id("io.gitlab.arturbosch.detekt") version "1.17.1"
- // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
- id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
+ id("org.jetbrains.kotlin.jvm") version "1.5.31"
+ // Gradle IntelliJ Plugin
+ id("org.jetbrains.intellij") version "1.2.1"
+ // Gradle Changelog Plugin
+ id("org.jetbrains.changelog") version "1.3.1"
+ // Gradle Qodana Plugin
+ id("org.jetbrains.qodana") version "0.1.13"
}
group = properties("pluginGroup")
@@ -26,55 +23,45 @@ version = properties("pluginVersion")
repositories {
mavenCentral()
}
-dependencies {
- detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
-}
-// Configure gradle-intellij-plugin plugin.
-// Read more: https://github.com/JetBrains/gradle-intellij-plugin
+// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
intellij {
pluginName.set(properties("pluginName"))
version.set(properties("platformVersion"))
type.set(properties("platformType"))
- downloadSources.set(properties("platformDownloadSources").toBoolean())
- updateSinceUntilBuild.set(true)
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty))
}
-// Configure gradle-changelog-plugin plugin.
-// Read more: https://github.com/JetBrains/gradle-changelog-plugin
+// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
changelog {
- version = properties("pluginVersion")
- groups = emptyList()
+ version.set(properties("pluginVersion"))
+ groups.set(emptyList())
}
-// Configure detekt plugin.
-// Read more: https://detekt.github.io/detekt/kotlindsl.html
-detekt {
- config = files("./detekt-config.yml")
- buildUponDefaultConfig = true
-
- reports {
- html.enabled = false
- xml.enabled = false
- txt.enabled = false
- }
+// Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin
+qodana {
+ cachePath.set(projectDir.resolve(".qodana").canonicalPath)
+ reportPath.set(projectDir.resolve("build/reports/inspections").canonicalPath)
+ saveReport.set(true)
+ showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false)
}
tasks {
- // Set the compatibility versions to 1.8
- withType {
- sourceCompatibility = "1.8"
- targetCompatibility = "1.8"
- }
- withType {
- kotlinOptions.jvmTarget = "1.8"
+ // Set the JVM compatibility versions
+ properties("javaVersion").let {
+ withType {
+ sourceCompatibility = it
+ targetCompatibility = it
+ }
+ withType {
+ kotlinOptions.jvmTarget = it
+ }
}
- withType {
- jvmTarget = "1.8"
+ wrapper {
+ gradleVersion = properties("gradleVersion")
}
patchPluginXml {
@@ -84,7 +71,7 @@ tasks {
// Extract the section from README.md and provide for the plugin's manifest
pluginDescription.set(
- File(projectDir, "README.md").readText().lines().run {
+ projectDir.resolve("README.md").readText().lines().run {
val start = ""
val end = ""
@@ -96,11 +83,26 @@ tasks {
)
// Get the latest available change notes from the changelog file
- changeNotes.set(provider { changelog.getLatest().toHTML() })
+ changeNotes.set(provider {
+ changelog.run {
+ getOrNull(properties("pluginVersion")) ?: getLatest()
+ }.toHTML()
+ })
}
- runPluginVerifier {
- ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty))
+ // Configure UI tests plugin
+ // Read more: https://github.com/JetBrains/intellij-ui-test-robot
+ runIdeForUiTests {
+ systemProperty("robot-server.port", "8082")
+ systemProperty("ide.mac.message.dialogs.as.sheets", "false")
+ systemProperty("jb.privacy.policy.text", "")
+ systemProperty("jb.consents.confirmation.enabled", "false")
+ }
+
+ signPlugin {
+ certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
+ privateKey.set(System.getenv("PRIVATE_KEY"))
+ password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
@@ -111,4 +113,4 @@ tasks {
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first()))
}
-}
+}
\ No newline at end of file
diff --git a/detekt-config.yml b/detekt-config.yml
deleted file mode 100644
index f9b8d75..0000000
--- a/detekt-config.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default detekt configuration:
-# https://github.com/detekt/detekt/blob/master/detekt-core/src/main/resources/default-detekt-config.yml
-
-formatting:
- Indentation:
- continuationIndentSize: 8
- ParameterListWrapping:
- indentSize: 8
diff --git a/example/images/Completion.PNG b/example/images/Completion.PNG
deleted file mode 100644
index 239fd68..0000000
Binary files a/example/images/Completion.PNG and /dev/null differ
diff --git a/example/images/TableView.PNG b/example/images/TableView.PNG
deleted file mode 100644
index 878dfce..0000000
Binary files a/example/images/TableView.PNG and /dev/null differ
diff --git a/example/images/TreeView.PNG b/example/images/TreeView.PNG
deleted file mode 100644
index f0a7247..0000000
Binary files a/example/images/TreeView.PNG and /dev/null differ
diff --git a/example/images/key-annotation.PNG b/example/images/key-annotation.PNG
new file mode 100644
index 0000000..df0ca9a
Binary files /dev/null and b/example/images/key-annotation.PNG differ
diff --git a/example/images/key-completion.PNG b/example/images/key-completion.PNG
new file mode 100644
index 0000000..3b1bd01
Binary files /dev/null and b/example/images/key-completion.PNG differ
diff --git a/example/images/key-edit.PNG b/example/images/key-edit.PNG
new file mode 100644
index 0000000..d6ab6ef
Binary files /dev/null and b/example/images/key-edit.PNG differ
diff --git a/example/images/settings.PNG b/example/images/settings.PNG
new file mode 100644
index 0000000..29cf385
Binary files /dev/null and b/example/images/settings.PNG differ
diff --git a/example/images/table-view.PNG b/example/images/table-view.PNG
new file mode 100644
index 0000000..fc02213
Binary files /dev/null and b/example/images/table-view.PNG differ
diff --git a/example/images/tree-view.PNG b/example/images/tree-view.PNG
new file mode 100644
index 0000000..7bf34da
Binary files /dev/null and b/example/images/tree-view.PNG differ
diff --git a/gradle.properties b/gradle.properties
index c31f92e..5ddf786 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,24 +3,29 @@
pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n
-pluginVersion = 1.5.1
+# SemVer format -> https://semver.org
+pluginVersion = 1.6.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
-pluginSinceBuild = 202
-pluginUntilBuild = 212.*
-
-# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl
-# See https://jb.gg/intellij-platform-builds-list for available build versions
-pluginVerifierIdeVersions = 2020.2.4, 2020.3.4, 2021.2
+pluginSinceBuild = 203
+pluginUntilBuild = 213.*
+# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
platformType = IC
-platformVersion = 2021.2
-platformDownloadSources = true
+platformVersion = 2020.3.4
+
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = org.jetbrains.kotlin
+# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
+javaVersion = 11
+
+# Gradle Releases -> https://github.com/gradle/gradle/releases
+gradleVersion = 7.3
+
# Opt-out flag for bundling Kotlin standard library.
-# See https://kotlinlang.org/docs/reference/using-gradle.html#dependency-on-the-standard-library for details.
+# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
+# suppress inspection "UnusedProperty"
kotlin.stdlib.default.dependency = false
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c..7454180 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 442d913..e750102 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0..744e882 100755
--- a/gradlew
+++ b/gradlew
@@ -72,7 +72,7 @@ case "`uname`" in
Darwin* )
darwin=true
;;
- MINGW* )
+ MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
diff --git a/qodana.yml b/qodana.yml
new file mode 100644
index 0000000..8b73731
--- /dev/null
+++ b/qodana.yml
@@ -0,0 +1,6 @@
+# Qodana configuration:
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html
+
+version: 1.0
+profile:
+ name: qodana.recommended
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/DataBus.java b/src/main/java/de/marhali/easyi18n/DataBus.java
new file mode 100644
index 0000000..101a8fd
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/DataBus.java
@@ -0,0 +1,55 @@
+package de.marhali.easyi18n;
+
+import de.marhali.easyi18n.model.bus.BusListener;
+import de.marhali.easyi18n.model.TranslationData;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Data-bus which is used to distribute changes regarding translations or ui tools to the participating components.
+ * @author marhali
+ */
+public class DataBus {
+
+ private final Set listener;
+
+ protected DataBus() {
+ this.listener = new HashSet<>();
+ }
+
+ /**
+ * Adds a participant to the event bus. Every participant needs to be added manually.
+ * @param listener Bus listener
+ */
+ public void addListener(BusListener listener) {
+ this.listener.add(listener);
+ }
+
+ /**
+ * Fires the called events on the returned prototype.
+ * The event will be distributed to all participants which were registered at execution time.
+ * @return Listener prototype
+ */
+ public BusListener propagate() {
+ return new BusListener() {
+ @Override
+ public void onUpdateData(@NotNull TranslationData data) {
+ listener.forEach(li -> li.onUpdateData(data));
+ }
+
+ @Override
+ public void onFocusKey(@Nullable String key) {
+ listener.forEach(li -> li.onFocusKey(key));
+ }
+
+ @Override
+ public void onSearchQuery(@Nullable String query) {
+ listener.forEach(li -> li.onSearchQuery(query));
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/DataStore.java b/src/main/java/de/marhali/easyi18n/DataStore.java
new file mode 100644
index 0000000..d8fcd83
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/DataStore.java
@@ -0,0 +1,118 @@
+package de.marhali.easyi18n;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.*;
+
+import de.marhali.easyi18n.io.IOStrategy;
+import de.marhali.easyi18n.io.json.JsonIOStrategy;
+import de.marhali.easyi18n.io.json.ModularizedJsonIOStrategy;
+import de.marhali.easyi18n.io.properties.PropertiesIOStrategy;
+import de.marhali.easyi18n.io.yaml.YamlIOStrategy;
+import de.marhali.easyi18n.model.SettingsState;
+import de.marhali.easyi18n.model.TranslationData;
+import de.marhali.easyi18n.service.FileChangeListener;
+import de.marhali.easyi18n.service.SettingsService;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Responsible for loading, saving and updating translation files.
+ * Provides access to the cached translation data which is used in the whole project.
+ * @author marhali
+ */
+public class DataStore {
+
+ private static final Set STRATEGIES = new LinkedHashSet<>(Arrays.asList(
+ new JsonIOStrategy(), new ModularizedJsonIOStrategy(),
+ new YamlIOStrategy("yaml"), new YamlIOStrategy("yml"),
+ new PropertiesIOStrategy()
+ ));
+
+ private final @NotNull Project project;
+ private final @NotNull FileChangeListener changeListener;
+
+ private @NotNull TranslationData data;
+
+ protected DataStore(@NotNull Project project) {
+ this.project = project;
+ this.data = new TranslationData(true, true); // Initialize with hard-coded configuration
+ this.changeListener = new FileChangeListener(project);
+
+ VirtualFileManager.getInstance().addAsyncFileListener(
+ this.changeListener, Disposer.newDisposable("EasyI18n"));
+ }
+
+ public @NotNull TranslationData getData() {
+ return data;
+ }
+
+ /**
+ * Loads the translation data into cache and overwrites any previous cached data.
+ * If the configuration does not fit an empty translation instance will be populated.
+ * @param successResult Consumer will inform if operation was successful
+ */
+ public void loadFromPersistenceLayer(@NotNull Consumer successResult) {
+ SettingsState state = SettingsService.getInstance(this.project).getState();
+ String localesPath = state.getLocalesPath();
+
+ if(localesPath == null || localesPath.isEmpty()) { // Populate empty instance
+ this.data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
+ return;
+ }
+
+ this.changeListener.updateLocalesPath(localesPath);
+
+ IOStrategy strategy = this.determineStrategy(state, localesPath);
+
+ strategy.read(this.project, localesPath, state, (data) -> {
+ this.data = data == null
+ ? new TranslationData(state.isSortKeys(), state.isNestedKeys())
+ : data;
+
+ successResult.accept(data != null);
+ });
+ }
+
+ /**
+ * Saves the cached translation data to the underlying io system.
+ * @param successResult Consumer will inform if operation was successful
+ */
+ public void saveToPersistenceLayer(@NotNull Consumer successResult) {
+ SettingsState state = SettingsService.getInstance(this.project).getState();
+ String localesPath = state.getLocalesPath();
+
+ if(localesPath == null || localesPath.isEmpty()) { // Cannot save without valid path
+ successResult.accept(false);
+ return;
+ }
+
+ IOStrategy strategy = this.determineStrategy(state, localesPath);
+
+ strategy.write(this.project, localesPath, state, this.data, successResult);
+ }
+
+ /**
+ * Chooses the right strategy for the opened project. An exception might be thrown on
+ * runtime if the project configuration (e.g. locale files does not fit in any strategy).
+ * @param state Plugin configuration
+ * @param localesPath Locales directory
+ * @return matching {@link IOStrategy}
+ */
+ public @NotNull IOStrategy determineStrategy(@NotNull SettingsState state, @NotNull String localesPath) {
+ for(IOStrategy strategy : STRATEGIES) {
+ if(strategy.canUse(this.project, localesPath, state)) {
+ return strategy;
+ }
+ }
+
+ throw new IllegalArgumentException("Could not determine i18n strategy. " +
+ "At least one locale file must be defined. " +
+ "For examples please visit https://github.com/marhali/easy-i18n");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/InstanceManager.java b/src/main/java/de/marhali/easyi18n/InstanceManager.java
new file mode 100644
index 0000000..129f1be
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/InstanceManager.java
@@ -0,0 +1,76 @@
+package de.marhali.easyi18n;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+
+import de.marhali.easyi18n.model.TranslationUpdate;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Central singleton component for managing an easy-i18n instance for a specific project.
+ * @author marhali
+ */
+public class InstanceManager {
+
+ private static final Map INSTANCES = new WeakHashMap<>();
+
+ private final DataStore store;
+ private final DataBus bus;
+
+ public static InstanceManager get(@NotNull Project project) {
+ InstanceManager instance = INSTANCES.get(project);
+
+ if(instance == null){
+ instance = new InstanceManager(project);
+ INSTANCES.put(project, instance);
+ }
+
+ return instance;
+ }
+
+ private InstanceManager(@NotNull Project project) {
+ this.store = new DataStore(project);
+ this.bus = new DataBus();
+
+ // Load data after first initialization
+ ApplicationManager.getApplication().invokeLater(() -> {
+ this.store.loadFromPersistenceLayer((success) -> {
+ this.bus.propagate().onUpdateData(this.store.getData());
+ });
+ });
+ }
+
+ public DataStore store() {
+ return this.store;
+ }
+
+ public DataBus bus() {
+ return this.bus;
+ }
+
+ public void processUpdate(TranslationUpdate update) {
+ if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation
+ this.store.getData().setTranslation(update.getOrigin().getKey(), null);
+ }
+
+ if(!update.isDeletion()) { // Create or re-create translation with changed data
+ this.store.getData().setTranslation(update.getChange().getKey(), update.getChange().getTranslation());
+ }
+
+ this.store.saveToPersistenceLayer(success -> {
+ if(success) {
+ this.bus.propagate().onUpdateData(this.store.getData());
+
+ if(!update.isDeletion()) {
+ this.bus.propagate().onFocusKey(update.getChange().getKey());
+ } else {
+ this.bus.propagate().onFocusKey(update.getOrigin().getKey());
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/action/AddAction.java b/src/main/java/de/marhali/easyi18n/action/AddAction.java
index 20f9dba..f2c2455 100644
--- a/src/main/java/de/marhali/easyi18n/action/AddAction.java
+++ b/src/main/java/de/marhali/easyi18n/action/AddAction.java
@@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.dialog.AddDialog;
+import de.marhali.easyi18n.util.PathUtil;
import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull;
@@ -42,7 +43,7 @@ public class AddAction extends AnAction {
TreePath path = manager.getTreeView().getTree().getSelectionPath();
if(path != null) {
- return TreeUtil.getFullPath(path) + ".";
+ return TreeUtil.getFullPath(path) + PathUtil.DELIMITER;
}
} else { // Table View
diff --git a/src/main/java/de/marhali/easyi18n/action/ReloadAction.java b/src/main/java/de/marhali/easyi18n/action/ReloadAction.java
index 5930963..323ba44 100644
--- a/src/main/java/de/marhali/easyi18n/action/ReloadAction.java
+++ b/src/main/java/de/marhali/easyi18n/action/ReloadAction.java
@@ -4,7 +4,7 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
-import de.marhali.easyi18n.service.DataStore;
+import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.NotNull;
@@ -23,6 +23,9 @@ public class ReloadAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
- DataStore.getInstance(e.getProject()).reloadFromDisk();
+ InstanceManager manager = InstanceManager.get(e.getProject());
+ manager.store().loadFromPersistenceLayer((success) -> {
+ manager.bus().propagate().onUpdateData(manager.store().getData());
+ });
}
}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java b/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java
index 8a50820..7702fba 100644
--- a/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java
+++ b/src/main/java/de/marhali/easyi18n/dialog/AddDialog.java
@@ -7,8 +7,9 @@ import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
-import de.marhali.easyi18n.service.DataStore;
+import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationCreate;
import javax.swing.*;
@@ -48,16 +49,16 @@ public class AddDialog {
}
private void saveTranslation() {
- Map messages = new HashMap<>();
+ Translation translation = new Translation();
valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) {
- messages.put(k, v.getText());
+ translation.put(k, v.getText());
}
});
- TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), messages));
- DataStore.getInstance(project).processUpdate(creation);
+ TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), translation));
+ InstanceManager.get(project).processUpdate(creation);
}
private DialogBuilder prepare() {
@@ -75,7 +76,8 @@ public class AddDialog {
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
valueTextFields = new HashMap<>();
- for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
+
+ for(String locale : InstanceManager.get(project).store().getData().getLocales()) {
JBLabel localeLabel = new JBLabel(locale);
JBTextField localeText = new JBTextField();
localeLabel.setLabelFor(localeText);
diff --git a/src/main/java/de/marhali/easyi18n/dialog/EditDialog.java b/src/main/java/de/marhali/easyi18n/dialog/EditDialog.java
index 09f3e21..7e788bf 100644
--- a/src/main/java/de/marhali/easyi18n/dialog/EditDialog.java
+++ b/src/main/java/de/marhali/easyi18n/dialog/EditDialog.java
@@ -6,11 +6,12 @@ import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
-import de.marhali.easyi18n.service.DataStore;
+import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationDelete;
-import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor;
+import de.marhali.easyi18n.model.TranslationUpdate;
import javax.swing.*;
import javax.swing.border.EtchedBorder;
@@ -40,23 +41,22 @@ public class EditDialog {
int code = prepare().show();
if(code == DialogWrapper.OK_EXIT_CODE) { // Edit
- DataStore.getInstance(project).processUpdate(new TranslationUpdate(origin, getChanges()));
-
+ InstanceManager.get(project).processUpdate(new TranslationUpdate(origin, getChanges()));
} else if(code == DeleteActionDescriptor.EXIT_CODE) { // Delete
- DataStore.getInstance(project).processUpdate(new TranslationDelete(origin));
+ InstanceManager.get(project).processUpdate(new TranslationDelete(origin));
}
}
private KeyedTranslation getChanges() {
- Map messages = new HashMap<>();
+ Translation translation = new Translation();
valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) {
- messages.put(k, v.getText());
+ translation.put(k, v.getText());
}
});
- return new KeyedTranslation(keyTextField.getText(), messages);
+ return new KeyedTranslation(keyTextField.getText(), translation);
}
private DialogBuilder prepare() {
@@ -74,9 +74,10 @@ public class EditDialog {
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
valueTextFields = new HashMap<>();
- for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
+
+ for(String locale : InstanceManager.get(project).store().getData().getLocales()) {
JBLabel localeLabel = new JBLabel(locale);
- JBTextField localeText = new JBTextField(this.origin.getTranslations().get(locale));
+ JBTextField localeText = new JBTextField(this.origin.getTranslation().get(locale));
localeLabel.setLabelFor(localeText);
valuePanel.add(localeLabel);
diff --git a/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java b/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java
index b774b3d..e4e4c3c 100644
--- a/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java
+++ b/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java
@@ -9,8 +9,9 @@ import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField;
+import de.marhali.easyi18n.InstanceManager;
+import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.service.SettingsService;
-import de.marhali.easyi18n.service.DataStore;
import javax.swing.*;
import java.awt.*;
@@ -28,6 +29,8 @@ public class SettingsDialog {
private JBTextField filePatternText;
private JBTextField previewLocaleText;
private JBTextField pathPrefixText;
+ private JBCheckBox sortKeysCheckbox;
+ private JBCheckBox nestedKeysCheckbox;
private JBCheckBox codeAssistanceCheckbox;
public SettingsDialog(Project project) {
@@ -35,30 +38,31 @@ public class SettingsDialog {
}
public void showAndHandle() {
- String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
- String filePattern = SettingsService.getInstance(project).getState().getFilePattern();
- String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
- String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
- boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance();
+ SettingsState state = SettingsService.getInstance(project).getState();
- if(prepare(localesPath, filePattern, previewLocale, pathPrefix, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
- SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText());
- SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText());
- SettingsService.getInstance(project).getState().setPreviewLocale(previewLocaleText.getText());
- SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected());
- SettingsService.getInstance(project).getState().setPathPrefix(pathPrefixText.getText());
+ if(prepare(state).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
+ state.setLocalesPath(pathText.getText());
+ state.setFilePattern(filePatternText.getText());
+ state.setPreviewLocale(previewLocaleText.getText());
+ state.setPathPrefix(pathPrefixText.getText());
+ state.setSortKeys(sortKeysCheckbox.isSelected());
+ state.setNestedKeys(nestedKeysCheckbox.isSelected());
+ state.setCodeAssistance(codeAssistanceCheckbox.isSelected());
// Reload instance
- DataStore.getInstance(project).reloadFromDisk();
+ InstanceManager manager = InstanceManager.get(project);
+ manager.store().loadFromPersistenceLayer((success) -> {
+ manager.bus().propagate().onUpdateData(manager.store().getData());
+ });
}
}
- private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, String pathPrefix, boolean codeAssistance) {
+ private DialogBuilder prepare(SettingsState state) {
JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
/* path */
JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text"));
- pathText = new TextFieldWithBrowseButton(new JTextField(localesPath));
+ pathText = new TextFieldWithBrowseButton(new JTextField(state.getLocalesPath()));
pathLabel.setLabelFor(pathText);
pathText.addBrowseFolderListener(ResourceBundle.getBundle("messages").getString("settings.path.title"), null, project, new FileChooserDescriptor(
@@ -69,14 +73,14 @@ public class SettingsDialog {
/* file pattern */
JBLabel filePatternLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.file-pattern"));
- filePatternText = new JBTextField(filePattern);
+ filePatternText = new JBTextField(state.getFilePattern());
rootPanel.add(filePatternLabel);
rootPanel.add(filePatternText);
/* preview locale */
JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview"));
- previewLocaleText = new JBTextField(previewLocale);
+ previewLocaleText = new JBTextField(state.getPreviewLocale());
previewLocaleLabel.setLabelFor(previewLocaleText);
rootPanel.add(previewLocaleLabel);
@@ -84,14 +88,26 @@ public class SettingsDialog {
/* path prefix */
JBLabel pathPrefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.prefix"));
- pathPrefixText = new JBTextField(pathPrefix);
+ pathPrefixText = new JBTextField(state.getPathPrefix());
rootPanel.add(pathPrefixLabel);
rootPanel.add(pathPrefixText);
+ /* sort keys */
+ sortKeysCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.keys.sort"));
+ sortKeysCheckbox.setSelected(state.isSortKeys());
+
+ rootPanel.add(sortKeysCheckbox);
+
+ /* nested keys */
+ nestedKeysCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.keys.nested"));
+ nestedKeysCheckbox.setSelected(state.isNestedKeys());
+
+ rootPanel.add(nestedKeysCheckbox);
+
/* code assistance */
codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance"));
- codeAssistanceCheckbox.setSelected(codeAssistance);
+ codeAssistanceCheckbox.setSelected(state.isCodeAssistance());
rootPanel.add(codeAssistanceCheckbox);
diff --git a/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java b/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java
index baad8bb..6bb9fe7 100644
--- a/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java
+++ b/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java
@@ -4,8 +4,8 @@ import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.project.Project;
-import de.marhali.easyi18n.model.LocalizedNode;
-import de.marhali.easyi18n.service.DataStore;
+import de.marhali.easyi18n.InstanceManager;
+import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
@@ -39,7 +39,7 @@ public class KeyAnnotator {
searchKey = searchKey.substring(1);
}
- LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(searchKey);
+ TranslationNode node = InstanceManager.get(project).store().getData().getNode(searchKey);
if(node == null) { // Unknown translation. Just ignore it
return;
diff --git a/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java b/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java
index a3a10b3..d76b936 100644
--- a/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java
+++ b/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java
@@ -5,9 +5,11 @@ import com.intellij.codeInsight.lookup.*;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.project.*;
import com.intellij.util.*;
-import de.marhali.easyi18n.model.*;
+import de.marhali.easyi18n.DataStore;
+import de.marhali.easyi18n.InstanceManager;
+import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.service.*;
-import de.marhali.easyi18n.util.TranslationsUtil;
+import de.marhali.easyi18n.util.PathUtil;
import org.jetbrains.annotations.*;
import java.util.*;
@@ -29,7 +31,8 @@ public class KeyCompletionProvider extends CompletionProvider fullKeys = store.getTranslations().getFullKeys();
+ Set fullKeys = store.getData().getFullKeys();
int sections = path.split("\\.").length;
int maxSectionForwardLookup = 5;
@@ -65,19 +68,20 @@ public class KeyCompletionProvider extends CompletionProvider sections + maxSectionForwardLookup) { // Key is too deep nested
- String shrinkKey = TranslationsUtil.sectionsToFullPath(Arrays.asList(
- Arrays.copyOf(keySections, sections + maxSectionForwardLookup)));
+ String shrinkKey = pathUtil.concat(Arrays.asList(
+ Arrays.copyOf(keySections, sections + maxSectionForwardLookup)
+ ));
result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey)
.appendTailText(" I18n([])", true));
} else {
- LocalizedNode node = store.getTranslations().getNode(key);
- String translation = node != null ? node.getValue().get(previewLocale) : null;
+ Translation translation = store.getData().getTranslation(key);
+ String content = translation.get(previewLocale);
result.addElement(LookupElementBuilder.create(pathPrefix + key)
.withIcon(AllIcons.Actions.PreserveCaseHover)
- .appendTailText(" I18n(" + previewLocale + ": " + translation + ")", true)
+ .appendTailText(" I18n(" + previewLocale + ": " + content + ")", true)
);
}
}
diff --git a/src/main/java/de/marhali/easyi18n/editor/KeyReference.java b/src/main/java/de/marhali/easyi18n/editor/KeyReference.java
index 1a8ea02..56d5733 100644
--- a/src/main/java/de/marhali/easyi18n/editor/KeyReference.java
+++ b/src/main/java/de/marhali/easyi18n/editor/KeyReference.java
@@ -4,12 +4,12 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
+import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
-import de.marhali.easyi18n.model.KeyedTranslation;
-import de.marhali.easyi18n.model.LocalizedNode;
-import de.marhali.easyi18n.service.DataStore;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -52,10 +52,10 @@ public class KeyReference extends PsiReferenceBase {
@Override
public void navigate(boolean requestFocus) {
- LocalizedNode node = DataStore.getInstance(getProject()).getTranslations().getNode(getKey());
+ Translation translation = InstanceManager.get(getProject()).store().getData().getTranslation(getKey());
- if(node != null) {
- new EditDialog(getProject(), new KeyedTranslation(getKey(), node.getValue())).showAndHandle();
+ if(translation != null) {
+ new EditDialog(getProject(), new KeyedTranslation(getKey(), translation)).showAndHandle();
} else {
new AddDialog(getProject(), getKey()).showAndHandle();
}
diff --git a/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyReferenceContributor.java b/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyReferenceContributor.java
index 960f6fb..a6b0542 100644
--- a/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyReferenceContributor.java
+++ b/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyReferenceContributor.java
@@ -4,8 +4,8 @@ import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
+import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
-import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
@@ -38,7 +38,7 @@ public class GenericKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY;
}
- if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
+ if(InstanceManager.get(element.getProject()).store().getData().getTranslation(value) == null) {
if(!KeyReference.isReferencable(value)) { // Creation policy
return PsiReference.EMPTY_ARRAY;
}
diff --git a/src/main/java/de/marhali/easyi18n/editor/kotlin/KotlinKeyReferenceContributor.java b/src/main/java/de/marhali/easyi18n/editor/kotlin/KotlinKeyReferenceContributor.java
index e871fc9..77bfd20 100644
--- a/src/main/java/de/marhali/easyi18n/editor/kotlin/KotlinKeyReferenceContributor.java
+++ b/src/main/java/de/marhali/easyi18n/editor/kotlin/KotlinKeyReferenceContributor.java
@@ -5,8 +5,8 @@ import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
+import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
-import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
@@ -45,7 +45,7 @@ public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY;
}
- if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
+ if(InstanceManager.get(element.getProject()).store().getData().getNode(value) == null) {
return PsiReference.EMPTY_ARRAY;
}
diff --git a/src/main/java/de/marhali/easyi18n/util/array/ArrayUtil.java b/src/main/java/de/marhali/easyi18n/io/ArrayMapper.java
similarity index 77%
rename from src/main/java/de/marhali/easyi18n/util/array/ArrayUtil.java
rename to src/main/java/de/marhali/easyi18n/io/ArrayMapper.java
index 2360e22..bebe612 100644
--- a/src/main/java/de/marhali/easyi18n/util/array/ArrayUtil.java
+++ b/src/main/java/de/marhali/easyi18n/io/ArrayMapper.java
@@ -1,6 +1,7 @@
-package de.marhali.easyi18n.util.array;
+package de.marhali.easyi18n.io;
import de.marhali.easyi18n.util.StringUtil;
+
import org.apache.commons.lang.StringEscapeUtils;
import java.text.MessageFormat;
@@ -10,11 +11,13 @@ import java.util.function.Function;
import java.util.regex.Pattern;
/**
- * Utility methods for simple array support.
+ * Simple array support for translation values.
+ * Some i18n systems allows the user to define array values for some translations.
+ * We support array values by wrapping them into: '!arr[valueA;valueB]'.
+ *
* @author marhali
*/
-public abstract class ArrayUtil {
-
+public abstract class ArrayMapper {
static final String PREFIX = "!arr[";
static final String SUFFIX = "]";
static final char DELIMITER = ';';
@@ -22,7 +25,7 @@ public abstract class ArrayUtil {
static final String SPLITERATOR_REGEX =
MessageFormat.format("(? String read(Iterator elements, Function stringFactory) {
+ protected static String read(Iterator elements, Function stringFactory) {
StringBuilder builder = new StringBuilder(PREFIX);
int i = 0;
@@ -43,7 +46,7 @@ public abstract class ArrayUtil {
return builder.toString();
}
- static void write(String concat, Consumer writeElement) {
+ protected static void write(String concat, Consumer writeElement) {
concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length());
for(String element : concat.split(SPLITERATOR_REGEX)) {
@@ -55,4 +58,4 @@ public abstract class ArrayUtil {
public static boolean isArray(String concat) {
return concat != null && concat.startsWith(PREFIX) && concat.endsWith(SUFFIX);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/IOStrategy.java b/src/main/java/de/marhali/easyi18n/io/IOStrategy.java
new file mode 100644
index 0000000..7a9fdc9
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/IOStrategy.java
@@ -0,0 +1,62 @@
+package de.marhali.easyi18n.io;
+
+import com.intellij.openapi.project.Project;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import de.marhali.easyi18n.model.SettingsState;
+import de.marhali.easyi18n.model.TranslationData;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+/**
+ * Primary interface for the exchange of translation data with the underlying IO system.
+ * The selection of the right IO strategy is done by the @canUse method (first match).
+ * Every strategy needs to be registered inside {@link de.marhali.easyi18n.DataStore}
+ *
+ * @author marhali
+ */
+public interface IOStrategy {
+ /**
+ * Decides whether this strategy should be applied or not. First matching one will be used.
+ * @param project IntelliJ project context
+ * @param localesPath Root directory which leads to all i18n files
+ * @param state Plugin configuration
+ * @return true if strategy is responsible for the found structure
+ */
+ boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state);
+
+ /**
+ * Loads the translation files and passes them in the result consumer.
+ * Result payload might be null if operation failed.
+ * @param project IntelliJ project context
+ * @param localesPath Root directory which leads to all i18n files
+ * @param state Plugin configuration
+ * @param result Passes loaded data
+ */
+ void read(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state,
+ @NotNull Consumer<@Nullable TranslationData> result);
+
+ /**
+ * Writes the provided translation data to the IO system.
+ * @param project InteliJ project context
+ * @param localesPath Root directory which leads to all i18n files
+ * @param state Plugin configuration
+ * @param data Translations to save
+ * @param result Indicates whether the operation was successful
+ */
+ void write(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state,
+ @NotNull TranslationData data, @NotNull Consumer result);
+
+ /**
+ * Checks if the provided file should be processed for translation data
+ * @param state Plugin configuration
+ * @param file File to check
+ * @return true if file matches pattern
+ */
+ default boolean isFileRelevant(@NotNull SettingsState state, @NotNull VirtualFile file) {
+ return file.getName().matches(state.getFilePattern());
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/io/TranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/TranslatorIO.java
deleted file mode 100644
index 91a40d1..0000000
--- a/src/main/java/de/marhali/easyi18n/io/TranslatorIO.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.marhali.easyi18n.io;
-
-import com.intellij.openapi.project.Project;
-
-import de.marhali.easyi18n.model.Translations;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.function.Consumer;
-
-/**
- * Interface to retrieve and save localized messages.
- * Can be implemented by various standards. Such as JSON, Properties-Bundle and so on.
- * @author marhali
- */
-public interface TranslatorIO {
-
- /**
- * Reads localized messages from the persistence layer.
- * @param project Opened intellij project
- * @param directoryPath The full path for the directory which holds all locale files
- * @param callback Contains loaded translations. Will be called after io operation. Content might be null on failure.
- */
- void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback);
-
- /**
- * Writes the provided messages (translations) to the persistence layer.
- * @param project Opened intellij project
- * @param translations Translations instance to save
- * @param directoryPath The full path for the directory which holds all locale files
- * @param callback Will be called after io operation. Can be used to determine if action was successful(true) or not
- */
- void save(@NotNull Project project, @NotNull Translations translations,
- @NotNull String directoryPath, @NotNull Consumer callback);
-}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java
deleted file mode 100644
index 1efbd0e..0000000
--- a/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package de.marhali.easyi18n.io.implementation;
-
-import com.google.gson.*;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-
-import de.marhali.easyi18n.io.TranslatorIO;
-import de.marhali.easyi18n.model.LocalizedNode;
-import de.marhali.easyi18n.model.Translations;
-import de.marhali.easyi18n.util.IOUtil;
-import de.marhali.easyi18n.util.JsonUtil;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.*;
-import java.util.function.Consumer;
-
-/**
- * Implementation for JSON translation files.
- * @author marhali
- */
-public class JsonTranslatorIO implements TranslatorIO {
-
- private static final String FILE_EXTENSION = "json";
- private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
-
- @Override
- public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
-
- ApplicationManager.getApplication().runReadAction(() -> {
- VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
-
- if(directory == null || directory.getChildren() == null) {
- throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
- }
-
- VirtualFile[] files = directory.getChildren();
-
- List locales = new ArrayList<>();
- LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
-
- try {
- for(VirtualFile file : files) {
-
- if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
- continue;
- }
-
- locales.add(file.getNameWithoutExtension());
-
- JsonObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(),
- file.getCharset()), JsonObject.class);
-
- JsonUtil.readTree(file.getNameWithoutExtension(), tree, nodes);
- }
-
- callback.accept(new Translations(locales, nodes));
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(null);
- }
- });
- }
-
- @Override
- public void save(@NotNull Project project, @NotNull Translations translations,
- @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().runWriteAction(() -> {
- try {
- for(String locale : translations.getLocales()) {
- JsonObject content = new JsonObject();
- JsonUtil.writeTree(locale, content, translations.getNodes());
-
- String fullPath = directoryPath + "/" + locale + "." + FILE_EXTENSION;
- File file = new File(fullPath);
- boolean created = file.createNewFile();
-
- VirtualFile vf = created ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
- : LocalFileSystem.getInstance().findFileByIoFile(file);
-
- vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
- }
-
- // Successfully saved
- callback.accept(true);
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(false);
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/ModularizedJsonTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/ModularizedJsonTranslatorIO.java
deleted file mode 100644
index 0cc70d9..0000000
--- a/src/main/java/de/marhali/easyi18n/io/implementation/ModularizedJsonTranslatorIO.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package de.marhali.easyi18n.io.implementation;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-
-import de.marhali.easyi18n.io.TranslatorIO;
-import de.marhali.easyi18n.model.LocalizedNode;
-import de.marhali.easyi18n.model.Translations;
-import de.marhali.easyi18n.util.IOUtil;
-import de.marhali.easyi18n.util.JsonUtil;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * IO operations for splitted / modularized json files. Each locale can have multiple translation files.
- * @author marhali
- */
-public class ModularizedJsonTranslatorIO implements TranslatorIO {
-
- private static final String FILE_EXTENSION = "json";
-
- @Override
- public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
-
- ApplicationManager.getApplication().runReadAction(() -> {
- VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
-
- if(directory == null || directory.getChildren() == null) {
- throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
- }
-
- VirtualFile[] localeDirectories = directory.getChildren();
-
- List locales = new ArrayList<>();
- LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
-
- try {
- for(VirtualFile localeDir : localeDirectories) {
- String locale = localeDir.getName();
- locales.add(locale);
-
- // Read all json modules
- for(VirtualFile module : localeDir.getChildren()) {
-
- if(!IOUtil.isFileRelevant(project, module)) { // File does not matches pattern
- continue;
- }
-
- JsonObject tree = JsonParser.parseReader(new InputStreamReader(module.getInputStream(),
- module.getCharset())).getAsJsonObject();
-
- String moduleName = module.getNameWithoutExtension();
- LocalizedNode moduleNode = nodes.getChildren(moduleName);
-
- if(moduleNode == null) { // Create module / sub node
- moduleNode = new LocalizedNode(moduleName, new ArrayList<>());
- nodes.addChildren(moduleNode);
- }
-
- JsonUtil.readTree(locale, tree, moduleNode);
- }
- }
-
- callback.accept(new Translations(locales, nodes));
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(null);
- }
- });
- }
-
- @Override
- public void save(@NotNull Project project, @NotNull Translations translations,
- @NotNull String directoryPath, @NotNull Consumer callback) {
-
- Gson gson = new GsonBuilder().setPrettyPrinting().create();
-
- ApplicationManager.getApplication().runWriteAction(() -> {
- try {
- for(String locale : translations.getLocales()) {
- // Use top level children as modules
- for (LocalizedNode module : translations.getNodes().getChildren()) {
- JsonObject content = new JsonObject();
- JsonUtil.writeTree(locale, content, module);
-
- String fullPath = directoryPath + "/" + locale + "/" + module.getKey() + "." + FILE_EXTENSION;
- File file = new File(fullPath);
- boolean created = file.createNewFile();
-
- VirtualFile vf = created ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
- : LocalFileSystem.getInstance().findFileByIoFile(file);
-
- vf.setBinaryContent(gson.toJson(content).getBytes(vf.getCharset()));
- }
- }
-
- // Successfully saved
- callback.accept(true);
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(false);
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java
deleted file mode 100644
index 458c49a..0000000
--- a/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package de.marhali.easyi18n.io.implementation;
-
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-
-import de.marhali.easyi18n.io.TranslatorIO;
-import de.marhali.easyi18n.model.LocalizedNode;
-import de.marhali.easyi18n.model.Translations;
-import de.marhali.easyi18n.util.IOUtil;
-import de.marhali.easyi18n.util.SortedProperties;
-import de.marhali.easyi18n.util.StringUtil;
-import de.marhali.easyi18n.util.TranslationsUtil;
-
-import org.apache.commons.lang.StringEscapeUtils;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.*;
-import java.util.*;
-import java.util.function.Consumer;
-
-/**
- * Implementation for properties translation files.
- * @author marhali
- */
-public class PropertiesTranslatorIO implements TranslatorIO {
-
- public static final String FILE_EXTENSION = "properties";
-
- @Override
- public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
-
- ApplicationManager.getApplication().runReadAction(() -> {
- VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
-
- if(directory == null || directory.getChildren() == null) {
- throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
- }
-
- VirtualFile[] files = directory.getChildren();
-
- List locales = new ArrayList<>();
- LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
-
- try {
- for (VirtualFile file : files) {
-
- if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
- continue;
- }
-
- locales.add(file.getNameWithoutExtension());
- SortedProperties properties = new SortedProperties();
- properties.load(new InputStreamReader(file.getInputStream(), file.getCharset()));
- readProperties(file.getNameWithoutExtension(), properties, nodes);
- }
-
- callback.accept(new Translations(locales, nodes));
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(null);
- }
- });
- }
-
- @Override
- public void save(@NotNull Project project, @NotNull Translations translations,
- @NotNull String directoryPath, @NotNull Consumer callback) {
-
- ApplicationManager.getApplication().runWriteAction(() -> {
- try {
- for(String locale : translations.getLocales()) {
- SortedProperties properties = new SortedProperties();
- writeProperties(locale, properties, translations.getNodes(), "");
-
- String fullPath = directoryPath + "/" + locale + "." + FILE_EXTENSION;
- VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
-
- StringWriter content = new StringWriter();
- properties.store(content, "I18n " + locale + " keys");
-
- file.setBinaryContent(content.toString().getBytes(file.getCharset()));
- }
-
- // Successfully saved
- callback.accept(true);
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(false);
- }
- });
- }
-
- private void writeProperties(String locale, Properties props, LocalizedNode node, String parentPath) {
- if(node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
- if(node.getValue().get(locale) != null) { // Translation is defined - track it
- String value = StringEscapeUtils.unescapeJava(node.getValue().get(locale));
- props.setProperty(parentPath, value);
- }
-
- } else {
- for(LocalizedNode children : node.getChildren()) {
- writeProperties(locale, props, children,
- parentPath + (parentPath.isEmpty() ? "" : ".") + children.getKey());
- }
- }
- }
-
- private void readProperties(String locale, Properties props, LocalizedNode parent) {
- props.forEach((key, value) -> {
- List sections = TranslationsUtil.getSections(String.valueOf(key));
-
- LocalizedNode node = parent;
-
- for (String section : sections) {
- LocalizedNode subNode = node.getChildren(section);
-
- if(subNode == null) {
- subNode = new LocalizedNode(section, new ArrayList<>());
- node.addChildren(subNode);
- }
-
- node = subNode;
- }
-
- Map messages = node.getValue();
- String escapedValue = StringUtil.escapeControls(String.valueOf(value), true);
- messages.put(locale, escapedValue);
- node.setValue(messages);
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java
deleted file mode 100644
index 4ed8e97..0000000
--- a/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package de.marhali.easyi18n.io.implementation;
-
-import com.intellij.openapi.application.*;
-import com.intellij.openapi.project.*;
-import com.intellij.openapi.vfs.*;
-
-import de.marhali.easyi18n.io.*;
-import de.marhali.easyi18n.model.*;
-import de.marhali.easyi18n.util.*;
-import de.marhali.easyi18n.util.array.YamlArrayUtil;
-
-import org.jetbrains.annotations.*;
-
-import thito.nodeflow.config.*;
-
-import java.io.*;
-import java.nio.charset.*;
-import java.util.*;
-import java.util.function.*;
-
-public class YamlTranslatorIO implements TranslatorIO {
- @Override
- public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
-
- ApplicationManager.getApplication().runReadAction(() -> {
- VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
-
- if(directory == null || directory.getChildren() == null) {
- throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
- }
-
- VirtualFile[] files = directory.getChildren();
-
- List locales = new ArrayList<>();
- LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
-
- try {
- for(VirtualFile file : files) {
-
- if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
- continue;
- }
-
- locales.add(file.getNameWithoutExtension());
-
- try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
- Section section = Section.parseToMap(reader);
- load(file.getNameWithoutExtension(), nodes, section);
- }
- }
-
- callback.accept(new Translations(locales, nodes));
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(null);
- }
- });
- }
-
- private void load(String locale, LocalizedNode node, Section section) {
- if (section instanceof MapSection) {
- for (String key : section.getKeys()) {
- LocalizedNode child = node.getChildren(key);
- if (child == null) {
- node.addChildren(child = new LocalizedNode(key, new ArrayList<>()));
- }
- LocalizedNode finalChild = child;
- MapSection map = section.getMap(key).orElse(null);
- if (map != null) {
- load(locale, finalChild, map);
- } else {
-
- if(section.isList(key) && section.getList(key).isPresent()) {
- child.getValue().put(locale, YamlArrayUtil.read(section.getList(key).get()));
- } else {
- String value = section.getString(key).orElse(null);
- if (value != null) {
- child.getValue().put(locale, value);
- }
- }
- }
- }
- }
- }
-
- private void save(LocalizedNode node, String locale, Section section, String path) {
- if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
- String value = node.getValue().get(locale);
- if (value != null) {
- section.set(path, YamlArrayUtil.isArray(value) ? YamlArrayUtil.write(value) : value);
- }
- } else {
- for (LocalizedNode child : node.getChildren()) {
- save(child, locale, section, path == null ? child.getKey() : path + "." + child.getKey());
- }
- }
- }
-
- @Override
- public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer callback) {
- ApplicationManager.getApplication().runWriteAction(() -> {
- try {
- for(String locale : translations.getLocales()) {
- Section section = new MapSection();
-
- save(translations.getNodes(), locale, section, null);
-
- String fullPath = directoryPath + "/" + locale + ".yml";
- VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
-
- file.setBinaryContent(Section.toString(section).getBytes(file.getCharset()));
- }
-
- // Successfully saved
- callback.accept(true);
-
- } catch(IOException e) {
- e.printStackTrace();
- callback.accept(false);
- }
- });
- }
-}
diff --git a/src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java b/src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java
similarity index 70%
rename from src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java
rename to src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java
index d61969c..aac41ed 100644
--- a/src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java
+++ b/src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java
@@ -1,13 +1,14 @@
-package de.marhali.easyi18n.util.array;
+package de.marhali.easyi18n.io.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
+import de.marhali.easyi18n.io.ArrayMapper;
/**
- * Utility methods to read and write json arrays.
+ * Map json array values.
* @author marhali
*/
-public class JsonArrayUtil extends ArrayUtil {
+public class JsonArrayMapper extends ArrayMapper {
public static String read(JsonArray array) {
return read(array.iterator(), JsonElement::getAsString);
}
@@ -17,4 +18,4 @@ public class JsonArrayUtil extends ArrayUtil {
write(concat, array::add);
return array;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java
new file mode 100644
index 0000000..40d57af
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java
@@ -0,0 +1,118 @@
+package de.marhali.easyi18n.io.json;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import de.marhali.easyi18n.io.IOStrategy;
+import de.marhali.easyi18n.model.SettingsState;
+import de.marhali.easyi18n.model.TranslationData;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.function.Consumer;
+
+/**
+ * Strategy for simple json locale files. Each locale has its own file.
+ * For example localesPath/en.json, localesPath/de.json.
+ * @author marhali
+ */
+public class JsonIOStrategy implements IOStrategy {
+
+ private static final String FILE_EXTENSION = "json";
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+
+ @Override
+ public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ return false;
+ }
+
+ for(VirtualFile children : directory.getChildren()) {
+ if(!children.isDirectory() && isFileRelevant(state, children)) {
+ if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void read(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
+ ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
+
+ ApplicationManager.getApplication().runReadAction(() -> {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
+ }
+
+ TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
+
+ try {
+ for(VirtualFile file : directory.getChildren()) {
+ if(file.isDirectory() || !isFileRelevant(state, file)) {
+ continue;
+ }
+
+ String locale = file.getNameWithoutExtension();
+ data.addLocale(locale);
+
+ JsonObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(), file.getCharset()),
+ JsonObject.class);
+
+ JsonMapper.read(locale, tree, data.getRootNode());
+ }
+
+ result.accept(data);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(null);
+ }
+ });
+ }
+
+ @Override
+ public void write(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer result) {
+ ApplicationManager.getApplication().runWriteAction(() -> {
+ try {
+ for(String locale : data.getLocales()) {
+ JsonObject content = new JsonObject();
+ JsonMapper.write(locale, content, data.getRootNode());
+
+ File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION);
+ boolean exists = file.createNewFile();
+
+ VirtualFile vf = exists
+ ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
+ : LocalFileSystem.getInstance().findFileByIoFile(file);
+
+ vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
+ }
+
+ result.accept(true);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(false);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java b/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java
new file mode 100644
index 0000000..55f5c43
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java
@@ -0,0 +1,73 @@
+package de.marhali.easyi18n.io.json;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+import de.marhali.easyi18n.model.Translation;
+import de.marhali.easyi18n.model.TranslationNode;
+import de.marhali.easyi18n.util.StringUtil;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.math.NumberUtils;
+
+import java.util.Map;
+
+/**
+ * Mapper for mapping json objects into translation nodes and backwards.
+ * @author marhali
+ */
+public class JsonMapper {
+
+ public static void read(String locale, JsonObject json, TranslationNode node) {
+ for(Map.Entry entry : json.entrySet()) {
+ String key = entry.getKey();
+ JsonElement value = entry.getValue();
+
+ TranslationNode childNode = node.getOrCreateChildren(key);
+
+ if(value.isJsonObject()) {
+ // Nested element - run recursively
+ read(locale, value.getAsJsonObject(), childNode);
+ } else {
+ Translation translation = childNode.getValue();
+
+ String content = entry.getValue().isJsonArray()
+ ? JsonArrayMapper.read(value.getAsJsonArray())
+ : StringUtil.escapeControls(value.getAsString(), true);
+
+ translation.put(locale, content);
+ childNode.setValue(translation);
+ }
+ }
+ }
+
+ public static void write(String locale, JsonObject json, TranslationNode node) {
+ for(Map.Entry entry : node.getChildren().entrySet()) {
+ String key = entry.getKey();
+ TranslationNode childNode = entry.getValue();
+
+ if(!childNode.isLeaf()) {
+ // Nested node - run recursively
+ JsonObject childJson = new JsonObject();
+ write(locale, childJson, childNode);
+ if(childJson.size() > 0) {
+ json.add(key, childJson);
+ }
+ } else {
+ Translation translation = childNode.getValue();
+ String content = translation.get(locale);
+
+ if(content != null) {
+ if(JsonArrayMapper.isArray(content)) {
+ json.add(key, JsonArrayMapper.write(content));
+ } else if(NumberUtils.isNumber(content)) {
+ json.add(key, new JsonPrimitive(NumberUtils.createNumber(content)));
+ } else {
+ json.add(key, new JsonPrimitive(StringEscapeUtils.unescapeJava(content)));
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java
new file mode 100644
index 0000000..7e4d877
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java
@@ -0,0 +1,149 @@
+package de.marhali.easyi18n.io.json;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import de.marhali.easyi18n.io.IOStrategy;
+import de.marhali.easyi18n.model.SettingsState;
+import de.marhali.easyi18n.model.TranslationData;
+import de.marhali.easyi18n.model.TranslationNode;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+
+/**
+ * Strategy for distributed json files per locale. Each locale can have multiple modules. The file name
+ * of each module will be used as the key for the underlying translations.
+ * Full key example: ..
+ *
+ * @author marhali
+ */
+public class ModularizedJsonIOStrategy implements IOStrategy {
+
+ private static final String FILE_EXTENSION = "json";
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+
+ @Override
+ public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ return false;
+ }
+
+ // We expect something like this:
+ // //
+
+ for(VirtualFile children : directory.getChildren()) {
+ if(children.isDirectory()) { // Contains module folders
+ for(VirtualFile moduleFile : children.getChildren()) {
+ if(!moduleFile.isDirectory() && isFileRelevant(state, moduleFile)) {
+ if(moduleFile.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void read(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
+ ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
+
+ ApplicationManager.getApplication().runReadAction(() -> {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
+ }
+
+ TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
+ VirtualFile[] localeDirectories = directory.getChildren();
+
+ try {
+ for(VirtualFile localeDir : localeDirectories) {
+ String locale = localeDir.getNameWithoutExtension();
+ data.addLocale(locale);
+
+ // Read all underlying module files
+ for(VirtualFile module : localeDir.getChildren()) {
+ if(module.isDirectory() || !isFileRelevant(state, module)) {
+ continue;
+ }
+
+ String moduleName = module.getNameWithoutExtension();
+
+ TranslationNode moduleNode = data.getNode(moduleName) != null
+ ? data.getNode(moduleName)
+ : new TranslationNode(state.isSortKeys() ? new TreeMap<>() : new LinkedHashMap<>());
+
+ JsonObject tree = GSON.fromJson(new InputStreamReader(module.getInputStream(),
+ module.getCharset()), JsonObject.class);
+
+ JsonMapper.read(locale, tree, moduleNode);
+
+ data.getRootNode().setChildren(moduleName, moduleNode);
+ }
+ }
+
+ result.accept(data);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(null);
+ }
+ });
+ }
+
+ // TODO: there will be problems when adding translations via TranslationData with non-nested key mode
+
+ @Override
+ public void write(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer result) {
+ ApplicationManager.getApplication().runWriteAction(() -> {
+ try {
+ for(String locale : data.getLocales()) {
+ for(Map.Entry entry : data.getRootNode().getChildren().entrySet()) {
+ String module = entry.getKey();
+
+ JsonObject content = new JsonObject();
+ JsonMapper.write(locale, content, entry.getValue());
+
+ String fullPath = localesPath + "/" + locale + "/" + module + "." + FILE_EXTENSION;
+ File file = new File(fullPath);
+ boolean exists = file.createNewFile();
+
+ VirtualFile vf = exists
+ ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
+ : LocalFileSystem.getInstance().findFileByIoFile(file);
+
+ vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
+ }
+ }
+
+ result.accept(true);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(false);
+ }
+ });
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/io/properties/PropertiesArrayMapper.java b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesArrayMapper.java
new file mode 100644
index 0000000..71741ba
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesArrayMapper.java
@@ -0,0 +1,23 @@
+package de.marhali.easyi18n.io.properties;
+
+import de.marhali.easyi18n.io.ArrayMapper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Map for 'properties' array values.
+ * @author marhali
+ */
+public class PropertiesArrayMapper extends ArrayMapper {
+ public static String read(String[] list) {
+ return read(Arrays.stream(list).iterator(), Object::toString);
+ }
+
+ public static String[] write(String concat) {
+ List list = new ArrayList<>();
+ write(concat, list::add);
+ return list.toArray(new String[0]);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java
new file mode 100644
index 0000000..0d3c9d4
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java
@@ -0,0 +1,116 @@
+package de.marhali.easyi18n.io.properties;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import de.marhali.easyi18n.io.IOStrategy;
+import de.marhali.easyi18n.model.SettingsState;
+import de.marhali.easyi18n.model.TranslationData;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.util.function.Consumer;
+
+/**
+ * Strategy for simple 'properties' locale files. Each locale has its own file.
+ * For example localesPath/en.properties, localesPath/de.properties.
+ * @author marhali
+ */
+public class PropertiesIOStrategy implements IOStrategy {
+
+ private static final String FILE_EXTENSION = "properties";
+
+ @Override
+ public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ return false;
+ }
+
+ for(VirtualFile children : directory.getChildren()) {
+ if(!children.isDirectory() && isFileRelevant(state, children)) {
+ if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void read(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
+ ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
+
+ ApplicationManager.getApplication().runReadAction(() -> {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
+ }
+
+ TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
+
+ try {
+ for(VirtualFile file : directory.getChildren()) {
+ if(file.isDirectory() || !isFileRelevant(state, file)) {
+ continue;
+ }
+
+ String locale = file.getNameWithoutExtension();
+ data.addLocale(locale);
+
+ SortableProperties properties = new SortableProperties(state.isSortKeys());
+ properties.load(new InputStreamReader(file.getInputStream(), file.getCharset()));
+ PropertiesMapper.read(locale, properties, data);
+ }
+
+ result.accept(data);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(null);
+ }
+ });
+ }
+
+ @Override
+ public void write(@NotNull Project project, @NotNull String localesPath,
+ @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer result) {
+ ApplicationManager.getApplication().runWriteAction(() -> {
+ try {
+ for(String locale : data.getLocales()) {
+ SortableProperties properties = new SortableProperties(state.isSortKeys());
+ PropertiesMapper.write(locale, properties, data);
+
+ File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION);
+ boolean exists = file.createNewFile();
+
+ VirtualFile vf = exists
+ ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
+ : LocalFileSystem.getInstance().findFileByIoFile(file);
+
+ StringWriter writer = new StringWriter();
+ properties.store(writer, null);
+
+ vf.setBinaryContent(writer.toString().getBytes(vf.getCharset()));
+ }
+
+ result.accept(true);
+
+ } catch(IOException e) {
+ e.printStackTrace();
+ result.accept(false);
+ }
+ });
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java
new file mode 100644
index 0000000..ba6aa6a
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java
@@ -0,0 +1,55 @@
+package de.marhali.easyi18n.io.properties;
+
+import de.marhali.easyi18n.model.Translation;
+import de.marhali.easyi18n.model.TranslationData;
+import de.marhali.easyi18n.model.TranslationNode;
+import de.marhali.easyi18n.util.StringUtil;
+
+import org.apache.commons.lang.math.NumberUtils;
+
+import java.util.Map;
+
+/**
+ * Mapper for mapping properties files into translation nodes and backwards.
+ * @author marhali
+ */
+public class PropertiesMapper {
+
+ public static void read(String locale, SortableProperties properties, TranslationData data) {
+ for(Map.Entry