Merge pull request #63 from marhali/feat/i18n-next

Feat/i18n next (v1.6.0)
This commit is contained in:
Marcel 2021-11-11 17:48:36 +01:00 committed by GitHub
commit 359a80d137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 3350 additions and 1947 deletions

View File

@ -3,7 +3,15 @@
version: 2 version: 2
updates: updates:
# Maintain dependencies for Gradle dependencies
- package-ecosystem: "gradle" - package-ecosystem: "gradle"
directory: "/" directory: "/"
target-branch: "next"
schedule:
interval: "daily"
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "next"
schedule: schedule:
interval: "daily" interval: "daily"

View File

@ -1,111 +1,53 @@
# GitHub Actions Workflow created for testing and preparing the plugin release in following steps: # GitHub Actions Workflow created for testing and preparing the plugin release in following steps:
# - validate Gradle Wrapper, # - validate Gradle Wrapper,
# - run test and verifyPlugin tasks, # - run 'test' and 'verifyPlugin' tasks,
# - run buildPlugin task and prepare artifact for the further tests, # - run Qodana inspections,
# - run IntelliJ Plugin Verifier, # - run 'buildPlugin' task and prepare artifact for the further tests,
# - run 'runPluginVerifier' task,
# - create a draft release. # - create a draft release.
# #
# Workflow is triggered on push and pull_request events. # Workflow is triggered on push and pull_request events.
# #
# Docs: # GitHub Actions reference: https://help.github.com/en/actions
# - GitHub Actions: https://help.github.com/en/actions
# - IntelliJ Plugin Verifier GitHub Action: https://github.com/ChrisCarini/intellij-platform-plugin-verifier-action
# #
## JBIJPPTPL ## JBIJPPTPL
name: Build 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: jobs:
# Run Gradle Wrapper Validation Action to verify the wrapper's checksum # Run Gradle Wrapper Validation Action to verify the wrapper's checksum
gradleValidation: # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks
name: Gradle Wrapper # Build plugin and provide the artifact for the next workflow jobs
build:
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
version: ${{ steps.properties.outputs.version }}
changelog: ${{ steps.properties.outputs.changelog }}
steps: steps:
# Check out current repository # Check out current repository
- name: Fetch Sources - name: Fetch Sources
uses: actions/checkout@v2 uses: actions/checkout@v2.4.0
# Validate wrapper # Validate wrapper
- name: Gradle Wrapper Validation - 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 # Setup Java 11 environment for the next steps
test:
name: Test
needs: gradleValidation
runs-on: ubuntu-latest
steps:
# Setup Java 1.8 environment for the next steps
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v1 uses: actions/setup-java@v2
with: with:
distribution: zulu
java-version: 11 java-version: 11
cache: gradle
# 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') }}
# Set environment variables # Set environment variables
- name: Export Properties - name: Export Properties
@ -119,128 +61,99 @@ jobs:
CHANGELOG="${CHANGELOG//'%'/'%25'}" CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
ARTIFACT="${NAME}-${VERSION}.zip"
echo "::set-output name=version::$VERSION" echo "::set-output name=version::$VERSION"
echo "::set-output name=name::$NAME" echo "::set-output name=name::$NAME"
echo "::set-output name=changelog::$CHANGELOG" 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" 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 # Cache Plugin Verifier IDEs
- name: Setup Plugin Verifier IDEs Cache - name: Setup Plugin Verifier IDEs Cache
uses: actions/cache@v2 uses: actions/cache@v2.1.6
with: with:
path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides 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 # Run Verify Plugin task and IntelliJ Plugin Verifier tool
- name: Verify Plugin - name: Run Plugin Verification tasks
run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} 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 # Prepare a draft release for GitHub Releases page for the manual verification
# If accepted and published, release workflow would be triggered # If accepted and published, release workflow would be triggered
releaseDraft: releaseDraft:
name: Release Draft name: Release Draft
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
needs: [build, verify] needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Check out current repository # Check out current repository
- name: Fetch Sources - 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 # Remove old release drafts by using the curl request for the available releases with draft flag
- name: Remove Old Release Drafts - name: Remove Old Release Drafts
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases \ gh api repos/{owner}/{repo}/releases \
| tr '\r\n' ' ' \ --jq '.[] | select(.draft == true) | .id' \
| jq '.[] | select(.draft == true) | .id' \ | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{}
| xargs -I '{}' \
curl -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases/{}
# Create new release draft - which is not publicly visible and requires manual acceptance # Create new release draft - which is not publicly visible and requires manual acceptance
- name: Create Release Draft - name: Create Release Draft
id: createDraft
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: run: |
tag_name: v${{ needs.build.outputs.version }} gh release create v${{ needs.build.outputs.version }} \
release_name: v${{ needs.build.outputs.version }} --draft \
body: ${{ needs.build.outputs.changelog }} --title "v${{ needs.build.outputs.version }}" \
draft: true --notes "$(cat << 'EOM'
${{ needs.build.outputs.changelog }}
# Download plugin artifact provided by the previous job EOM
- 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

View File

@ -14,57 +14,66 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 # Check out current repository
- name: Fetch Sources - name: Fetch Sources
uses: actions/checkout@v2 uses: actions/checkout@v2.4.0
with: with:
ref: ${{ github.event.release.tag_name }} 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 # Publish the plugin to the Marketplace
- name: Publish Plugin - name: Publish Plugin
env: env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: ./gradlew publishPlugin run: ./gradlew publishPlugin
# Patch changelog, commit and push to the current repository # Upload artifact as a release asset
changelog: - name: Upload Release Asset
name: Update Changelog env:
needs: release GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-latest run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
steps:
# Setup Java 1.8 environment for the next steps # Create pull request
- name: Setup Java - name: Create Pull Request
uses: actions/setup-java@v1 if: ${{ steps.properties.outputs.changelog != '' }}
with: env:
java-version: 11 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 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
run: | run: |
git config --local user.email "action@github.com" VERSION="${{ github.event.release.tag_name }}"
git config --local user.name "GitHub Action" BRANCH="changelog-update-$VERSION"
git commit -m "Update changelog" -a git config user.email "action@github.com"
git config user.name "GitHub Action"
# Push changes git checkout -b $BRANCH
- name: Push changes git commit -am "Changelog update - $VERSION"
uses: ad-m/github-push-action@master git push --set-upstream origin $BRANCH
with: gh pr create \
branch: main --title "Changelog update - \`$VERSION\`" \
github_token: ${{ secrets.GITHUB_TOKEN }} --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
--base main \
--head $BRANCH

60
.github/workflows/run-ui-tests.yml vendored Normal file
View File

@ -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

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.gradle .gradle
.idea .idea
.qodana
build build

View File

@ -0,0 +1,22 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run IDE for UI Tests" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="runIdeForUiTests" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -1,24 +1,24 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Plugin" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Run Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" /> <log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" /> <option name="scriptParameters" value="" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value="runIde" /> <option value="runIde" />
</list> </list>
</option> </option>
<option name="vmOptions" value="" /> <option name="vmOptions" value="" />
</ExternalSystemSettings> </ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled> <DebugAllEnabled>false</DebugAllEnabled>
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@ -1,24 +1,24 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" /> <log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" /> <option name="scriptParameters" value="" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value="check" /> <option value="test" />
</list> </list>
</option> </option>
<option name="vmOptions" value="" /> <option name="vmOptions" value="" />
</ExternalSystemSettings> </ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled> <DebugAllEnabled>false</DebugAllEnabled>
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@ -1,26 +1,26 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Verifications" type="GradleRunConfiguration" factoryName="Gradle"> <configuration default="false" name="Run Verifications" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" /> <log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" /> <option name="scriptParameters" value="" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value="runPluginVerifier" /> <option value="runPluginVerifier" />
</list> </list>
</option> </option>
<option name="vmOptions" value="" /> <option name="vmOptions" value="" />
</ExternalSystemSettings> </ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled> <DebugAllEnabled>false</DebugAllEnabled>
<method v="2"> <method v="2">
<option name="Gradle.BeforeRunTask" enabled="true" tasks="clean" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" /> <option name="Gradle.BeforeRunTask" enabled="true" tasks="clean" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" />
</method> </method>
</configuration> </configuration>
</component> </component>

26
.run/Run Qodana.run.xml Normal file
View File

@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Qodana" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="QODANA_SHOW_REPORT" value="true" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="cleanInspections runInspections" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -3,6 +3,20 @@
# easy-i18n Changelog # easy-i18n Changelog
## [Unreleased] ## [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] ## [1.5.1]
### Fixed ### Fixed
- Exception on key annotation if path-prefix is undefined - Exception on key annotation if path-prefix is undefined

View File

@ -6,29 +6,40 @@
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/marhalide) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/marhalide)
<!-- Plugin description --> <!-- Plugin description -->
This is an easy plugin to manage internationalization for JSON or Resource-Bundle(Properties) based locale files. 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!
Most common use case is for translating Webapps or simple Java Applications. Translating large scale projects was never that easy with your favourite IDE!
## Use Cases ## 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 - 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 based Resource-Bundle - Java projects based on Resource-Bundle's
- Projects that uses yaml, json or properties as locale file base for internationalization
## Features ## Features
- UI Tool Window with Table- and Tree-View representation - UI Tool Window which supports tree- or table-view
- Easily Add / Edit / Delete translations - Easily Add / Edit / Delete translations
- Filter / Search function to hide irrelevant keys - Filter function with full-text-search support
- Key completion and annotation inside editor - Editor Assistance: Key completion, annotation and referencing
- Key sorting and nesting can be configured
- Configurable locales directory & preferred locale for ui presentation - Configurable locales directory & preferred locale for ui presentation
- Supports modularized (splitted) json files - Missing language translations will be indicated red
- Translation keys with missing definition for any locale will be displayed red - Quick actions: <kbd>right-click</kbd> or <kbd>DEL</kbd> to edit or delete a translation
- Quick edit any translation by right-click (IntelliJ Popup Action) - Automatically reloads translation data if any locale file was changed
- Quick delete any translation via <kbd>DEL</kbd>-Key
<!-- Plugin description end --> <!-- Plugin description end -->
## Screenshots ## Screenshots
![Tree View](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/TreeView.PNG "Tree View") ![Tree View](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/tree-view.PNG)
![Table View](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/TableView.PNG "Table View") ![TableView](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/table-view.PNG)
![Key Completion](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/Completion.PNG "Key Completion") ![KeyCompletion](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/key-completion.PNG)
![KeyAnnotation](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/key-annotation.PNG)
![KeyEdit](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/key-edit.PNG)
![Settings](https://raw.githubusercontent.com/marhali/easy-i18n/main/example/images/settings.PNG)
## Supported IO Strategies (locale files)
- Json: <kbd>json</kbd> files inside locales directory
- Namespaced Json: Multiple <kbd>json</kbd> files per locale directory
- Yaml: <kbd>yml</kbd> or <kbd>yaml</kbd> files inside locales directory
- Properties: <kbd>properties</kbd> files inside locales directory
If there are any files in the locales folder that should not be processed, they can be ignored with the <kbd>Translation file pattern</kbd> option.
## Installation ## Installation
- Using IDE built-in plugin system: - 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 - Install plugin. See **Installation** section
- Create a directory which will hold the locale files - 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. **{}**) - 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 - Click on the **Settings** Action inside the EasyI18n Tool Window
- Select the created directory (optional: define the preferred locale to view) and press Ok - Select the created directory (optional: define the preferred locale to view) and press **Ok**
- Translations can now be created / edited or deleted - 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. Examples for the configuration can be found in the [/example](https://github.com/marhali/easy-i18n/tree/main/example) folder.

View File

@ -1,4 +1,3 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.changelog.markdownToHTML import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@ -8,15 +7,13 @@ plugins {
// Java support // Java support
id("java") id("java")
// Kotlin support // Kotlin support
id("org.jetbrains.kotlin.jvm") version "1.5.10" id("org.jetbrains.kotlin.jvm") version "1.5.31"
// gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin // Gradle IntelliJ Plugin
id("org.jetbrains.intellij") version "1.0" id("org.jetbrains.intellij") version "1.2.1"
// gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin // Gradle Changelog Plugin
id("org.jetbrains.changelog") version "1.1.2" id("org.jetbrains.changelog") version "1.3.1"
// detekt linter - read more: https://detekt.github.io/detekt/gradle.html // Gradle Qodana Plugin
id("io.gitlab.arturbosch.detekt") version "1.17.1" id("org.jetbrains.qodana") version "0.1.13"
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
} }
group = properties("pluginGroup") group = properties("pluginGroup")
@ -26,55 +23,45 @@ version = properties("pluginVersion")
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
}
// Configure gradle-intellij-plugin plugin. // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
// Read more: https://github.com/JetBrains/gradle-intellij-plugin
intellij { intellij {
pluginName.set(properties("pluginName")) pluginName.set(properties("pluginName"))
version.set(properties("platformVersion")) version.set(properties("platformVersion"))
type.set(properties("platformType")) type.set(properties("platformType"))
downloadSources.set(properties("platformDownloadSources").toBoolean())
updateSinceUntilBuild.set(true)
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty))
} }
// Configure gradle-changelog-plugin plugin. // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
// Read more: https://github.com/JetBrains/gradle-changelog-plugin
changelog { changelog {
version = properties("pluginVersion") version.set(properties("pluginVersion"))
groups = emptyList() groups.set(emptyList())
} }
// Configure detekt plugin. // Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin
// Read more: https://detekt.github.io/detekt/kotlindsl.html qodana {
detekt { cachePath.set(projectDir.resolve(".qodana").canonicalPath)
config = files("./detekt-config.yml") reportPath.set(projectDir.resolve("build/reports/inspections").canonicalPath)
buildUponDefaultConfig = true saveReport.set(true)
showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false)
reports {
html.enabled = false
xml.enabled = false
txt.enabled = false
}
} }
tasks { tasks {
// Set the compatibility versions to 1.8 // Set the JVM compatibility versions
withType<JavaCompile> { properties("javaVersion").let {
sourceCompatibility = "1.8" withType<JavaCompile> {
targetCompatibility = "1.8" sourceCompatibility = it
} targetCompatibility = it
withType<KotlinCompile> { }
kotlinOptions.jvmTarget = "1.8" withType<KotlinCompile> {
kotlinOptions.jvmTarget = it
}
} }
withType<Detekt> { wrapper {
jvmTarget = "1.8" gradleVersion = properties("gradleVersion")
} }
patchPluginXml { patchPluginXml {
@ -84,7 +71,7 @@ tasks {
// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest // Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
pluginDescription.set( pluginDescription.set(
File(projectDir, "README.md").readText().lines().run { projectDir.resolve("README.md").readText().lines().run {
val start = "<!-- Plugin description -->" val start = "<!-- Plugin description -->"
val end = "<!-- Plugin description end -->" val end = "<!-- Plugin description end -->"
@ -96,11 +83,26 @@ tasks {
) )
// Get the latest available change notes from the changelog file // 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 { // Configure UI tests plugin
ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty)) // 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", "<!--999.999-->")
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 { publishPlugin {

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
example/images/key-edit.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
example/images/settings.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -3,24 +3,29 @@
pluginGroup = de.marhali.easyi18n pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n 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 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions. # for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild = 202 pluginSinceBuild = 203
pluginUntilBuild = 212.* pluginUntilBuild = 213.*
# 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
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
platformType = IC platformType = IC
platformVersion = 2021.2 platformVersion = 2020.3.4
platformDownloadSources = true
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = org.jetbrains.kotlin 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. # 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 kotlin.stdlib.default.dependency = false

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
gradlew vendored
View File

@ -72,7 +72,7 @@ case "`uname`" in
Darwin* ) Darwin* )
darwin=true darwin=true
;; ;;
MINGW* ) MSYS* | MINGW* )
msys=true msys=true
;; ;;
NONSTOP* ) NONSTOP* )

6
qodana.yml Normal file
View File

@ -0,0 +1,6 @@
# Qodana configuration:
# https://www.jetbrains.com/help/qodana/qodana-yaml.html
version: 1.0
profile:
name: qodana.recommended

View File

@ -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<BusListener> 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));
}
};
}
}

View File

@ -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<IOStrategy> 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<Boolean> 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<Boolean> 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");
}
}

View File

@ -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<Project, InstanceManager> 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());
}
}
});
}
}

View File

@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import de.marhali.easyi18n.service.WindowManager; import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.dialog.AddDialog; import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.util.PathUtil;
import de.marhali.easyi18n.util.TreeUtil; import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -42,7 +43,7 @@ public class AddAction extends AnAction {
TreePath path = manager.getTreeView().getTree().getSelectionPath(); TreePath path = manager.getTreeView().getTree().getSelectionPath();
if(path != null) { if(path != null) {
return TreeUtil.getFullPath(path) + "."; return TreeUtil.getFullPath(path) + PathUtil.DELIMITER;
} }
} else { // Table View } else { // Table View

View File

@ -4,7 +4,7 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.AnActionEvent;
import de.marhali.easyi18n.service.DataStore; import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -23,6 +23,9 @@ public class ReloadAction extends AnAction {
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { 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());
});
} }
} }

View File

@ -7,8 +7,9 @@ import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField; 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.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationCreate; import de.marhali.easyi18n.model.TranslationCreate;
import javax.swing.*; import javax.swing.*;
@ -48,16 +49,16 @@ public class AddDialog {
} }
private void saveTranslation() { private void saveTranslation() {
Map<String, String> messages = new HashMap<>(); Translation translation = new Translation();
valueTextFields.forEach((k, v) -> { valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) { if(!v.getText().isEmpty()) {
messages.put(k, v.getText()); translation.put(k, v.getText());
} }
}); });
TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), messages)); TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), translation));
DataStore.getInstance(project).processUpdate(creation); InstanceManager.get(project).processUpdate(creation);
} }
private DialogBuilder prepare() { private DialogBuilder prepare() {
@ -75,7 +76,8 @@ public class AddDialog {
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2)); JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
valueTextFields = new HashMap<>(); 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); JBLabel localeLabel = new JBLabel(locale);
JBTextField localeText = new JBTextField(); JBTextField localeText = new JBTextField();
localeLabel.setLabelFor(localeText); localeLabel.setLabelFor(localeText);

View File

@ -6,11 +6,12 @@ import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField; 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.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationDelete; import de.marhali.easyi18n.model.TranslationDelete;
import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor; import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor;
import de.marhali.easyi18n.model.TranslationUpdate;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EtchedBorder; import javax.swing.border.EtchedBorder;
@ -40,23 +41,22 @@ public class EditDialog {
int code = prepare().show(); int code = prepare().show();
if(code == DialogWrapper.OK_EXIT_CODE) { // Edit 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 } else if(code == DeleteActionDescriptor.EXIT_CODE) { // Delete
DataStore.getInstance(project).processUpdate(new TranslationDelete(origin)); InstanceManager.get(project).processUpdate(new TranslationDelete(origin));
} }
} }
private KeyedTranslation getChanges() { private KeyedTranslation getChanges() {
Map<String, String> messages = new HashMap<>(); Translation translation = new Translation();
valueTextFields.forEach((k, v) -> { valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) { 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() { private DialogBuilder prepare() {
@ -74,9 +74,10 @@ public class EditDialog {
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2)); JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
valueTextFields = new HashMap<>(); 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); 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); localeLabel.setLabelFor(localeText);
valuePanel.add(localeLabel); valuePanel.add(localeLabel);

View File

@ -9,8 +9,9 @@ import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField; 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.SettingsService;
import de.marhali.easyi18n.service.DataStore;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@ -28,6 +29,8 @@ public class SettingsDialog {
private JBTextField filePatternText; private JBTextField filePatternText;
private JBTextField previewLocaleText; private JBTextField previewLocaleText;
private JBTextField pathPrefixText; private JBTextField pathPrefixText;
private JBCheckBox sortKeysCheckbox;
private JBCheckBox nestedKeysCheckbox;
private JBCheckBox codeAssistanceCheckbox; private JBCheckBox codeAssistanceCheckbox;
public SettingsDialog(Project project) { public SettingsDialog(Project project) {
@ -35,30 +38,31 @@ public class SettingsDialog {
} }
public void showAndHandle() { public void showAndHandle() {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath(); SettingsState state = SettingsService.getInstance(project).getState();
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();
if(prepare(localesPath, filePattern, previewLocale, pathPrefix, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes if(prepare(state).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText()); state.setLocalesPath(pathText.getText());
SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText()); state.setFilePattern(filePatternText.getText());
SettingsService.getInstance(project).getState().setPreviewLocale(previewLocaleText.getText()); state.setPreviewLocale(previewLocaleText.getText());
SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected()); state.setPathPrefix(pathPrefixText.getText());
SettingsService.getInstance(project).getState().setPathPrefix(pathPrefixText.getText()); state.setSortKeys(sortKeysCheckbox.isSelected());
state.setNestedKeys(nestedKeysCheckbox.isSelected());
state.setCodeAssistance(codeAssistanceCheckbox.isSelected());
// Reload instance // 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)); JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
/* path */ /* path */
JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text")); 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); pathLabel.setLabelFor(pathText);
pathText.addBrowseFolderListener(ResourceBundle.getBundle("messages").getString("settings.path.title"), null, project, new FileChooserDescriptor( pathText.addBrowseFolderListener(ResourceBundle.getBundle("messages").getString("settings.path.title"), null, project, new FileChooserDescriptor(
@ -69,14 +73,14 @@ public class SettingsDialog {
/* file pattern */ /* file pattern */
JBLabel filePatternLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.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(filePatternLabel);
rootPanel.add(filePatternText); rootPanel.add(filePatternText);
/* preview locale */ /* preview locale */
JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview")); JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview"));
previewLocaleText = new JBTextField(previewLocale); previewLocaleText = new JBTextField(state.getPreviewLocale());
previewLocaleLabel.setLabelFor(previewLocaleText); previewLocaleLabel.setLabelFor(previewLocaleText);
rootPanel.add(previewLocaleLabel); rootPanel.add(previewLocaleLabel);
@ -84,14 +88,26 @@ public class SettingsDialog {
/* path prefix */ /* path prefix */
JBLabel pathPrefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.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(pathPrefixLabel);
rootPanel.add(pathPrefixText); 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 */ /* code assistance */
codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance")); codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance"));
codeAssistanceCheckbox.setSelected(codeAssistance); codeAssistanceCheckbox.setSelected(state.isCodeAssistance());
rootPanel.add(codeAssistanceCheckbox); rootPanel.add(codeAssistanceCheckbox);

View File

@ -4,8 +4,8 @@ import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.LocalizedNode; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.service.DataStore; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -39,7 +39,7 @@ public class KeyAnnotator {
searchKey = searchKey.substring(1); 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 if(node == null) { // Unknown translation. Just ignore it
return; return;

View File

@ -5,9 +5,11 @@ import com.intellij.codeInsight.lookup.*;
import com.intellij.icons.AllIcons; import com.intellij.icons.AllIcons;
import com.intellij.openapi.project.*; import com.intellij.openapi.project.*;
import com.intellij.util.*; 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.service.*;
import de.marhali.easyi18n.util.TranslationsUtil; import de.marhali.easyi18n.util.PathUtil;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
import java.util.*; import java.util.*;
@ -29,7 +31,8 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
return; return;
} }
DataStore store = DataStore.getInstance(project); DataStore store = InstanceManager.get(project).store();
PathUtil pathUtil = new PathUtil(project);
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix(); String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
@ -54,7 +57,7 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
pathPrefix += "."; pathPrefix += ".";
} }
List<String> fullKeys = store.getTranslations().getFullKeys(); Set<String> fullKeys = store.getData().getFullKeys();
int sections = path.split("\\.").length; int sections = path.split("\\.").length;
int maxSectionForwardLookup = 5; int maxSectionForwardLookup = 5;
@ -65,19 +68,20 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
String[] keySections = key.split("\\."); String[] keySections = key.split("\\.");
if(keySections.length > sections + maxSectionForwardLookup) { // Key is too deep nested if(keySections.length > sections + maxSectionForwardLookup) { // Key is too deep nested
String shrinkKey = TranslationsUtil.sectionsToFullPath(Arrays.asList( String shrinkKey = pathUtil.concat(Arrays.asList(
Arrays.copyOf(keySections, sections + maxSectionForwardLookup))); Arrays.copyOf(keySections, sections + maxSectionForwardLookup)
));
result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey) result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey)
.appendTailText(" I18n([])", true)); .appendTailText(" I18n([])", true));
} else { } else {
LocalizedNode node = store.getTranslations().getNode(key); Translation translation = store.getData().getTranslation(key);
String translation = node != null ? node.getValue().get(previewLocale) : null; String content = translation.get(previewLocale);
result.addElement(LookupElementBuilder.create(pathPrefix + key) result.addElement(LookupElementBuilder.create(pathPrefix + key)
.withIcon(AllIcons.Actions.PreserveCaseHover) .withIcon(AllIcons.Actions.PreserveCaseHover)
.appendTailText(" I18n(" + previewLocale + ": " + translation + ")", true) .appendTailText(" I18n(" + previewLocale + ": " + content + ")", true)
); );
} }
} }

View File

@ -4,12 +4,12 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*; import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement; import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.AddDialog; import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -52,10 +52,10 @@ public class KeyReference extends PsiReferenceBase<PsiElement> {
@Override @Override
public void navigate(boolean requestFocus) { 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) { if(translation != null) {
new EditDialog(getProject(), new KeyedTranslation(getKey(), node.getValue())).showAndHandle(); new EditDialog(getProject(), new KeyedTranslation(getKey(), translation)).showAndHandle();
} else { } else {
new AddDialog(getProject(), getKey()).showAndHandle(); new AddDialog(getProject(), getKey()).showAndHandle();
} }

View File

@ -4,8 +4,8 @@ import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*; import com.intellij.psi.*;
import com.intellij.util.ProcessingContext; import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference; import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -38,7 +38,7 @@ public class GenericKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY; 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 if(!KeyReference.isReferencable(value)) { // Creation policy
return PsiReference.EMPTY_ARRAY; return PsiReference.EMPTY_ARRAY;
} }

View File

@ -5,8 +5,8 @@ import com.intellij.psi.*;
import com.intellij.util.ProcessingContext; import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference; import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -45,7 +45,7 @@ public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY; 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; return PsiReference.EMPTY_ARRAY;
} }

View File

@ -1,6 +1,7 @@
package de.marhali.easyi18n.util.array; package de.marhali.easyi18n.io;
import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
import java.text.MessageFormat; import java.text.MessageFormat;
@ -10,11 +11,13 @@ import java.util.function.Function;
import java.util.regex.Pattern; 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 * @author marhali
*/ */
public abstract class ArrayUtil { public abstract class ArrayMapper {
static final String PREFIX = "!arr["; static final String PREFIX = "!arr[";
static final String SUFFIX = "]"; static final String SUFFIX = "]";
static final char DELIMITER = ';'; static final char DELIMITER = ';';
@ -22,7 +25,7 @@ public abstract class ArrayUtil {
static final String SPLITERATOR_REGEX = static final String SPLITERATOR_REGEX =
MessageFormat.format("(?<!\\\\){0}", Pattern.quote(String.valueOf(DELIMITER))); MessageFormat.format("(?<!\\\\){0}", Pattern.quote(String.valueOf(DELIMITER)));
static <T> String read(Iterator<T> elements, Function<T, String> stringFactory) { protected static <T> String read(Iterator<T> elements, Function<T, String> stringFactory) {
StringBuilder builder = new StringBuilder(PREFIX); StringBuilder builder = new StringBuilder(PREFIX);
int i = 0; int i = 0;
@ -43,7 +46,7 @@ public abstract class ArrayUtil {
return builder.toString(); return builder.toString();
} }
static void write(String concat, Consumer<String> writeElement) { protected static void write(String concat, Consumer<String> writeElement) {
concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length()); concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length());
for(String element : concat.split(SPLITERATOR_REGEX)) { for(String element : concat.split(SPLITERATOR_REGEX)) {

View File

@ -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<Boolean> 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());
}
}

View File

@ -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<Translations> 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<Boolean> callback);
}

View File

@ -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<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
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<Boolean> 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);
}
});
}
}

View File

@ -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<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] localeDirectories = directory.getChildren();
List<String> 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<Boolean> 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);
}
});
}
}

View File

@ -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<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for (VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
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<Boolean> 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<String> 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<String, String> messages = node.getValue();
String escapedValue = StringUtil.escapeControls(String.valueOf(value), true);
messages.put(locale, escapedValue);
node.setValue(messages);
});
}
}

View File

@ -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<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
Section section = Section.parseToMap(reader);
load(file.getNameWithoutExtension(), nodes, section);
}
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
private void load(String locale, LocalizedNode node, Section section) {
if (section instanceof MapSection) {
for (String key : section.getKeys()) {
LocalizedNode child = node.getChildren(key);
if (child == null) {
node.addChildren(child = new LocalizedNode(key, new ArrayList<>()));
}
LocalizedNode finalChild = child;
MapSection map = section.getMap(key).orElse(null);
if (map != null) {
load(locale, finalChild, map);
} else {
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<Boolean> callback) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
Section section = new MapSection();
save(translations.getNodes(), locale, section, null);
String fullPath = directoryPath + "/" + locale + ".yml";
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
file.setBinaryContent(Section.toString(section).getBytes(file.getCharset()));
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
}

View File

@ -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.JsonArray;
import com.google.gson.JsonElement; 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 * @author marhali
*/ */
public class JsonArrayUtil extends ArrayUtil { public class JsonArrayMapper extends ArrayMapper {
public static String read(JsonArray array) { public static String read(JsonArray array) {
return read(array.iterator(), JsonElement::getAsString); return read(array.iterator(), JsonElement::getAsString);
} }

View File

@ -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<Boolean> 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);
}
});
}
}

View File

@ -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<String, JsonElement> 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<String, TranslationNode> 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)));
}
}
}
}
}
}

View File

@ -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. <br/>
* Full key example: <moduleFileName>.<username>.<title>
*
* @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:
// <localesPath>/<localeDir>/<moduleFile>
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<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
for(Map.Entry<String, TranslationNode> 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);
}
});
}
}

View File

@ -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<String> list = new ArrayList<>();
write(concat, list::add);
return list.toArray(new String[0]);
}
}

View File

@ -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<Boolean> 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);
}
});
}
}

View File

@ -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<Object, Object> entry : properties.entrySet()) {
String key = String.valueOf(entry.getKey());
Object value = entry.getValue();
Translation translation = data.getTranslation(key);
if(translation == null) {
translation = new Translation();
}
String content = value instanceof String[]
? PropertiesArrayMapper.read((String[]) value)
: StringUtil.escapeControls(String.valueOf(value), true);
translation.put(locale, content);
data.setTranslation(key, translation);
}
}
public static void write(String locale, SortableProperties properties, TranslationData data) {
for(String key : data.getFullKeys()) {
Translation translation = data.getTranslation(key);
if(translation != null && translation.containsKey(locale)) {
String content = translation.get(locale);
if(PropertiesArrayMapper.isArray(content)) {
properties.put(key, PropertiesArrayMapper.write(content));
} else if(NumberUtils.isNumber(content)) {
properties.put(key, NumberUtils.createNumber(content));
} else {
properties.put(key, content);
}
}
}
}
}

View File

@ -0,0 +1,45 @@
package de.marhali.easyi18n.io.properties;
import java.util.*;
/**
* Extends {@link Properties} class to support sorted or non-sorted keys.
* @author marhali
*/
public class SortableProperties extends Properties {
private final transient Map<Object, Object> properties;
public SortableProperties(boolean sort) {
this.properties = sort ? new TreeMap<>() : new LinkedHashMap<>();
}
public Map<Object, Object> getProperties() {
return this.properties;
}
@Override
public Object get(Object key) {
return this.properties.get(key);
}
@Override
public Set<Object> keySet() {
return Collections.unmodifiableSet(this.properties.keySet());
}
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
return this.properties.entrySet();
}
@Override
public synchronized Object put(Object key, Object value) {
return this.properties.put(key, value);
}
@Override
public String toString() {
return this.properties.toString();
}
}

View File

@ -1,15 +1,16 @@
package de.marhali.easyi18n.util.array; package de.marhali.easyi18n.io.yaml;
import de.marhali.easyi18n.io.ArrayMapper;
import thito.nodeflow.config.ListSection; import thito.nodeflow.config.ListSection;
/** /**
* Utility methods to read and write yaml lists. * Map for yaml array values.
* @author marhali * @author marhali
*/ */
public class YamlArrayUtil extends ArrayUtil { public class YamlArrayMapper extends ArrayMapper {
public static String read(ListSection list) { public static String read(ListSection list) {
return read(list.iterator(), Object::toString); return read(list.iterator(), Object::toString);
} }
public static ListSection write(String concat) { public static ListSection write(String concat) {

View File

@ -0,0 +1,121 @@
package de.marhali.easyi18n.io.yaml;
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 thito.nodeflow.config.MapSection;
import thito.nodeflow.config.Section;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.function.Consumer;
/**
* Strategy for simple yaml locale files. Each locale has its own file.
* For example localesPath/en.y(a)ml, localesPath/de.y(a)ml
* @author marhali
*/
public class YamlIOStrategy implements IOStrategy {
private final String FILE_EXTENSION;
public YamlIOStrategy(@NotNull String fileExtension) {
this.FILE_EXTENSION = fileExtension;
}
@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);
try(Reader reader = new InputStreamReader(file.getInputStream(), file.getCharset())) {
Section section = Section.parseToMap(reader);
YamlMapper.read(locale, section, 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<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
Section section = new MapSection();
YamlMapper.write(locale, section, 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(Section.toString(section).getBytes(vf.getCharset()));
}
result.accept(true);
} catch(IOException e) {
e.printStackTrace();
result.accept(false);
}
});
}
}

View File

@ -0,0 +1,72 @@
package de.marhali.easyi18n.io.yaml;
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 thito.nodeflow.config.ListSection;
import thito.nodeflow.config.MapSection;
import thito.nodeflow.config.Section;
import java.util.Map;
/**
* Mapper for mapping yaml files into translation nodes and backwards.
* @author marhali
*/
public class YamlMapper {
public static void read(String locale, Section section, TranslationNode node) {
for(String key : section.getKeys()) {
Object value = section.getInScope(key).get();
TranslationNode childNode = node.getOrCreateChildren(key);
if(value instanceof MapSection) {
// Nested element - run recursively
read(locale, (MapSection) value, childNode);
} else {
Translation translation = childNode.getValue();
String content = value instanceof ListSection
? YamlArrayMapper.read((ListSection) value)
: StringUtil.escapeControls(String.valueOf(value), true);
translation.put(locale, content);
childNode.setValue(translation);
}
}
}
public static void write(String locale, Section section, TranslationNode node) {
for(Map.Entry<String, TranslationNode> entry : node.getChildren().entrySet()) {
String key = entry.getKey();
TranslationNode childNode = entry.getValue();
if(!childNode.isLeaf()) {
// Nested node - run recursively
MapSection childSection = new MapSection();
write(locale, childSection, childNode);
if(childSection.size() > 0) {
section.setInScope(key, childSection);
}
} else {
Translation translation = childNode.getValue();
String content = translation.get(locale);
if(content != null) {
if(YamlArrayMapper.isArray(content)) {
section.setInScope(key, YamlArrayMapper.write(content));
} else if(NumberUtils.isNumber(content)) {
section.setInScope(key, NumberUtils.createNumber(content));
} else {
section.setInScope(key, StringEscapeUtils.unescapeJava(content));
}
}
}
}
}
}

View File

@ -1,19 +0,0 @@
package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Interface to communicate data changes between data store and ui components.
* @author marhali
*/
public interface DataSynchronizer {
/**
* Propagates data changes to implementation classes.
* @param translations Updated translations model
* @param searchQuery Can be used to filter visible data. Like a search function for the full key path
* @param scrollToKey Focus specific translation. Can be null to disable this function
*/
void synchronize(@NotNull Translations translations, @Nullable String searchQuery, @Nullable String scrollToKey);
}

View File

@ -1,42 +1,43 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model;
import java.util.Map; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Translated messages for a dedicated key. * I18n translation with associated key path (full-key).
* @author marhali * @author marhali
*/ */
public class KeyedTranslation { public class KeyedTranslation {
private String key; private @NotNull String key;
private Map<String, String> translations; private @Nullable Translation translation;
public KeyedTranslation(String key, Map<String, String> translations) { public KeyedTranslation(@NotNull String key, @Nullable Translation translation) {
this.key = key; this.key = key;
this.translations = translations; this.translation = translation;
} }
public String getKey() { public @NotNull String getKey() {
return key; return key;
} }
public void setKey(String key) { public void setKey(@NotNull String key) {
this.key = key; this.key = key;
} }
public Map<String, String> getTranslations() { public @Nullable Translation getTranslation() {
return translations; return translation;
} }
public void setTranslations(Map<String, String> translations) { public void setTranslation(@NotNull Translation translation) {
this.translations = translations; this.translation = translation;
} }
@Override @Override
public String toString() { public String toString() {
return "KeyedTranslation{" + return "KeyedTranslation{" +
"key='" + key + '\'' + "key='" + key + '\'' +
", translations=" + translations + ", translation=" + translation +
'}'; '}';
} }
} }

View File

@ -1,77 +0,0 @@
package de.marhali.easyi18n.model;
import de.marhali.easyi18n.util.MapUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Represents structured tree view for translated messages.
* @author marhali
*/
public class LocalizedNode {
public static final String ROOT_KEY = "root";
@NotNull
private final String key;
@NotNull
private TreeMap<String, LocalizedNode> children;
@NotNull
private Map<String, String> value;
public LocalizedNode(@NotNull String key, @NotNull List<LocalizedNode> children) {
this.key = key;
this.children = MapUtil.convertToTreeMap(children);
this.value = new HashMap<>();
}
public LocalizedNode(@NotNull String key, @NotNull Map<String, String> value) {
this.key = key;
this.children = new TreeMap<>();
this.value = value;
}
public @NotNull String getKey() {
return key;
}
public boolean isLeaf() {
return children.isEmpty();
}
public @NotNull Collection<LocalizedNode> getChildren() {
return children.values();
}
public @Nullable LocalizedNode getChildren(@NotNull String key) {
return children.get(key);
}
public void setChildren(@NotNull LocalizedNode... children) {
this.value.clear();
this.children = MapUtil.convertToTreeMap(Arrays.asList(children));
}
public void addChildren(@NotNull LocalizedNode... children) {
this.value.clear();
Arrays.stream(children).forEach(e -> this.children.put(e.getKey(), e));
}
public void removeChildren(@NotNull String key) {
this.children.remove(key);
}
public @NotNull Map<String, String> getValue() {
return value;
}
public void setValue(@NotNull Map<String, String> value) {
this.children.clear();
this.value = value;
}
}

View File

@ -12,12 +12,16 @@ public class SettingsState {
public static final String DEFAULT_PREVIEW_LOCALE = "en"; public static final String DEFAULT_PREVIEW_LOCALE = "en";
public static final String DEFAULT_FILE_PATTERN = ".*"; public static final String DEFAULT_FILE_PATTERN = ".*";
public static final String DEFAULT_PATH_PREFIX = ""; public static final String DEFAULT_PATH_PREFIX = "";
public static final boolean DEFAULT_SORT_KEYS = true;
public static final boolean DEFAULT_NESTED_KEYS = true;
public static final boolean DEFAULT_CODE_ASSISTANCE = true; public static final boolean DEFAULT_CODE_ASSISTANCE = true;
private String localesPath; private String localesPath;
private String filePattern; private String filePattern;
private String previewLocale; private String previewLocale;
private String pathPrefix; private String pathPrefix;
private Boolean sortKeys;
private Boolean nestedKeys;
private Boolean codeAssistance; private Boolean codeAssistance;
public SettingsState() {} public SettingsState() {}
@ -54,6 +58,22 @@ public class SettingsState {
this.pathPrefix = pathPrefix; this.pathPrefix = pathPrefix;
} }
public boolean isSortKeys() {
return sortKeys == null ? DEFAULT_SORT_KEYS : sortKeys;
}
public void setSortKeys(boolean sortKeys) {
this.sortKeys = sortKeys;
}
public boolean isNestedKeys() {
return nestedKeys == null ? DEFAULT_NESTED_KEYS : nestedKeys;
}
public void setNestedKeys(boolean nestedKeys) {
this.nestedKeys = nestedKeys;
}
public boolean isCodeAssistance() { public boolean isCodeAssistance() {
return codeAssistance == null ? DEFAULT_CODE_ASSISTANCE : codeAssistance; return codeAssistance == null ? DEFAULT_CODE_ASSISTANCE : codeAssistance;
} }

View File

@ -0,0 +1,29 @@
package de.marhali.easyi18n.model;
import java.util.HashMap;
/**
* Represents all translations for an element. The assignment to an element is done in the using class.
* This class contains only the translations for this unspecific element.
* @author marhali
*/
public class Translation extends HashMap<String, String> {
public Translation() {
super();
}
public Translation(String locale, String content) {
this();
super.put(locale, content);
}
public Translation add(String locale, String content) {
super.put(locale, content);
return this;
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,186 @@
package de.marhali.easyi18n.model;
import de.marhali.easyi18n.util.PathUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Cached translation data. The data is stored in a tree structure.
* Tree behaviour (sorted, non-sorted) can be specified via constructor.
* For more please see {@link TranslationNode}. Example tree view:
* <br/>
* user: <br/>
* -- principal: 'Principal' <br/>
* -- username: <br/>
* -- -- title: 'Username' <br/>
* auth: <br/>
* -- logout: 'Logout' <br/>
* -- login: 'Login' <br/>
*
* @author marhali
*/
public class TranslationData {
private final PathUtil pathUtil;
@NotNull
private final Set<String> locales;
@NotNull
private final TranslationNode rootNode;
/**
* Creates an empty instance.
* @param sort Should the translation keys be sorted alphabetically
*/
public TranslationData(boolean sort, boolean nestKeys) {
this(nestKeys, new HashSet<>(), new TranslationNode(sort ? new TreeMap<>() : new LinkedHashMap<>()));
}
/**
* @param nestKeys Apply key nesting. See {@link PathUtil}
* @param locales Languages which can be used for translation
* @param rootNode Translation tree structure
*/
public TranslationData(boolean nestKeys, @NotNull Set<String> locales, @NotNull TranslationNode rootNode) {
this.pathUtil = new PathUtil(nestKeys);
this.locales = locales;
this.rootNode = rootNode;
}
/**
* @return Set of languages which can receive translations
*/
public @NotNull Set<String> getLocales() {
return this.locales;
}
/**
* @param locale Adds the provided locale to the supported languages list
*/
public void addLocale(@NotNull String locale) {
this.locales.add(locale);
}
/**
* @return root node which contains all translations
*/
public @NotNull TranslationNode getRootNode() {
return this.rootNode;
}
/**
* @param fullPath Absolute translation path
* @return Translation node which leads to translations or nested child's
*/
public @Nullable TranslationNode getNode(@NotNull String fullPath) {
List<String> sections = this.pathUtil.split(fullPath);
TranslationNode node = this.rootNode;
if(fullPath.isEmpty()) { // Return root node if empty path was supplied
return node;
}
for(String section : sections) {
if(node == null) {
return null;
}
node = node.getChildren().get(section);
}
return node;
}
/**
* @param fullPath Absolute translation key path
* @return Found translation. Can be null if path is empty or is not a leaf element
*/
public @Nullable Translation getTranslation(@NotNull String fullPath) {
TranslationNode node = this.getNode(fullPath);
if(node == null || !node.isLeaf()) {
return null;
}
return node.getValue();
}
/**
* @param fullPath Absolute translation key path
* @param translation Translation to set. Can be null to delete the corresponding node
*/
public void setTranslation(@NotNull String fullPath, @Nullable Translation translation) {
List<String> sections = this.pathUtil.split(fullPath);
String nodeKey = sections.remove(sections.size() - 1); // Edge case last section
TranslationNode node = this.rootNode;
if(fullPath.isEmpty()) {
throw new IllegalArgumentException("Path cannot be empty");
}
for(String section : sections) { // Go to the level of the key (@nodeKey)
TranslationNode childNode = node.getChildren().get(section);
if(childNode == null) {
if(translation == null) { // Path should not be empty for delete
throw new IllegalArgumentException("Delete action on empty path");
}
// Created nested section
childNode = node.setChildren(section);
}
node = childNode;
}
if(translation == null) { // Delete
node.removeChildren(nodeKey);
if(node.getChildren().isEmpty() && !node.isRoot()) { // Parent is empty now. Run delete recursively
this.setTranslation(this.pathUtil.concat(sections), null);
}
} else { // Create or overwrite
node.setChildren(nodeKey, translation);
}
}
/**
* @return All translation keys as absolute paths (full-key)
*/
public @NotNull Set<String> getFullKeys() {
return this.getFullKeys("", this.rootNode); // Just use root node
}
/**
* @param parentPath Parent key path
* @param node Node section to begin with
* @return All translation keys where the path contains the specified @parentPath
*/
public @NotNull Set<String> getFullKeys(String parentPath, TranslationNode node) {
Set<String> keys = new LinkedHashSet<>();
if(node.isLeaf()) { // This node does not lead to child's - just add the key
keys.add(parentPath);
}
for(Map.Entry<String, TranslationNode> children : node.getChildren().entrySet()) {
keys.addAll(this.getFullKeys(this.pathUtil.append(parentPath, children.getKey()), children.getValue()));
}
return keys;
}
@Override
public String toString() {
return "TranslationData{" +
"mapClass=" + rootNode.getChildren().getClass().getSimpleName() +
", pathUtil=" + pathUtil +
", locales=" + locales +
", rootNode=" + rootNode +
'}';
}
}

View File

@ -0,0 +1,117 @@
package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
/**
* Translation tree node. Manages child nodes which can be translations or also
* nodes which can lead to another translation or node.
* Navigation inside a node can be upward and downward. To construct the full
* translation key (full-key) every parent needs to be resolved recursively.
* -
* Whether the children nodes should be sorted is determined by the parent node.
* For root nodes (empty parent) the {@link java.util.Map}-Type must be specified
* to determine which sorting should be applied.
*
* @author marhali
*/
public class TranslationNode {
@Nullable
private TranslationNode parent;
@NotNull
private Map<String, TranslationNode> children;
@NotNull
private Translation value;
/**
* Root node initializer. E.g. see {@link java.util.TreeMap} or {@link java.util.HashMap}
* @param children Decide which implementation should be used (sorted, non-sorted)
*/
public TranslationNode(@NotNull Map<String, TranslationNode> children) {
this.parent = null;
this.children = children;
this.value = new Translation();
}
/**
* @return true if this node is considered as root node
*/
public boolean isRoot() {
return this.parent == null;
}
/**
* @return true if this node does not lead to other children nodes (just contains {@link Translation} itself).
* The root node is never treated as a leaf node.
*/
public boolean isLeaf() {
return this.children.isEmpty() && !this.isRoot();
}
public void setParent(@Nullable TranslationNode parent) {
this.parent = parent;
}
public @NotNull Translation getValue() {
return value;
}
public void setValue(@NotNull Translation value) {
this.children.clear();
this.value = value;
}
public @NotNull Map<String, TranslationNode> getChildren() {
return this.children;
}
public void setChildren(@NotNull String key, @NotNull TranslationNode node) {
node.setParent(this); // Track parent if adding children's
this.value.clear();
this.children.put(key, node);
}
@SuppressWarnings("unchecked")
public @NotNull TranslationNode setChildren(@NotNull String key) {
try {
TranslationNode node = new TranslationNode(this.children.getClass().getDeclaredConstructor().newInstance());
this.setChildren(key, node);
return node;
} catch(Exception e) {
e.printStackTrace();
throw new RuntimeException("Cannot create children of map type " + this.children.getClass().getSimpleName());
}
}
public void setChildren(@NotNull String key, @NotNull Translation translation) {
this.setChildren(key).setValue(translation);
}
public @NotNull TranslationNode getOrCreateChildren(@NotNull String key) {
TranslationNode node = this.children.get(key);
if(node == null) {
node = this.setChildren(key);
}
return node;
}
public void removeChildren(@NotNull String key) {
this.children.remove(key);
}
@Override
public String toString() {
return "TranslationNode{" +
"parent=" + parent +
", children=" + children.keySet() +
", value=" + value +
'}';
}
}

View File

@ -3,7 +3,9 @@ package de.marhali.easyi18n.model;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Represents an update for a translated I18n-Key. Supports key creation, manipulation and deletion. * Represents an update for a translated i18n key.
* Supports translation creation, manipulation and deletion.
*
* @author marhali * @author marhali
*/ */
public class TranslationUpdate { public class TranslationUpdate {
@ -16,24 +18,24 @@ public class TranslationUpdate {
this.change = change; this.change = change;
} }
public KeyedTranslation getOrigin() { public @Nullable KeyedTranslation getOrigin() {
return origin; return origin;
} }
public KeyedTranslation getChange() { public @Nullable KeyedTranslation getChange() {
return change; return change;
} }
public boolean isCreation() { public boolean isCreation() {
return origin == null; return this.origin == null;
} }
public boolean isDeletion() { public boolean isDeletion() {
return change == null; return this.change == null;
} }
public boolean isKeyChange() { public boolean isKeyChange() {
return origin != null && change != null && !origin.getKey().equals(change.getKey()); return this.origin != null && this.change != null && !this.origin.getKey().equals(this.change.getKey());
} }
@Override @Override

View File

@ -1,108 +0,0 @@
package de.marhali.easyi18n.model;
import de.marhali.easyi18n.util.TranslationsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents translation state instance. IO operations will be based on this file.
* @author marhali
*/
public class Translations {
public static Translations empty() {
return new Translations(new ArrayList<>(), new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>()));
}
@NotNull
private final List<String> locales;
@NotNull
private final LocalizedNode nodes;
/**
* Constructs a new translation state instance.
* @param locales List of all locales which are used for create / edit I18n-Key operations
* @param nodes Represents the translation state. Internally handled as a tree. See {@link LocalizedNode}
*/
public Translations(@NotNull List<String> locales, @NotNull LocalizedNode nodes) {
this.locales = locales;
this.nodes = nodes;
}
public @NotNull List<String> getLocales() {
return locales;
}
public @NotNull LocalizedNode getNodes() {
return nodes;
}
public @Nullable LocalizedNode getNode(@NotNull String fullPath) {
List<String> sections = TranslationsUtil.getSections(fullPath);
LocalizedNode node = nodes;
for(String section : sections) {
if(node == null) {
return null;
}
node = node.getChildren(section);
}
return node;
}
public @NotNull LocalizedNode getOrCreateNode(@NotNull String fullPath) {
List<String> sections = TranslationsUtil.getSections(fullPath);
LocalizedNode node = nodes;
for(String section : sections) {
LocalizedNode subNode = node.getChildren(section);
if(subNode == null) {
subNode = new LocalizedNode(section, new ArrayList<>());
node.addChildren(subNode);
}
node = subNode;
}
return node;
}
public @NotNull List<String> getFullKeys() {
List<String> keys = new ArrayList<>();
if(nodes.isLeaf()) { // Root has no children
return keys;
}
for(LocalizedNode children : nodes.getChildren()) {
keys.addAll(getFullKeys("", children));
}
return keys;
}
public @NotNull List<String> getFullKeys(String parentFullPath, LocalizedNode localizedNode) {
List<String> keys = new ArrayList<>();
if(localizedNode.isLeaf()) {
keys.add(parentFullPath + (parentFullPath.isEmpty() ? "" : ".") + localizedNode.getKey());
return keys;
}
for(LocalizedNode children : localizedNode.getChildren()) {
String childrenPath = parentFullPath + (parentFullPath.isEmpty() ? "" : ".") + localizedNode.getKey();
keys.addAll(getFullKeys(childrenPath, children));
}
return keys;
}
}

View File

@ -0,0 +1,8 @@
package de.marhali.easyi18n.model.bus;
/**
* Interface for communication of changes for participants of the data bus.
* Every listener needs to be registered manually via {@link de.marhali.easyi18n.DataBus}.
* @author marhali
*/
public interface BusListener extends UpdateDataListener, FocusKeyListener, SearchQueryListener {}

View File

@ -0,0 +1,15 @@
package de.marhali.easyi18n.model.bus;
import org.jetbrains.annotations.Nullable;
/**
* Single event listener.
* @author marhali
*/
public interface FocusKeyListener {
/**
* Move the specified translation key (full-key) into focus.
* @param key Absolute translation key
*/
void onFocusKey(@Nullable String key);
}

View File

@ -0,0 +1,16 @@
package de.marhali.easyi18n.model.bus;
import org.jetbrains.annotations.Nullable;
/**
* Single event listener.
* @author marhali
*/
public interface SearchQueryListener {
/**
* Filter the displayed data according to the search query. Supply 'null' to return to the normal state.
* The keys and the content itself should be considered.
* @param query Filter key or content
*/
void onSearchQuery(@Nullable String query);
}

View File

@ -0,0 +1,16 @@
package de.marhali.easyi18n.model.bus;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
/**
* Single event listener.
* @author marhali
*/
public interface UpdateDataListener {
/**
* Update the translations based on the supplied data.
* @param data Updated translations
*/
void onUpdateData(@NotNull TranslationData data);
}

View File

@ -1,122 +0,0 @@
package de.marhali.easyi18n.model.table;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.model.Translations;
import org.jetbrains.annotations.Nls;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Table model to represents localized messages.
* @author marhali
*/
public class TableModelTranslator implements TableModel {
private final Translations translations;
private final List<String> locales;
private final List<String> fullKeys;
private final Consumer<TranslationUpdate> updater;
/**
* @param translations Translations instance
* @param searchQuery Search / filter param
* @param updater Consumer which can be called on cell change / update
*/
public TableModelTranslator(Translations translations, String searchQuery, Consumer<TranslationUpdate> updater) {
this.translations = translations;
this.locales = translations.getLocales();
this.updater = updater;
List<String> fullKeys = translations.getFullKeys();
if(searchQuery != null && !searchQuery.isEmpty()) { // Filter keys by searchQuery
fullKeys.removeIf(key -> !key.startsWith(searchQuery));
}
this.fullKeys = fullKeys;
}
@Override
public int getRowCount() {
return fullKeys.size();
}
@Override
public int getColumnCount() {
return locales.size() + 1; // Number of locales plus 1 for the Key's column
}
@Nls
@Override
public String getColumnName(int columnIndex) {
if(columnIndex == 0) {
return "<html><b>Key</b></html>";
}
return "<html><b>" + locales.get(columnIndex - 1) + "</b></html>";
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return rowIndex > 0; // Everything should be editable except the headline
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
if(columnIndex == 0) { // Keys
return fullKeys.get(rowIndex);
}
String key = fullKeys.get(rowIndex);
String locale = locales.get(columnIndex - 1);
LocalizedNode node = translations.getNode(key);
return node == null ? null : node.getValue().get(locale);
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
String key = String.valueOf(getValueAt(rowIndex, 0));
LocalizedNode node = translations.getNode(key);
if(node == null) { // Unknown cell
return;
}
String newKey = columnIndex == 0 ? String.valueOf(aValue) : key;
Map<String, String> messages = node.getValue();
// Locale message update
if(columnIndex > 0) {
if(aValue == null || ((String) aValue).isEmpty()) {
messages.remove(locales.get(columnIndex - 1));
} else {
messages.put(locales.get(columnIndex - 1), String.valueOf(aValue));
}
}
TranslationUpdate update = new TranslationUpdate(new KeyedTranslation(key, messages),
new KeyedTranslation(newKey, messages));
updater.accept(update);
}
@Override
public void addTableModelListener(TableModelListener l) {}
@Override
public void removeTableModelListener(TableModelListener l) {}
}

View File

@ -1,132 +0,0 @@
package de.marhali.easyi18n.model.tree;
import com.intellij.ide.projectView.PresentationData;
import com.intellij.openapi.project.Project;
import com.intellij.ui.JBColor;
import de.marhali.easyi18n.service.SettingsService;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.util.TranslationsUtil;
import de.marhali.easyi18n.util.UiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.tree.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* I18n key tree preparation.
* @author marhali
*/
public class TreeModelTranslator extends DefaultTreeModel {
private final @NotNull Project project;
private final @NotNull Translations translations;
private final @Nullable String searchQuery;
public TreeModelTranslator(
@NotNull Project project, @NotNull Translations translations, @Nullable String searchQuery) {
super(null);
this.project = project;
this.translations = translations;
this.searchQuery = searchQuery;
setRoot(generateNodes());
}
private DefaultMutableTreeNode generateNodes() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode(LocalizedNode.ROOT_KEY);
if(translations.getNodes().isLeaf()) { // Empty tree
return root;
}
List<String> searchSections = searchQuery == null ?
Collections.emptyList() : TranslationsUtil.getSections(searchQuery);
for(LocalizedNode children : translations.getNodes().getChildren()) {
generateSubNodes(root, children, new ArrayList<>(searchSections));
}
return root;
}
private void generateSubNodes(DefaultMutableTreeNode parent,
LocalizedNode localizedNode, List<String> searchSections) {
String searchKey = searchSections.isEmpty() ? null : searchSections.remove(0);
if(searchKey != null && !localizedNode.getKey().startsWith(searchKey)) { // Filter node
return;
}
if(localizedNode.isLeaf()) {
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String title = localizedNode.getKey();
String sub = "(" + previewLocale + ": " + localizedNode.getValue().get(previewLocale) + ")";
String tooltip = UiUtil.generateHtmlTooltip(localizedNode.getValue());
PresentationData data = new PresentationData(title, sub, null, null);
data.setTooltip(tooltip);
if(localizedNode.getValue().size() != translations.getLocales().size()) {
data.setForcedTextForeground(JBColor.RED);
}
parent.add(new DefaultMutableTreeNode(data));
} else {
DefaultMutableTreeNode sub = new DefaultMutableTreeNode(localizedNode.getKey());
parent.add(sub);
for(LocalizedNode children : localizedNode.getChildren()) {
generateSubNodes(sub, children, new ArrayList<>(searchSections));
}
}
}
public TreePath findTreePath(@NotNull String fullPath) {
List<String> sections = TranslationsUtil.getSections(fullPath);
Object[] nodes = new Object[sections.size() + 1];
int pos = 0;
TreeNode currentNode = (TreeNode) this.getRoot();
nodes[pos] = currentNode;
for(String section : sections) {
pos++;
currentNode = findNode(currentNode, section);
nodes[pos] = currentNode;
}
return new TreePath(nodes);
}
public @Nullable DefaultMutableTreeNode findNode(@NotNull TreeNode parent, @NotNull String key) {
for(int i = 0; i < parent.getChildCount(); i++) {
TreeNode child = parent.getChildAt(i);
if(child instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode mutableChild = (DefaultMutableTreeNode) child;
String childKey = mutableChild.getUserObject().toString();
if(mutableChild.getUserObject() instanceof PresentationData) {
childKey = ((PresentationData) mutableChild.getUserObject()).getPresentableText();
}
if(childKey != null && childKey.equals(key)) {
return mutableChild;
}
}
}
throw new NullPointerException("Cannot find node by key: " + key);
}
}

View File

@ -1,173 +0,0 @@
package de.marhali.easyi18n.service;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.model.DataSynchronizer;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.TranslationDelete;
import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.util.IOUtil;
import de.marhali.easyi18n.util.TranslationsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.function.Consumer;
/**
* Factory service to manage localized messages for multiple projects at once.
* @author marhali
*/
public class DataStore {
private static final Map<Project, DataStore> INSTANCES = new WeakHashMap<>();
private final Project project;
private final List<DataSynchronizer> synchronizer;
private Translations translations;
private String searchQuery;
public static DataStore getInstance(@NotNull Project project) {
DataStore store = INSTANCES.get(project);
if(store == null) {
store = new DataStore(project);
INSTANCES.put(project, store);
}
return store;
}
private DataStore(@NotNull Project project) {
this.project = project;
this.synchronizer = new ArrayList<>();
this.translations = Translations.empty();
// Load data after first initialization
ApplicationManager.getApplication().invokeLater(this::reloadFromDisk, ModalityState.NON_MODAL);
}
/**
* Registers a new synchronizer which will receive {@link #translations} updates.
* @param synchronizer Synchronizer. See {@link DataSynchronizer}
*/
public void addSynchronizer(DataSynchronizer synchronizer) {
this.synchronizer.add(synchronizer);
}
/**
* Loads all translations from disk and overrides current {@link #translations} state.
*/
public void reloadFromDisk() {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) {
// Propagate empty state
this.translations = Translations.empty();
synchronize(searchQuery, null);
} else {
TranslatorIO io = IOUtil.determineFormat(project, localesPath);
io.read(project, localesPath, (loadedTranslations) -> {
this.translations = loadedTranslations == null ? Translations.empty() : loadedTranslations;
synchronize(searchQuery, null);
});
}
}
/**
* Saves the current translation state to disk. See {@link TranslatorIO#save(Project, Translations, String, Consumer)}
* @param callback Complete callback. Indicates if operation was successful(true) or not
*/
public void saveToDisk(@NotNull Consumer<Boolean> callback) {
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) { // Cannot save without valid path
return;
}
TranslatorIO io = IOUtil.determineFormat(project, localesPath);
io.save(project, translations, localesPath, callback);
}
/**
* Propagates provided search string to all synchronizer to display only relevant keys
* @param fullPath Full i18n key (e.g. user.username.title). Can be null to display all keys
*/
public void searchBeyKey(@Nullable String fullPath) {
// Use synchronizer to propagate search instance to all views
synchronize(this.searchQuery = fullPath, null);
}
/**
* Processes the provided update. Updates translation instance and propagates changes. See {@link DataSynchronizer}
* @param update The update to process. For more information see {@link TranslationUpdate}
*/
public void processUpdate(TranslationUpdate update) {
if(update.isDeletion() || update.isKeyChange()) { // Delete origin i18n key
String originKey = update.getOrigin().getKey();
List<String> sections = TranslationsUtil.getSections(originKey);
String nodeKey = sections.remove(sections.size() - 1); // Remove last node, which needs to be removed by parent
LocalizedNode node = translations.getNodes();
for(String section : sections) {
if(node == null) { // Might be possible on multi-delete
break;
}
node = node.getChildren(section);
}
if(node != null) { // Only remove if parent exists. Might be already deleted on multi-delete
node.removeChildren(nodeKey);
// Parent is empty now, we need to remove it as well (except root)
if(node.getChildren().isEmpty() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
processUpdate(new TranslationDelete(new KeyedTranslation(
TranslationsUtil.sectionsToFullPath(sections), null)));
}
}
}
String scrollTo = update.isDeletion() ? null : update.getChange().getKey();
if(!update.isDeletion()) { // Recreate with changed val / create
LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey());
node.setValue(update.getChange().getTranslations());
}
// Persist changes and propagate them on success
saveToDisk(success -> {
if(success) {
synchronize(searchQuery, scrollTo);
}
});
}
/**
* @return Current translation state
*/
public @NotNull Translations getTranslations() {
return translations;
}
/**
* Synchronizes current translation's state to all connected subscribers.
* @param searchQuery Optional search by full key filter (ui view)
* @param scrollTo Optional scroll to full key (ui view)
*/
public void synchronize(@Nullable String searchQuery, @Nullable String scrollTo) {
synchronizer.forEach(subscriber -> subscriber.synchronize(this.translations, searchQuery, scrollTo));
}
}

View File

@ -0,0 +1,67 @@
package de.marhali.easyi18n.service;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.AsyncFileListener;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
/**
* Listens for file changes inside configured @localesPath. See {@link AsyncFileListener}.
* Will trigger the reload function of the i18n instance if a relevant file was changed.
* @author marhali
*/
public class FileChangeListener implements AsyncFileListener {
private static final Logger logger = Logger.getInstance(FileChangeListener.class);
private final @NotNull Project project;
private @Nullable String localesPath;
public FileChangeListener(@NotNull Project project) {
this.project = project;
this.localesPath = null; // Wait for any update before listening to file changes
}
public void updateLocalesPath(@Nullable String localesPath) {
if(localesPath != null && !localesPath.isEmpty()) {
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(file != null && file.isDirectory()) {
this.localesPath = file.getPath();
return;
}
}
this.localesPath = null;
}
@Override
public ChangeApplier prepareChange(@NotNull List<? extends @NotNull VFileEvent> events) {
return new ChangeApplier() {
@Override
public void afterVfsChange() {
if(localesPath != null) {
events.forEach((e) -> {
if(e.getPath().contains(localesPath)) { // Perform reload
logger.debug("Detected file change. Reloading instance...");
InstanceManager manager = InstanceManager.get(project);
manager.store().loadFromPersistenceLayer((success) -> {
manager.bus().propagate().onUpdateData(manager.store().getData());
});
}
});
}
}
};
}
}

View File

@ -1,4 +1,4 @@
package de.marhali.easyi18n; package de.marhali.easyi18n.service;
import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
@ -7,8 +7,7 @@ import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content; import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory; import com.intellij.ui.content.ContentFactory;
import de.marhali.easyi18n.service.DataStore; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.action.*; import de.marhali.easyi18n.action.*;
import de.marhali.easyi18n.tabs.TableView; import de.marhali.easyi18n.tabs.TableView;
import de.marhali.easyi18n.tabs.TreeView; import de.marhali.easyi18n.tabs.TreeView;
@ -48,16 +47,16 @@ public class TranslatorToolWindowFactory implements ToolWindowFactory {
actions.add(new AddAction()); actions.add(new AddAction());
actions.add(new ReloadAction()); actions.add(new ReloadAction());
actions.add(new SettingsAction()); actions.add(new SettingsAction());
actions.add(new SearchAction((searchString) -> DataStore.getInstance(project).searchBeyKey(searchString))); actions.add(new SearchAction((query) -> InstanceManager.get(project).bus().propagate().onSearchQuery(query)));
toolWindow.setTitleActions(actions); toolWindow.setTitleActions(actions);
// Initialize Window Manager // Initialize Window Manager
WindowManager.getInstance().initialize(toolWindow, treeView, tableView); WindowManager.getInstance().initialize(toolWindow, treeView, tableView);
// Synchronize ui with underlying data // Synchronize ui with underlying data
DataStore store = DataStore.getInstance(project); InstanceManager manager = InstanceManager.get(project);
store.addSynchronizer(treeView); manager.bus().addListener(treeView);
store.addSynchronizer(tableView); manager.bus().addListener(tableView);
store.synchronize(null, null); manager.bus().propagate().onUpdateData(manager.store().getData());
} }
} }

View File

@ -5,6 +5,10 @@ import com.intellij.openapi.wm.ToolWindow;
import de.marhali.easyi18n.tabs.TableView; import de.marhali.easyi18n.tabs.TableView;
import de.marhali.easyi18n.tabs.TreeView; import de.marhali.easyi18n.tabs.TreeView;
/**
* Provides access to the plugin's own tool-window.
* @author marhali
*/
public class WindowManager { public class WindowManager {
private static WindowManager INSTANCE; private static WindowManager INSTANCE;

View File

@ -4,17 +4,14 @@ import com.intellij.openapi.project.Project;
import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable; import com.intellij.ui.table.JBTable;
import de.marhali.easyi18n.service.DataStore; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.LocalizedNode; import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.model.DataSynchronizer;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.TranslationDelete;
import de.marhali.easyi18n.model.table.TableModelTranslator;
import de.marhali.easyi18n.dialog.EditDialog; import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.DeleteKeyListener; import de.marhali.easyi18n.listener.DeleteKeyListener;
import de.marhali.easyi18n.listener.PopupClickListener; import de.marhali.easyi18n.listener.PopupClickListener;
import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.renderer.TableRenderer; import de.marhali.easyi18n.renderer.TableRenderer;
import de.marhali.easyi18n.tabs.mapper.TableModelMapper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -28,10 +25,12 @@ import java.util.ResourceBundle;
* Shows translation state as table. * Shows translation state as table.
* @author marhali * @author marhali
*/ */
public class TableView implements DataSynchronizer { public class TableView implements BusListener {
private final Project project; private final Project project;
private TableModelMapper currentMapper;
private JPanel rootPanel; private JPanel rootPanel;
private JPanel containerPanel; private JPanel containerPanel;
@ -54,10 +53,10 @@ public class TableView implements DataSynchronizer {
if(row >= 0) { if(row >= 0) {
String fullPath = String.valueOf(table.getValueAt(row, 0)); String fullPath = String.valueOf(table.getValueAt(row, 0));
LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(fullPath); Translation translation = InstanceManager.get(project).store().getData().getTranslation(fullPath);
if(node != null) { if(translation != null) {
new EditDialog(project, new KeyedTranslation(fullPath, node.getValue())).showAndHandle(); new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
} }
} }
} }
@ -67,32 +66,40 @@ public class TableView implements DataSynchronizer {
for (int selectedRow : table.getSelectedRows()) { for (int selectedRow : table.getSelectedRows()) {
String fullPath = String.valueOf(table.getValueAt(selectedRow, 0)); String fullPath = String.valueOf(table.getValueAt(selectedRow, 0));
DataStore.getInstance(project).processUpdate( InstanceManager.get(project).processUpdate(
new TranslationDelete(new KeyedTranslation(fullPath, null))); new TranslationDelete(new KeyedTranslation(fullPath, null))
);
} }
}; };
} }
@Override @Override
public void synchronize(@NotNull Translations translations, public void onUpdateData(@NotNull TranslationData data) {
@Nullable String searchQuery, @Nullable String scrollTo) { table.setModel(this.currentMapper = new TableModelMapper(data, update ->
InstanceManager.get(project).processUpdate(update)));
}
table.setModel(new TableModelTranslator(translations, searchQuery, update -> @Override
DataStore.getInstance(project).processUpdate(update))); public void onFocusKey(@Nullable String key) {
int row = -1;
if(scrollTo != null) { for (int i = 0; i < table.getRowCount(); i++) {
int row = -1; if (String.valueOf(table.getValueAt(i, 0)).equals(key)) {
row = i;
for (int i = 0; i < table.getRowCount(); i++) {
if (String.valueOf(table.getValueAt(i, 0)).equals(scrollTo)) {
row = i;
}
} }
}
if (row > -1) { // Matched @scrollTo if (row > -1) { // Matched @key
table.scrollRectToVisible( table.scrollRectToVisible(
new Rectangle(0, (row * table.getRowHeight()) + table.getHeight(), 0, 0)); new Rectangle(0, (row * table.getRowHeight()) + table.getHeight(), 0, 0));
} }
}
@Override
public void onSearchQuery(@Nullable String query) {
if(this.currentMapper != null) {
this.currentMapper.onSearchQuery(query);
this.table.updateUI();
} }
} }

View File

@ -8,19 +8,20 @@ import com.intellij.openapi.project.Project;
import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree; import com.intellij.ui.treeStructure.Tree;
import de.marhali.easyi18n.service.DataStore; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.DataSynchronizer;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.model.KeyedTranslation; import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationDelete; import de.marhali.easyi18n.model.TranslationDelete;
import de.marhali.easyi18n.model.tree.TreeModelTranslator; import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.action.treeview.CollapseTreeViewAction; import de.marhali.easyi18n.action.treeview.CollapseTreeViewAction;
import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction; import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction;
import de.marhali.easyi18n.dialog.EditDialog; import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.DeleteKeyListener; import de.marhali.easyi18n.listener.DeleteKeyListener;
import de.marhali.easyi18n.listener.PopupClickListener; import de.marhali.easyi18n.listener.PopupClickListener;
import de.marhali.easyi18n.renderer.TreeRenderer; import de.marhali.easyi18n.renderer.TreeRenderer;
import de.marhali.easyi18n.service.SettingsService;
import de.marhali.easyi18n.tabs.mapper.TreeModelMapper;
import de.marhali.easyi18n.util.TreeUtil; import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -36,10 +37,12 @@ import java.util.ResourceBundle;
* Show translation state as tree. * Show translation state as tree.
* @author marhali * @author marhali
*/ */
public class TreeView implements DataSynchronizer { public class TreeView implements BusListener {
private final Project project; private final Project project;
private TreeModelMapper currentMapper;
private JPanel rootPanel; private JPanel rootPanel;
private JPanel toolBarPanel; private JPanel toolBarPanel;
private JPanel containerPanel; private JPanel containerPanel;
@ -77,18 +80,28 @@ public class TreeView implements DataSynchronizer {
} }
@Override @Override
public void synchronize(@NotNull Translations translations, public void onUpdateData(@NotNull TranslationData data) {
@Nullable String searchQuery, @Nullable String scrollTo) { tree.setModel(this.currentMapper = new TreeModelMapper(data, SettingsService.getInstance(project).getState()));
}
TreeModelTranslator model = new TreeModelTranslator(project, translations, searchQuery); @Override
tree.setModel(model); public void onFocusKey(@Nullable String key) {
if(key != null && currentMapper != null) {
TreePath path = currentMapper.findTreePath(key);
this.tree.scrollPathToVisible(path);
if(searchQuery != null && !searchQuery.isEmpty()) { if(this.tree.isCollapsed(path)) {
expandAll().run(); this.tree.expandPath(path);
}
} }
}
if(scrollTo != null) { @Override
tree.scrollPathToVisible(model.findTreePath(scrollTo)); public void onSearchQuery(@Nullable String query) {
if(this.currentMapper != null) {
this.currentMapper.onSearchQuery(query);
this.expandAll().run();
this.tree.updateUI();
} }
} }
@ -100,10 +113,10 @@ public class TreeView implements DataSynchronizer {
if(node.getUserObject() instanceof PresentationData) { if(node.getUserObject() instanceof PresentationData) {
String fullPath = TreeUtil.getFullPath(path); String fullPath = TreeUtil.getFullPath(path);
LocalizedNode localizedNode = DataStore.getInstance(project).getTranslations().getNode(fullPath); Translation translation = InstanceManager.get(project).store().getData().getTranslation(fullPath);
if(localizedNode != null) { if(translation != null) {
new EditDialog(project,new KeyedTranslation(fullPath, localizedNode.getValue())).showAndHandle(); new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
} }
} }
} }
@ -120,8 +133,9 @@ public class TreeView implements DataSynchronizer {
for (TreePath path : tree.getSelectionPaths()) { for (TreePath path : tree.getSelectionPaths()) {
String fullPath = TreeUtil.getFullPath(path); String fullPath = TreeUtil.getFullPath(path);
DataStore.getInstance(project).processUpdate( InstanceManager.get(project).processUpdate(
new TranslationDelete(new KeyedTranslation(fullPath, null))); new TranslationDelete(new KeyedTranslation(fullPath, null))
);
} }
}; };
} }

View File

@ -0,0 +1,135 @@
package de.marhali.easyi18n.tabs.mapper;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.model.bus.SearchQueryListener;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Mapping {@link TranslationData} to {@link TableModel}.
* @author marhali
*/
public class TableModelMapper implements TableModel, SearchQueryListener {
private final @NotNull TranslationData data;
private final @NotNull List<String> locales;
private @NotNull List<String> fullKeys;
private final @NotNull Consumer<TranslationUpdate> updater;
public TableModelMapper(@NotNull TranslationData data, @NotNull Consumer<TranslationUpdate> updater) {
this.data = data;
this.locales = new ArrayList<>(data.getLocales());
this.fullKeys = new ArrayList<>(data.getFullKeys());
this.updater = updater;
}
@Override
public void onSearchQuery(@Nullable String query) {
if(query == null) { // Reset
this.fullKeys = new ArrayList<>(this.data.getFullKeys());
return;
}
query = query.toLowerCase();
List<String> matches = new ArrayList<>();
for(String key : this.data.getFullKeys()) {
if(key.toLowerCase().contains(query)) {
matches.add(key);
} else {
for(String content : this.data.getTranslation(key).values()) {
if(content.toLowerCase().contains(query)) {
matches.add(key);
}
}
}
}
this.fullKeys = matches;
}
@Override
public int getRowCount() {
return this.fullKeys.size();
}
@Override
public int getColumnCount() {
return this.locales.size() + 1; // Number of locales + 1 (key column)
}
@Nls
@Override
public String getColumnName(int columnIndex) {
if(columnIndex == 0) {
return "<html><b>Key</b></html>";
}
return "<html><b>" + this.locales.get(columnIndex - 1) + "</b></html>";
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return rowIndex > 0; // Everything should be editable except the headline
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
if(columnIndex == 0) { // Keys
return this.fullKeys.get(rowIndex);
}
String key = this.fullKeys.get(rowIndex);
String locale = this.locales.get(columnIndex - 1);
Translation translation = this.data.getTranslation(key);
return translation == null ? null : translation.get(locale);
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
String key = String.valueOf(this.getValueAt(rowIndex, 0));
Translation translation = this.data.getTranslation(key);
if(translation == null) { // Unknown cell
return;
}
String newKey = columnIndex == 0 ? String.valueOf(aValue) : key;
// Translation content update
if(columnIndex > 0) {
if(aValue == null || ((String) aValue).isEmpty()) {
translation.remove(this.locales.get(columnIndex - 1));
} else {
translation.put(this.locales.get(columnIndex - 1), String.valueOf(aValue));
}
}
TranslationUpdate update = new TranslationUpdate(new KeyedTranslation(key, translation),
new KeyedTranslation(newKey, translation));
this.updater.accept(update);
}
@Override
public void addTableModelListener(TableModelListener l) {}
@Override
public void removeTableModelListener(TableModelListener l) {}
}

View File

@ -0,0 +1,143 @@
package de.marhali.easyi18n.tabs.mapper;
import com.intellij.ide.projectView.PresentationData;
import com.intellij.ui.JBColor;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.bus.SearchQueryListener;
import de.marhali.easyi18n.util.PathUtil;
import de.marhali.easyi18n.util.UiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.tree.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Mapping {@link TranslationData} to {@link TreeModel}.
* @author marhali
*/
public class TreeModelMapper extends DefaultTreeModel implements SearchQueryListener {
private final TranslationData data;
private final SettingsState state;
public TreeModelMapper(TranslationData data, SettingsState state) {
super(null);
this.data = data;
this.state = state;
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
this.generateNodes(rootNode, this.data.getRootNode());
super.setRoot(rootNode);
}
@Override
public void onSearchQuery(@Nullable String query) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
TranslationData shadow = new TranslationData(this.state.isSortKeys(), this.state.isNestedKeys());
if(query == null) {
this.generateNodes(rootNode, this.data.getRootNode());
super.setRoot(rootNode);
return;
}
query = query.toLowerCase();
for(String currentKey : this.data.getFullKeys()) {
Translation translation = this.data.getTranslation(currentKey);
String loweredKey = currentKey.toLowerCase();
if(query.contains(loweredKey) || loweredKey.contains(query)) {
shadow.setTranslation(currentKey, translation);
continue;
}
for(String currentContent : translation.values()) {
if(currentContent.toLowerCase().contains(query)) {
shadow.setTranslation(currentKey, translation);
break;
}
}
}
this.generateNodes(rootNode, shadow.getRootNode());
super.setRoot(rootNode);
}
private void generateNodes(@NotNull DefaultMutableTreeNode parent, @NotNull TranslationNode translationNode) {
for(Map.Entry<String, TranslationNode> entry : translationNode.getChildren().entrySet()) {
String key = entry.getKey();
TranslationNode childTranslationNode = entry.getValue();
if(!childTranslationNode.isLeaf()) {
// Nested node - run recursively
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(key);
this.generateNodes(childNode, childTranslationNode);
parent.add(childNode);
} else {
String previewLocale = this.state.getPreviewLocale();
String sub = "(" + previewLocale + ": " + childTranslationNode.getValue().get(previewLocale) + ")";
String tooltip = UiUtil.generateHtmlTooltip(childTranslationNode.getValue());
PresentationData data = new PresentationData(key, sub, null, null);
data.setTooltip(tooltip);
if(childTranslationNode.getValue().size() != this.data.getLocales().size()) {
data.setForcedTextForeground(JBColor.RED);
}
parent.add(new DefaultMutableTreeNode(data));
}
}
}
public @NotNull TreePath findTreePath(@NotNull String fullPath) {
List<String> sections = new PathUtil(this.state.isNestedKeys()).split(fullPath);
List<Object> nodes = new ArrayList<>();
TreeNode currentNode = (TreeNode) this.getRoot();
nodes.add(currentNode);
for(String section : sections) {
currentNode = this.findNode(currentNode, section);
if(currentNode == null) {
break;
}
nodes.add(currentNode);
}
return new TreePath(nodes.toArray());
}
public @Nullable DefaultMutableTreeNode findNode(@NotNull TreeNode parent, @NotNull String key) {
for(int i = 0; i < parent.getChildCount(); i++) {
TreeNode child = parent.getChildAt(i);
if(child instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode mutableChild = (DefaultMutableTreeNode) child;
String childKey = mutableChild.getUserObject().toString();
if(mutableChild.getUserObject() instanceof PresentationData) {
childKey = ((PresentationData) mutableChild.getUserObject()).getPresentableText();
}
if(childKey != null && childKey.equals(key)) {
return mutableChild;
}
}
}
return null;
}
}

View File

@ -1,71 +0,0 @@
package de.marhali.easyi18n.util;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.implementation.*;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
import java.io.File;
/**
* IO operations utility.
* @author marhali
*/
public class IOUtil {
/**
* Determines the {@link TranslatorIO} which should be used for the specified directoryPath
* @param project Current intellij project
* @param directoryPath The full path to the parent directory which holds the translation files
* @return IO handler to use for file operations
*/
public static TranslatorIO determineFormat(@NotNull Project project, @NotNull String directoryPath) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] children = directory.getChildren();
for(VirtualFile file : children) {
if(file.isDirectory()) { // Modularized locale files
// ATM we only support modularized JSON files
return new ModularizedJsonTranslatorIO();
}
if(!isFileRelevant(project, file)) {
continue;
}
switch(file.getFileType().getDefaultExtension().toLowerCase()) {
case "json":
return new JsonTranslatorIO();
case "properties":
return new PropertiesTranslatorIO();
case "yml":
return new YamlTranslatorIO();
default:
System.err.println("Unsupported i18n locale file format: "
+ file.getFileType().getDefaultExtension());
}
}
throw new IllegalStateException("Could not determine i18n format. At least one locale file must be defined");
}
/**
* Checks if the provided file matches the file pattern specified by configuration
* @param project Current intellij project
* @param file File to check
* @return True if relevant otherwise false
*/
public static boolean isFileRelevant(@NotNull Project project, @NotNull VirtualFile file) {
String pattern = SettingsService.getInstance(project).getState().getFilePattern();
return file.getName().matches(pattern);
}
}

View File

@ -1,97 +0,0 @@
package de.marhali.easyi18n.util;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.util.array.JsonArrayUtil;
import org.apache.commons.lang.StringEscapeUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Json tree utilities for writing and reading {@link LocalizedNode}'s
* @author marhali
*/
public class JsonUtil {
/**
* Creates a {@link JsonObject} based from an {@link LocalizedNode}
* @param locale Current locale
* @param parent Parent json. Can be an entire json document
* @param node The node instance
*/
public static void writeTree(String locale, JsonObject parent, LocalizedNode node) {
if(node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
if(node.getValue().get(locale) != null) {
if(JsonArrayUtil.isArray(node.getValue().get(locale))) {
parent.add(node.getKey(), JsonArrayUtil.write(node.getValue().get(locale)));
} else {
String value = StringEscapeUtils.unescapeJava(node.getValue().get(locale));
parent.add(node.getKey(), new JsonPrimitive(value));
}
}
} else {
for(LocalizedNode children : node.getChildren()) {
if(children.isLeaf()) {
writeTree(locale, parent, children);
} else {
JsonObject childrenJson = new JsonObject();
writeTree(locale, childrenJson, children);
if(childrenJson.size() > 0) {
parent.add(children.getKey(), childrenJson);
}
}
}
}
}
/**
* Reads a {@link JsonObject} and writes the tree into the provided {@link LocalizedNode}
* @param locale Current locale
* @param json Json to read
* @param data Node. Can be a root node
*/
public static void readTree(String locale, JsonObject json, LocalizedNode data) {
for(Map.Entry<String, JsonElement> entry : json.entrySet()) {
String key = entry.getKey();
try {
// Try to go one level deeper
JsonObject childObject = entry.getValue().getAsJsonObject();
LocalizedNode childrenNode = data.getChildren(key);
if(childrenNode == null) {
childrenNode = new LocalizedNode(key, new ArrayList<>());
data.addChildren(childrenNode);
}
readTree(locale, childObject, childrenNode);
} catch(IllegalStateException e) { // Reached end for this node
LocalizedNode leafNode = data.getChildren(key);
if(leafNode == null) {
leafNode = new LocalizedNode(key, new HashMap<>());
data.addChildren(leafNode);
}
Map<String, String> messages = leafNode.getValue();
String value = entry.getValue().isJsonArray()
? JsonArrayUtil.read(entry.getValue().getAsJsonArray())
: StringUtil.escapeControls(entry.getValue().getAsString(), true);
messages.put(locale, value);
leafNode.setValue(messages);
}
}
}
}

View File

@ -1,28 +0,0 @@
package de.marhali.easyi18n.util;
import de.marhali.easyi18n.model.LocalizedNode;
import java.util.List;
import java.util.TreeMap;
/**
* Map utilities.
* @author marhali
*/
public class MapUtil {
/**
* Converts the provided list into a tree map.
* @param list List of nodes
* @return TreeMap based on node key and node object
*/
public static TreeMap<String, LocalizedNode> convertToTreeMap(List<LocalizedNode> list) {
TreeMap<String, LocalizedNode> map = new TreeMap<>();
for(LocalizedNode item : list) {
map.put(item.getKey(), item);
}
return map;
}
}

View File

@ -0,0 +1,73 @@
package de.marhali.easyi18n.util;
import de.marhali.easyi18n.service.SettingsService;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Utility tool for split and merge translation key paths.
* Some i18n implementations require to NOT nest the translation keys.
* This util takes care of this and checks the configured setting for this case.
* @author marhali
*/
public class PathUtil {
public static final char DELIMITER = '.';
private final boolean nestKeys;
public PathUtil(boolean nestKeys) {
this.nestKeys = nestKeys;
}
public PathUtil(Project project) {
this.nestKeys = SettingsService.getInstance(project).getState().isNestedKeys();
}
public @NotNull List<String> split(@NotNull String path) {
// Does not contain any sections or key nesting is disabled
if(!path.contains(String.valueOf(DELIMITER)) || !nestKeys) {
return new ArrayList<>(Collections.singletonList(path));
}
return new ArrayList<>(Arrays.asList(path.split("\\" + DELIMITER)));
}
public @NotNull String concat(@NotNull List<String> sections) {
StringBuilder builder = new StringBuilder();
// For disabled key nesting this should be only one section
for(String section : sections) {
if(builder.length() > 0) {
builder.append(DELIMITER);
}
builder.append(section);
}
return builder.toString();
}
public @NotNull String append(@NotNull String parentPath, @NotNull String children) {
StringBuilder builder = new StringBuilder(parentPath);
if(builder.length() > 0) { // Only add delimiter between parent and child if parent is NOT empty
builder.append(DELIMITER);
}
return builder.append(children).toString();
}
@Override
public String toString() {
return "PathUtil{" +
"nestKeys=" + nestKeys +
'}';
}
}

View File

@ -1,31 +0,0 @@
package de.marhali.easyi18n.util;
import java.util.*;
/**
* Applies sorting to {@link Properties} files.
* @author marhali
*/
public class SortedProperties extends Properties {
@Override
public Set<Object> keySet() {
return Collections.unmodifiableSet(new TreeSet<>(super.keySet()));
}
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
TreeMap<Object, Object> sorted = new TreeMap<>();
for(Object key : super.keySet()) {
sorted.put(key, get(key));
}
return sorted.entrySet();
}
@Override
public synchronized Enumeration<Object> keys() {
return Collections.enumeration(new TreeSet<>(super.keySet()));
}
}

View File

@ -1,47 +0,0 @@
package de.marhali.easyi18n.util;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Utility tool to support the translations instance
* @author marhali
*/
public class TranslationsUtil {
/**
* Retrieve all sections for the specified path (mostly fullPath)
* @param path The path
* @return Sections. E.g. input user.username.title -> Output: [user, username, title]
*/
public static @NotNull List<String> getSections(@NotNull String path) {
if(!path.contains(".")) {
return new ArrayList<>(Collections.singletonList(path));
}
return new ArrayList<>(Arrays.asList(path.split("\\.")));
}
/**
* Concatenate the given sections to a single string.
* @param sections The sections
* @return Full path. E.g. input [user, username, title] -> Output: user.username.title
*/
public static @NotNull String sectionsToFullPath(@NotNull List<String> sections) {
StringBuilder builder = new StringBuilder();
for (String section : sections) {
if(builder.length() > 0) {
builder.append(".");
}
builder.append(section);
}
return builder.toString();
}
}

View File

@ -1,7 +1,6 @@
package de.marhali.easyi18n.util; package de.marhali.easyi18n.util;
import com.intellij.ide.projectView.PresentationData; import com.intellij.ide.projectView.PresentationData;
import de.marhali.easyi18n.model.LocalizedNode;
import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
@ -20,19 +19,18 @@ public class TreeUtil {
public static String getFullPath(TreePath path) { public static String getFullPath(TreePath path) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for (Object obj : path.getPath()) { for (Object obj : path.getPath()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj; DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj;
Object value = node.getUserObject(); Object value = node.getUserObject();
String section = value instanceof PresentationData ? String section = value instanceof PresentationData ?
((PresentationData) value).getPresentableText() : String.valueOf(value); ((PresentationData) value).getPresentableText() : String.valueOf(value);
if(section == null || section.equals(LocalizedNode.ROOT_KEY)) { // Skip root node if(value == null) { // Skip empty sections
continue; continue;
} }
if(builder.length() != 0) { if(builder.length() != 0) {
builder.append("."); builder.append(PathUtil.DELIMITER);
} }
builder.append(section); builder.append(section);

View File

@ -1,3 +1,4 @@
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin url="https://github.com/marhali/easy-i18n"> <idea-plugin url="https://github.com/marhali/easy-i18n">
<id>de.marhali.easyi18n</id> <id>de.marhali.easyi18n</id>
<name>Easy I18n</name> <name>Easy I18n</name>
@ -10,7 +11,7 @@
<depends optional="true" config-file="de.marhali.easyi18n-kotlin.xml">org.jetbrains.kotlin</depends> <depends optional="true" config-file="de.marhali.easyi18n-kotlin.xml">org.jetbrains.kotlin</depends>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<toolWindow id="Easy I18n" anchor="bottom" factoryClass="de.marhali.easyi18n.TranslatorToolWindowFactory" /> <toolWindow id="Easy I18n" anchor="bottom" factoryClass="de.marhali.easyi18n.service.TranslatorToolWindowFactory" />
<projectService serviceImplementation="de.marhali.easyi18n.service.SettingsService" /> <projectService serviceImplementation="de.marhali.easyi18n.service.SettingsService" />
<completion.contributor language="any" <completion.contributor language="any"

View File

@ -7,7 +7,7 @@ action.add=Add Translation
action.edit=Edit Translation action.edit=Edit Translation
action.reload=Reload From Disk action.reload=Reload From Disk
action.settings=Settings action.settings=Settings
action.search=Search Key... action.search=Search...
action.delete=Delete action.delete=Delete
translation.key=Key translation.key=Key
translation.locales=Locales translation.locales=Locales
@ -16,4 +16,6 @@ settings.path.text=Locales directory
settings.path.file-pattern=Translation file pattern settings.path.file-pattern=Translation file pattern
settings.path.prefix=Path prefix settings.path.prefix=Path prefix
settings.preview=Preview locale settings.preview=Preview locale
settings.keys.sort=Sort translation keys alphabetically
settings.keys.nested=Nest translation keys if possible
settings.editor.assistance=I18n key completion, annotation and reference inside editor settings.editor.assistance=I18n key completion, annotation and reference inside editor

View File

@ -0,0 +1,270 @@
package de.marhali.easyi18n;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import org.junit.Assert;
import org.junit.Test;
import java.util.*;
/**
* Unit tests for {@link TranslationData} in combination with {@link TranslationNode}
* @author marhali
*/
public class TranslationDataTest {
private final int numOfTranslations = 18;
private void addTranslations(TranslationData data) {
data.setTranslation("zulu", new Translation("en", "test"));
data.setTranslation("gamma", new Translation("en", "test"));
data.setTranslation("foxtrot.super.long.key", new Translation("en", "test"));
data.setTranslation("bravo.b", new Translation("en", "test"));
data.setTranslation("bravo.c", new Translation("en", "test"));
data.setTranslation("bravo.a", new Translation("en", "test"));
data.setTranslation("bravo.d", new Translation("en", "test"));
data.setTranslation("bravo.long.bravo", new Translation("en", "test"));
data.setTranslation("bravo.long.charlie.a", new Translation("en", "test"));
data.setTranslation("bravo.long.alpha", new Translation("en", "test"));
data.setTranslation("alpha.b", new Translation("en", "test"));
data.setTranslation("alpha.c", new Translation("en", "test"));
data.setTranslation("alpha.a", new Translation("en", "test"));
data.setTranslation("alpha.d", new Translation("en", "test"));
data.setTranslation("charlie.b", new Translation("en", "test"));
data.setTranslation("charlie.c", new Translation("en", "test"));
data.setTranslation("charlie.a", new Translation("en", "test"));
data.setTranslation("charlie.d", new Translation("en", "test"));
}
@Test
public void testKeySorting() {
TranslationData data = new TranslationData(true, true);
this.addTranslations(data);
Set<String> expectation = new LinkedHashSet<>(Arrays.asList(
"alpha.a", "alpha.b", "alpha.c", "alpha.d",
"bravo.a", "bravo.b", "bravo.c", "bravo.d",
"bravo.long.alpha", "bravo.long.bravo", "bravo.long.charlie.a",
"charlie.a", "charlie.b", "charlie.c", "charlie.d",
"foxtrot.super.long.key",
"gamma",
"zulu"
));
Assert.assertEquals(data.getFullKeys(), expectation);
}
@Test
public void testKeyUnordered() {
TranslationData data = new TranslationData(false, true);
this.addTranslations(data);
Set<String> expectation = new LinkedHashSet<>(Arrays.asList(
"zulu",
"gamma",
"foxtrot.super.long.key",
"bravo.b", "bravo.c", "bravo.a", "bravo.d",
"bravo.long.bravo", "bravo.long.charlie.a", "bravo.long.alpha",
"alpha.b", "alpha.c", "alpha.a", "alpha.d",
"charlie.b", "charlie.c", "charlie.a", "charlie.d"
));
Assert.assertEquals(data.getFullKeys(), expectation);
}
@Test
public void testKeyNesting() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("nested.alpha", new Translation("en", "test"));
data.setTranslation("nested.bravo", new Translation("en", "test"));
data.setTranslation("other.alpha", new Translation("en", "test"));
data.setTranslation("other.bravo", new Translation("en", "test"));
Assert.assertEquals(data.getRootNode().getChildren().size(), 2);
for(TranslationNode node : data.getRootNode().getChildren().values()) {
Assert.assertFalse(node.isLeaf());
}
}
@Test
public void testKeyNonNested() {
TranslationData data = new TranslationData(true, false);
this.addTranslations(data);
Assert.assertEquals(data.getRootNode().getChildren().size(), this.numOfTranslations);
for(TranslationNode node : data.getRootNode().getChildren().values()) {
Assert.assertTrue(node.isLeaf());
}
}
@Test
public void testDeleteNested() {
TranslationData data = new TranslationData(true, true);
Translation value = new Translation("en", "test");
data.setTranslation("alpha", value);
data.setTranslation("nested.alpha", value);
data.setTranslation("nested.long.bravo", value);
Assert.assertEquals(data.getFullKeys().size(), 3);
data.setTranslation("alpha", null);
data.setTranslation("nested.alpha", null);
data.setTranslation("nested.long.bravo", null);
Assert.assertEquals(data.getFullKeys().size(), 0);
Assert.assertNull(data.getTranslation("alpha"));
Assert.assertNull(data.getTranslation("nested.alpha"));
Assert.assertNull(data.getTranslation("nested.long.bravo"));
}
@Test
public void testDeleteNonNested() {
TranslationData data = new TranslationData(true, false);
Translation value = new Translation("en", "test");
data.setTranslation("alpha", value);
data.setTranslation("nested.alpha", value);
data.setTranslation("nested.long.bravo", value);
Assert.assertEquals(data.getFullKeys().size(), 3);
data.setTranslation("alpha", null);
data.setTranslation("nested.alpha", null);
data.setTranslation("nested.long.bravo", null);
Assert.assertEquals(data.getFullKeys().size(), 0);
Assert.assertNull(data.getTranslation("alpha"));
Assert.assertNull(data.getTranslation("nested.alpha"));
Assert.assertNull(data.getTranslation("nested.long.bravo"));
}
@Test
public void testRecurseDeleteNonNested() {
TranslationData data = new TranslationData(true, false);
this.addTranslations(data);
data.setTranslation("foxtrot.super.long.key", null);
Assert.assertNull(data.getTranslation("foxtrot.super.long.key"));
Assert.assertNull(data.getRootNode().getChildren().get("foxtrot"));
}
@Test
public void testRecurseDeleteNested() {
TranslationData data = new TranslationData(true, true);
this.addTranslations(data);
data.setTranslation("foxtrot.super.long.key", null);
Assert.assertNull(data.getTranslation("foxtrot.super.long.key"));
Assert.assertNull(data.getRootNode().getChildren().get("foxtrot"));
}
@Test
public void testOverwriteNonNested() {
TranslationData data = new TranslationData(true, false);
Translation before = new Translation("en", "before");
Translation after = new Translation("en", "after");
data.setTranslation("alpha", before);
data.setTranslation("nested.alpha", before);
data.setTranslation("nested.long.bravo", before);
Assert.assertEquals(data.getTranslation("alpha"), before);
Assert.assertEquals(data.getTranslation("alpha"), before);
Assert.assertEquals(data.getTranslation("alpha"), before);
data.setTranslation("alpha", after);
data.setTranslation("nested.alpha", after);
data.setTranslation("nested.long.bravo", after);
Assert.assertEquals(data.getTranslation("alpha"), after);
Assert.assertEquals(data.getTranslation("alpha"), after);
Assert.assertEquals(data.getTranslation("alpha"), after);
}
@Test
public void testOverwriteNested() {
TranslationData data = new TranslationData(true, true);
Translation before = new Translation("en", "before");
Translation after = new Translation("en", "after");
data.setTranslation("alpha", before);
data.setTranslation("nested.alpha", before);
data.setTranslation("nested.long.bravo", before);
Assert.assertEquals(data.getTranslation("alpha"), before);
Assert.assertEquals(data.getTranslation("alpha"), before);
Assert.assertEquals(data.getTranslation("alpha"), before);
data.setTranslation("alpha", after);
data.setTranslation("nested.alpha", after);
data.setTranslation("nested.long.bravo", after);
Assert.assertEquals(data.getTranslation("alpha"), after);
Assert.assertEquals(data.getTranslation("alpha"), after);
Assert.assertEquals(data.getTranslation("alpha"), after);
}
@Test
public void testRecurseTransformNested() {
TranslationData data = new TranslationData(true, true);
Translation value = new Translation("en", "test");
data.setTranslation("alpha.nested.key", value);
data.setTranslation("alpha.other", value);
data.setTranslation("bravo", value);
Assert.assertEquals(data.getFullKeys().size(), 3);
data.setTranslation("alpha.nested", value);
data.setTranslation("alpha.other.new", value);
data.setTranslation("bravo", null);
Assert.assertEquals(data.getFullKeys().size(), 2);
Assert.assertNull(data.getTranslation("alpha.nested.key"));
Assert.assertNull(data.getTranslation("alpha.other"));
Assert.assertNull(data.getTranslation("bravo"));
Assert.assertEquals(data.getTranslation("alpha.nested"), value);
Assert.assertEquals(data.getTranslation("alpha.other.new"), value);
}
@Test
public void testRecurseTransformNonNested() {
TranslationData data = new TranslationData(true, false);
Translation value = new Translation("en", "test");
data.setTranslation("alpha.nested.key", value);
data.setTranslation("alpha.other", value);
data.setTranslation("bravo", value);
Assert.assertEquals(data.getFullKeys().size(), 3);
data.setTranslation("alpha.nested", value);
data.setTranslation("alpha.other.new", value);
data.setTranslation("bravo", null);
Assert.assertEquals(data.getFullKeys().size(), 4);
Assert.assertNull(data.getTranslation("bravo"));
Assert.assertEquals(data.getTranslation("alpha.nested.key"), value);
Assert.assertEquals(data.getTranslation("alpha.other"), value);
Assert.assertEquals(data.getTranslation("alpha.nested"), value);
Assert.assertEquals(data.getTranslation("alpha.other.new"), value);
}
}

View File

@ -0,0 +1,45 @@
package de.marhali.easyi18n.mapper;
import de.marhali.easyi18n.model.Translation;
import org.junit.Test;
/**
* Defines test cases for {@link de.marhali.easyi18n.model.TranslationNode} mapping.
* @author marhali
*/
public abstract class AbstractMapperTest {
protected final String specialCharacters = "Special characters: äü@Öä€/$§;.-?+~#```'' end";
protected final String arraySimple = "!arr[first;second]";
protected final String arrayEscaped = "!arr[first\\;element;second element;third\\;element]";
protected final String leadingSpace = " leading space";
@Test
public abstract void testNonSorting();
@Test
public abstract void testSorting();
@Test
public abstract void testArrays();
@Test
public abstract void testSpecialCharacters();
@Test
public abstract void testNestedKeys();
@Test
public abstract void testNonNestedKeys();
@Test
public abstract void testLeadingSpace();
@Test
public abstract void testNumbers();
protected Translation create(String content) {
return new Translation("en", content);
}
}

View File

@ -0,0 +1,159 @@
package de.marhali.easyi18n.mapper;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.io.json.JsonArrayMapper;
import de.marhali.easyi18n.io.json.JsonMapper;
import de.marhali.easyi18n.model.TranslationData;
import org.apache.commons.lang.StringEscapeUtils;
import org.junit.Assert;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Unit tests for {@link de.marhali.easyi18n.io.json.JsonMapper}
* @author marhali
*/
public class JsonMapperTest extends AbstractMapperTest {
@Override
public void testNonSorting() {
JsonObject input = new JsonObject();
input.add("zulu", new JsonPrimitive("test"));
input.add("alpha", new JsonPrimitive("test"));
input.add("bravo", new JsonPrimitive("test"));
TranslationData data = new TranslationData(false, true);
JsonMapper.read("en", input, data.getRootNode());
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("zulu", "alpha", "bravo"));
Assert.assertEquals(expect, output.keySet());
}
@Override
public void testSorting() {
JsonObject input = new JsonObject();
input.add("zulu", new JsonPrimitive("test"));
input.add("alpha", new JsonPrimitive("test"));
input.add("bravo", new JsonPrimitive("test"));
TranslationData data = new TranslationData(true, true);
JsonMapper.read("en", input, data.getRootNode());
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("alpha", "bravo", "zulu"));
Assert.assertEquals(expect, output.keySet());
}
@Override
public void testArrays() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("simple", create(arraySimple));
data.setTranslation("escaped", create(arrayEscaped));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.get("simple").isJsonArray());
Assert.assertEquals(arraySimple, JsonArrayMapper.read(output.get("simple").getAsJsonArray()));
Assert.assertTrue(output.get("escaped").isJsonArray());
Assert.assertEquals(arrayEscaped, StringEscapeUtils.unescapeJava(JsonArrayMapper.read(output.get("escaped").getAsJsonArray())));
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertTrue(JsonArrayMapper.isArray(input.getTranslation("simple").get("en")));
Assert.assertTrue(JsonArrayMapper.isArray(input.getTranslation("escaped").get("en")));
}
@Override
public void testSpecialCharacters() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("chars", create(specialCharacters));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(specialCharacters, output.get("chars").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals(specialCharacters, StringEscapeUtils.unescapeJava(input.getTranslation("chars").get("en")));
}
@Override
public void testNestedKeys() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("nested.key.section", create("test"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals("test", output.getAsJsonObject("nested").getAsJsonObject("key").get("section").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("nested.key.section").get("en"));
}
@Override
public void testNonNestedKeys() {
TranslationData data = new TranslationData(true, false);
data.setTranslation("long.key.with.many.sections", create("test"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.has("long.key.with.many.sections"));
TranslationData input = new TranslationData(true, false);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("long.key.with.many.sections").get("en"));
}
@Override
public void testLeadingSpace() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("space", create(leadingSpace));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(leadingSpace, output.get("space").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals(leadingSpace, input.getTranslation("space").get("en"));
}
@Override
public void testNumbers() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("numbered", create("15000"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(15000, output.get("numbered").getAsNumber());
JsonObject input = new JsonObject();
input.addProperty("numbered", 143.23);
JsonMapper.read("en", input, data.getRootNode());
Assert.assertEquals("143.23", data.getTranslation("numbered").get("en"));
}
}

View File

@ -0,0 +1,156 @@
package de.marhali.easyi18n.mapper;
import de.marhali.easyi18n.io.properties.PropertiesArrayMapper;
import de.marhali.easyi18n.io.properties.PropertiesMapper;
import de.marhali.easyi18n.io.properties.SortableProperties;
import de.marhali.easyi18n.model.TranslationData;
import org.apache.commons.lang.StringEscapeUtils;
import org.junit.Assert;
import java.util.*;
/**
* Unit tests for {@link de.marhali.easyi18n.io.properties.PropertiesMapper}
* @author marhali
*/
public class PropertiesMapperTest extends AbstractMapperTest {
@Override
public void testNonSorting() {
SortableProperties input = new SortableProperties(false);
input.setProperty("zulu", "test");
input.setProperty("alpha", "test");
input.setProperty("bravo", "test");
TranslationData data = new TranslationData(false, true);
PropertiesMapper.read("en", input, data);
SortableProperties output = new SortableProperties(false);
PropertiesMapper.write("en", output, data);
List<String> expect = Arrays.asList("zulu", "alpha", "bravo");
Assert.assertEquals(expect, new ArrayList<>(output.keySet()));
}
@Override
public void testSorting() {
SortableProperties input = new SortableProperties(true);
input.setProperty("zulu", "test");
input.setProperty("alpha", "test");
input.setProperty("bravo", "test");
TranslationData data = new TranslationData(true, true);
PropertiesMapper.read("en", input, data);
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
List<String> expect = Arrays.asList("alpha", "bravo", "zulu");
Assert.assertEquals(expect, new ArrayList<>(output.keySet()));
}
@Override
public void testArrays() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("simple", create(arraySimple));
data.setTranslation("escaped", create(arrayEscaped));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertTrue(output.get("simple") instanceof String[]);
Assert.assertEquals(arraySimple, PropertiesArrayMapper.read((String[]) output.get("simple")));
Assert.assertTrue(output.get("escaped") instanceof String[]);
Assert.assertEquals(arrayEscaped, StringEscapeUtils.unescapeJava(PropertiesArrayMapper.read((String[]) output.get("escaped"))));
TranslationData input = new TranslationData(true, true);
PropertiesMapper.read("en", output, input);
Assert.assertTrue(PropertiesArrayMapper.isArray(input.getTranslation("simple").get("en")));
Assert.assertTrue(PropertiesArrayMapper.isArray(input.getTranslation("escaped").get("en")));
}
@Override
public void testSpecialCharacters() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("chars", create(specialCharacters));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertEquals(specialCharacters, output.get("chars"));
TranslationData input = new TranslationData(true, true);
PropertiesMapper.read("en", output, input);
Assert.assertEquals(specialCharacters, StringEscapeUtils.unescapeJava(input.getTranslation("chars").get("en")));
}
@Override
public void testNestedKeys() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("nested.key.sections", create("test"));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertEquals("test", output.get("nested.key.sections"));
TranslationData input = new TranslationData(true, true);
PropertiesMapper.read("en", output, input);
Assert.assertTrue(input.getRootNode().getChildren().containsKey("nested"));
Assert.assertEquals("test", input.getTranslation("nested.key.sections").get("en"));
}
@Override
public void testNonNestedKeys() {
TranslationData data = new TranslationData(true, false);
data.setTranslation("long.key.with.many.sections", create("test"));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertNotNull(output.get("long.key.with.many.sections"));
TranslationData input = new TranslationData(true, false);
PropertiesMapper.read("en", output, input);
Assert.assertEquals("test", input.getRootNode().getChildren()
.get("long.key.with.many.sections").getValue().get("en"));
}
@Override
public void testLeadingSpace() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("space", create(leadingSpace));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertEquals(leadingSpace, output.get("space"));
TranslationData input = new TranslationData(true, true);
PropertiesMapper.read("en", output, input);
Assert.assertEquals(leadingSpace, input.getTranslation("space").get("en"));
}
@Override
public void testNumbers() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("numbered", create("15000"));
SortableProperties output = new SortableProperties(true);
PropertiesMapper.write("en", output, data);
Assert.assertEquals(15000, output.get("numbered"));
SortableProperties input = new SortableProperties(true);
input.put("numbered", 143.23);
PropertiesMapper.read("en", input, data);
Assert.assertEquals("143.23", data.getTranslation("numbered").get("en"));
}
}

View File

@ -0,0 +1,158 @@
package de.marhali.easyi18n.mapper;
import de.marhali.easyi18n.io.yaml.YamlArrayMapper;
import de.marhali.easyi18n.io.yaml.YamlMapper;
import de.marhali.easyi18n.model.TranslationData;
import org.apache.commons.lang.StringEscapeUtils;
import org.junit.Assert;
import thito.nodeflow.config.MapSection;
import thito.nodeflow.config.Section;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Unit tests for {@link de.marhali.easyi18n.io.yaml.YamlMapper}
* @author marhali
*/
public class YamlMapperTest extends AbstractMapperTest {
@Override
public void testNonSorting() {
Section input = new MapSection();
input.set("zulu", "test");
input.set("alpha", "test");
input.set("bravo", "test");
TranslationData data = new TranslationData(false, true);
YamlMapper.read("en", input, data.getRootNode());
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("zulu", "alpha", "bravo"));
Assert.assertEquals(expect, output.getKeys());
}
@Override
public void testSorting() {
Section input = new MapSection();
input.set("zulu", "test");
input.set("alpha", "test");
input.set("bravo", "test");
TranslationData data = new TranslationData(true, true);
YamlMapper.read("en", input, data.getRootNode());
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("alpha", "bravo", "zulu"));
Assert.assertEquals(expect, output.getKeys());
}
@Override
public void testArrays() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("simple", create(arraySimple));
data.setTranslation("escaped", create(arrayEscaped));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.isList("simple"));
Assert.assertEquals(arraySimple, YamlArrayMapper.read(output.getList("simple").get()));
Assert.assertTrue(output.isList("escaped"));
Assert.assertEquals(arrayEscaped, StringEscapeUtils.unescapeJava(YamlArrayMapper.read(output.getList("escaped").get())));
TranslationData input = new TranslationData(true, true);
YamlMapper.read("en", output, input.getRootNode());
Assert.assertTrue(YamlArrayMapper.isArray(input.getTranslation("simple").get("en")));
Assert.assertTrue(YamlArrayMapper.isArray(input.getTranslation("escaped").get("en")));
}
@Override
public void testSpecialCharacters() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("chars", create(specialCharacters));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertEquals(specialCharacters, output.getString("chars").get());
TranslationData input = new TranslationData(true, true);
YamlMapper.read("en", output, input.getRootNode());
Assert.assertEquals(specialCharacters, StringEscapeUtils.unescapeJava(input.getTranslation("chars").get("en")));
}
@Override
public void testNestedKeys() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("nested.key.section", create("test"));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertEquals("test", output.getString("nested.key.section").get());
TranslationData input = new TranslationData(true, true);
YamlMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("nested.key.section").get("en"));
}
@Override
public void testNonNestedKeys() {
TranslationData data = new TranslationData(true, false);
data.setTranslation("long.key.with.many.sections", create("test"));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.getKeys().contains("long.key.with.many.sections"));
TranslationData input = new TranslationData(true, false);
YamlMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("long.key.with.many.sections").get("en"));
}
@Override
public void testLeadingSpace() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("space", create(leadingSpace));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertEquals(leadingSpace, output.getString("space").get());
TranslationData input = new TranslationData(true, true);
YamlMapper.read("en", output, input.getRootNode());
Assert.assertEquals(leadingSpace, input.getTranslation("space").get("en"));
}
@Override
public void testNumbers() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("numbered", create("15000"));
Section output = new MapSection();
YamlMapper.write("en", output, data.getRootNode());
Assert.assertEquals(15000, output.getInteger("numbered").get().intValue());
Section input = new MapSection();
input.set("numbered", 143.23);
YamlMapper.read("en", input, data.getRootNode());
Assert.assertEquals("143.23", data.getTranslation("numbered").get("en"));
}
}