Merge pull request #63 from marhali/feat/i18n-next
Feat/i18n next (v1.6.0)
8
.github/dependabot.yml
vendored
@ -3,7 +3,15 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Gradle dependencies
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
target-branch: "next"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "next"
|
||||
schedule:
|
||||
interval: "daily"
|
265
.github/workflows/build.yml
vendored
@ -1,111 +1,53 @@
|
||||
# GitHub Actions Workflow created for testing and preparing the plugin release in following steps:
|
||||
# - validate Gradle Wrapper,
|
||||
# - run test and verifyPlugin tasks,
|
||||
# - run buildPlugin task and prepare artifact for the further tests,
|
||||
# - run IntelliJ Plugin Verifier,
|
||||
# - run 'test' and 'verifyPlugin' tasks,
|
||||
# - run Qodana inspections,
|
||||
# - run 'buildPlugin' task and prepare artifact for the further tests,
|
||||
# - run 'runPluginVerifier' task,
|
||||
# - create a draft release.
|
||||
#
|
||||
# Workflow is triggered on push and pull_request events.
|
||||
#
|
||||
# Docs:
|
||||
# - GitHub Actions: https://help.github.com/en/actions
|
||||
# - IntelliJ Plugin Verifier GitHub Action: https://github.com/ChrisCarini/intellij-platform-plugin-verifier-action
|
||||
# GitHub Actions reference: https://help.github.com/en/actions
|
||||
#
|
||||
## JBIJPPTPL
|
||||
|
||||
name: Build
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
# Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests)
|
||||
push:
|
||||
branches: [main]
|
||||
# Trigger the workflow on any pull request
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
# Run Gradle Wrapper Validation Action to verify the wrapper's checksum
|
||||
gradleValidation:
|
||||
name: Gradle Wrapper
|
||||
# Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks
|
||||
# Build plugin and provide the artifact for the next workflow jobs
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.properties.outputs.version }}
|
||||
changelog: ${{ steps.properties.outputs.changelog }}
|
||||
steps:
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
# Validate wrapper
|
||||
- name: Gradle Wrapper Validation
|
||||
uses: gradle/wrapper-validation-action@v1.0.3
|
||||
uses: gradle/wrapper-validation-action@v1.0.4
|
||||
|
||||
# Run verifyPlugin and test Gradle tasks
|
||||
test:
|
||||
name: Test
|
||||
needs: gradleValidation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Setup Java 1.8 environment for the next steps
|
||||
# Setup Java 11 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 11
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Cache Gradle dependencies
|
||||
- name: Setup Gradle Dependencies Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
|
||||
|
||||
# Cache Gradle Wrapper
|
||||
- name: Setup Gradle Wrapper Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
# Run detekt, ktlint and tests
|
||||
- name: Run Linters and Test
|
||||
run: ./gradlew check
|
||||
|
||||
# Run verifyPlugin Gradle task
|
||||
- name: Verify Plugin
|
||||
run: ./gradlew verifyPlugin
|
||||
|
||||
# Build plugin with buildPlugin Gradle task and provide the artifact for the next workflow jobs
|
||||
# Requires test job to be passed
|
||||
build:
|
||||
name: Build
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
name: ${{ steps.properties.outputs.name }}
|
||||
version: ${{ steps.properties.outputs.version }}
|
||||
changelog: ${{ steps.properties.outputs.changelog }}
|
||||
artifact: ${{ steps.properties.outputs.artifact }}
|
||||
steps:
|
||||
|
||||
# Setup Java 1.8 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Cache Gradle Dependencies
|
||||
- name: Setup Gradle Dependencies Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
|
||||
|
||||
# Cache Gradle Wrapper
|
||||
- name: Setup Gradle Wrapper Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
cache: gradle
|
||||
|
||||
# Set environment variables
|
||||
- name: Export Properties
|
||||
@ -119,128 +61,99 @@ jobs:
|
||||
CHANGELOG="${CHANGELOG//'%'/'%25'}"
|
||||
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
|
||||
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
|
||||
ARTIFACT="${NAME}-${VERSION}.zip"
|
||||
|
||||
echo "::set-output name=version::$VERSION"
|
||||
echo "::set-output name=name::$NAME"
|
||||
echo "::set-output name=changelog::$CHANGELOG"
|
||||
echo "::set-output name=artifact::$ARTIFACT"
|
||||
|
||||
# Build artifact using buildPlugin Gradle task
|
||||
- name: Build Plugin
|
||||
run: ./gradlew buildPlugin
|
||||
|
||||
# Upload plugin artifact to make it available in the next jobs
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: plugin-artifact
|
||||
path: ./build/distributions/${{ steps.properties.outputs.artifact }}
|
||||
|
||||
# Verify built plugin using IntelliJ Plugin Verifier tool
|
||||
# Requires build job to be passed
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Setup Java 1.8 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Cache Gradle Dependencies
|
||||
- name: Setup Gradle Dependencies Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
|
||||
|
||||
# Cache Gradle Wrapper
|
||||
- name: Setup Gradle Wrapper Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
# Set environment variables
|
||||
- name: Export Properties
|
||||
id: properties
|
||||
shell: bash
|
||||
run: |
|
||||
PROPERTIES="$(./gradlew properties --console=plain -q)"
|
||||
IDE_VERSIONS="$(echo "$PROPERTIES" | grep "^pluginVerifierIdeVersions:" | base64)"
|
||||
|
||||
echo "::set-output name=ideVersions::$IDE_VERSIONS"
|
||||
echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier"
|
||||
./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier
|
||||
# Run tests
|
||||
- name: Run Tests
|
||||
run: ./gradlew test
|
||||
|
||||
# Collect Tests Result of failed tests
|
||||
- name: Collect Tests Result
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: tests-result
|
||||
path: ${{ github.workspace }}/build/reports/tests
|
||||
|
||||
# Cache Plugin Verifier IDEs
|
||||
- name: Setup Plugin Verifier IDEs Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides
|
||||
key: ${{ runner.os }}-plugin-verifier-${{ steps.properties.outputs.ideVersions }}
|
||||
key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
|
||||
|
||||
# Run IntelliJ Plugin Verifier action using GitHub Action
|
||||
- name: Verify Plugin
|
||||
# Run Verify Plugin task and IntelliJ Plugin Verifier tool
|
||||
- name: Run Plugin Verification tasks
|
||||
run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }}
|
||||
|
||||
# Collect Plugin Verifier Result
|
||||
- name: Collect Plugin Verifier Result
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: pluginVerifier-result
|
||||
path: ${{ github.workspace }}/build/reports/pluginVerifier
|
||||
|
||||
# Run Qodana inspections
|
||||
- name: Qodana - Code Inspection
|
||||
uses: JetBrains/qodana-action@v2.1-eap
|
||||
|
||||
# Collect Qodana Result
|
||||
- name: Collect Qodana Result
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: qodana-result
|
||||
path: ${{ github.workspace }}/qodana
|
||||
|
||||
# Prepare plugin archive content for creating artifact
|
||||
- name: Prepare Plugin Artifact
|
||||
id: artifact
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${{ github.workspace }}/build/distributions
|
||||
FILENAME=`ls *.zip`
|
||||
unzip "$FILENAME" -d content
|
||||
echo "::set-output name=filename::$FILENAME"
|
||||
# Store already-built plugin as an artifact for downloading
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
with:
|
||||
name: ${{ steps.artifact.outputs.filename }}
|
||||
path: ./build/distributions/content/*/*
|
||||
|
||||
# Prepare a draft release for GitHub Releases page for the manual verification
|
||||
# If accepted and published, release workflow would be triggered
|
||||
releaseDraft:
|
||||
name: Release Draft
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: [build, verify]
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
# Remove old release drafts by using the curl request for the available releases with draft flag
|
||||
- name: Remove Old Release Drafts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases \
|
||||
| tr '\r\n' ' ' \
|
||||
| jq '.[] | select(.draft == true) | .id' \
|
||||
| xargs -I '{}' \
|
||||
curl -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases/{}
|
||||
|
||||
gh api repos/{owner}/{repo}/releases \
|
||||
--jq '.[] | select(.draft == true) | .id' \
|
||||
| xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{}
|
||||
# Create new release draft - which is not publicly visible and requires manual acceptance
|
||||
- name: Create Release Draft
|
||||
id: createDraft
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
release_name: v${{ needs.build.outputs.version }}
|
||||
body: ${{ needs.build.outputs.changelog }}
|
||||
draft: true
|
||||
|
||||
# Download plugin artifact provided by the previous job
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: plugin-artifact
|
||||
|
||||
# Upload artifact as a release asset
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createDraft.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.build.outputs.artifact }}
|
||||
asset_name: ${{ needs.build.outputs.artifact }}
|
||||
asset_content_type: application/zip
|
||||
run: |
|
||||
gh release create v${{ needs.build.outputs.version }} \
|
||||
--draft \
|
||||
--title "v${{ needs.build.outputs.version }}" \
|
||||
--notes "$(cat << 'EOM'
|
||||
${{ needs.build.outputs.changelog }}
|
||||
EOM
|
||||
)"
|
91
.github/workflows/release.yml
vendored
@ -14,57 +14,66 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Setup Java 1.8 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
# Setup Java 11 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 11
|
||||
cache: gradle
|
||||
|
||||
# Set environment variables
|
||||
- name: Export Properties
|
||||
id: properties
|
||||
shell: bash
|
||||
run: |
|
||||
CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
|
||||
${{ github.event.release.body }}
|
||||
EOM
|
||||
)"
|
||||
|
||||
echo "::set-output name=changelog::$CHANGELOG"
|
||||
# Update Unreleased section with the current release note
|
||||
- name: Patch Changelog
|
||||
if: ${{ steps.properties.outputs.changelog != '' }}
|
||||
run: |
|
||||
./gradlew patchChangelog --release-note "$(cat << 'EOM'
|
||||
${{ steps.properties.outputs.changelog }}
|
||||
EOM
|
||||
)"
|
||||
# Publish the plugin to the Marketplace
|
||||
- name: Publish Plugin
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
run: ./gradlew publishPlugin
|
||||
|
||||
# Patch changelog, commit and push to the current repository
|
||||
changelog:
|
||||
name: Update Changelog
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Upload artifact as a release asset
|
||||
- name: Upload Release Asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
|
||||
|
||||
# Setup Java 1.8 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check out current repository
|
||||
- name: Fetch Sources
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
# Update Unreleased section with the current version
|
||||
- name: Patch Changelog
|
||||
run: ./gradlew patchChangelog
|
||||
|
||||
# Commit patched Changelog
|
||||
- name: Commit files
|
||||
# Create pull request
|
||||
- name: Create Pull Request
|
||||
if: ${{ steps.properties.outputs.changelog != '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git commit -m "Update changelog" -a
|
||||
|
||||
# Push changes
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
branch: main
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
BRANCH="changelog-update-$VERSION"
|
||||
git config user.email "action@github.com"
|
||||
git config user.name "GitHub Action"
|
||||
git checkout -b $BRANCH
|
||||
git commit -am "Changelog update - $VERSION"
|
||||
git push --set-upstream origin $BRANCH
|
||||
gh pr create \
|
||||
--title "Changelog update - \`$VERSION\`" \
|
||||
--body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
|
||||
--base main \
|
||||
--head $BRANCH
|
60
.github/workflows/run-ui-tests.yml
vendored
Normal 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
@ -1,3 +1,4 @@
|
||||
.gradle
|
||||
.idea
|
||||
.qodana
|
||||
build
|
22
.run/Run IDE for UI Tests.run.xml
Normal 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>
|
@ -11,7 +11,7 @@
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="check" />
|
||||
<option value="test" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
|
26
.run/Run Qodana.run.xml
Normal 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>
|
14
CHANGELOG.md
@ -3,6 +3,20 @@
|
||||
# easy-i18n Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- The search function now supports full-text-search
|
||||
- Automatically reload translation data on file system change
|
||||
- Sorting of translation keys can now be disabled via configuration
|
||||
- Key section nesting can be disabled via configuration
|
||||
- Numbers will be stored as number type whenever possible
|
||||
- Code signing of plugin source
|
||||
|
||||
### Changed
|
||||
- Better focus keys in tree-view after edit
|
||||
- Optimized internal data structure (io, cache, events)
|
||||
- Adjusted compatibility matrix to 2020.3 - 2021.3
|
||||
- Updated dependencies and improved README file
|
||||
|
||||
## [1.5.1]
|
||||
### Fixed
|
||||
- Exception on key annotation if path-prefix is undefined
|
||||
|
43
README.md
@ -6,29 +6,40 @@
|
||||
[](https://paypal.me/marhalide)
|
||||
|
||||
<!-- Plugin description -->
|
||||
This is an easy plugin to manage internationalization for JSON or Resource-Bundle(Properties) based locale files.
|
||||
Most common use case is for translating Webapps or simple Java Applications. Translating large scale projects was never that easy with your favourite IDE!
|
||||
This is a plugin for easier management of translation files of projects that need to be translated into different languages. Translating large projects has never been so easy with your favorite IDE!
|
||||
|
||||
## Use Cases
|
||||
- Webapps: For example [Vue](https://vuejs.org/) with [vue-i18n](https://kazupon.github.io/vue-i18n/) or any other JSON translation file based technology
|
||||
- Java based Resource-Bundle
|
||||
- Webapps: [Vue](https://vuejs.org/) with [vue-i18n](https://kazupon.github.io/vue-i18n/), [React](https://reactjs.org/) or any other json based technology
|
||||
- Java projects based on Resource-Bundle's
|
||||
- Projects that uses yaml, json or properties as locale file base for internationalization
|
||||
|
||||
## Features
|
||||
- UI Tool Window with Table- and Tree-View representation
|
||||
- UI Tool Window which supports tree- or table-view
|
||||
- Easily Add / Edit / Delete translations
|
||||
- Filter / Search function to hide irrelevant keys
|
||||
- Key completion and annotation inside editor
|
||||
- Filter function with full-text-search support
|
||||
- Editor Assistance: Key completion, annotation and referencing
|
||||
- Key sorting and nesting can be configured
|
||||
- Configurable locales directory & preferred locale for ui presentation
|
||||
- Supports modularized (splitted) json files
|
||||
- Translation keys with missing definition for any locale will be displayed red
|
||||
- Quick edit any translation by right-click (IntelliJ Popup Action)
|
||||
- Quick delete any translation via <kbd>DEL</kbd>-Key
|
||||
- Missing language translations will be indicated red
|
||||
- Quick actions: <kbd>right-click</kbd> or <kbd>DEL</kbd> to edit or delete a translation
|
||||
- Automatically reloads translation data if any locale file was changed
|
||||
<!-- Plugin description end -->
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
- Using IDE built-in plugin system:
|
||||
@ -45,8 +56,8 @@ Most common use case is for translating Webapps or simple Java Applications. Tra
|
||||
- Install plugin. See **Installation** section
|
||||
- Create a directory which will hold the locale files
|
||||
- Create a file for each required locale (e.g de.json, en.json) Note: Each json file must at least define an empty section (e.g. **{}**)
|
||||
- Click on the **Settings** Action inside the Easy I18n Tool Window
|
||||
- Select the created directory (optional: define the preferred locale to view) and press Ok
|
||||
- Click on the **Settings** Action inside the EasyI18n Tool Window
|
||||
- Select the created directory (optional: define the preferred locale to view) and press **Ok**
|
||||
- Translations can now be created / edited or deleted
|
||||
|
||||
Examples for the configuration can be found in the [/example](https://github.com/marhali/easy-i18n/tree/main/example) folder.
|
||||
|
@ -1,4 +1,3 @@
|
||||
import io.gitlab.arturbosch.detekt.Detekt
|
||||
import org.jetbrains.changelog.markdownToHTML
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@ -8,15 +7,13 @@ plugins {
|
||||
// Java support
|
||||
id("java")
|
||||
// Kotlin support
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.10"
|
||||
// gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
|
||||
id("org.jetbrains.intellij") version "1.0"
|
||||
// gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
|
||||
id("org.jetbrains.changelog") version "1.1.2"
|
||||
// detekt linter - read more: https://detekt.github.io/detekt/gradle.html
|
||||
id("io.gitlab.arturbosch.detekt") version "1.17.1"
|
||||
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
|
||||
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.31"
|
||||
// Gradle IntelliJ Plugin
|
||||
id("org.jetbrains.intellij") version "1.2.1"
|
||||
// Gradle Changelog Plugin
|
||||
id("org.jetbrains.changelog") version "1.3.1"
|
||||
// Gradle Qodana Plugin
|
||||
id("org.jetbrains.qodana") version "0.1.13"
|
||||
}
|
||||
|
||||
group = properties("pluginGroup")
|
||||
@ -26,55 +23,45 @@ version = properties("pluginVersion")
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
|
||||
}
|
||||
|
||||
// Configure gradle-intellij-plugin plugin.
|
||||
// Read more: https://github.com/JetBrains/gradle-intellij-plugin
|
||||
// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin
|
||||
intellij {
|
||||
pluginName.set(properties("pluginName"))
|
||||
version.set(properties("platformVersion"))
|
||||
type.set(properties("platformType"))
|
||||
downloadSources.set(properties("platformDownloadSources").toBoolean())
|
||||
updateSinceUntilBuild.set(true)
|
||||
|
||||
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
|
||||
plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty))
|
||||
}
|
||||
|
||||
// Configure gradle-changelog-plugin plugin.
|
||||
// Read more: https://github.com/JetBrains/gradle-changelog-plugin
|
||||
// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
|
||||
changelog {
|
||||
version = properties("pluginVersion")
|
||||
groups = emptyList()
|
||||
version.set(properties("pluginVersion"))
|
||||
groups.set(emptyList())
|
||||
}
|
||||
|
||||
// Configure detekt plugin.
|
||||
// Read more: https://detekt.github.io/detekt/kotlindsl.html
|
||||
detekt {
|
||||
config = files("./detekt-config.yml")
|
||||
buildUponDefaultConfig = true
|
||||
|
||||
reports {
|
||||
html.enabled = false
|
||||
xml.enabled = false
|
||||
txt.enabled = false
|
||||
}
|
||||
// Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin
|
||||
qodana {
|
||||
cachePath.set(projectDir.resolve(".qodana").canonicalPath)
|
||||
reportPath.set(projectDir.resolve("build/reports/inspections").canonicalPath)
|
||||
saveReport.set(true)
|
||||
showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// Set the compatibility versions to 1.8
|
||||
// Set the JVM compatibility versions
|
||||
properties("javaVersion").let {
|
||||
withType<JavaCompile> {
|
||||
sourceCompatibility = "1.8"
|
||||
targetCompatibility = "1.8"
|
||||
sourceCompatibility = it
|
||||
targetCompatibility = it
|
||||
}
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
kotlinOptions.jvmTarget = it
|
||||
}
|
||||
}
|
||||
|
||||
withType<Detekt> {
|
||||
jvmTarget = "1.8"
|
||||
wrapper {
|
||||
gradleVersion = properties("gradleVersion")
|
||||
}
|
||||
|
||||
patchPluginXml {
|
||||
@ -84,7 +71,7 @@ tasks {
|
||||
|
||||
// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
|
||||
pluginDescription.set(
|
||||
File(projectDir, "README.md").readText().lines().run {
|
||||
projectDir.resolve("README.md").readText().lines().run {
|
||||
val start = "<!-- Plugin description -->"
|
||||
val end = "<!-- Plugin description end -->"
|
||||
|
||||
@ -96,11 +83,26 @@ tasks {
|
||||
)
|
||||
|
||||
// Get the latest available change notes from the changelog file
|
||||
changeNotes.set(provider { changelog.getLatest().toHTML() })
|
||||
changeNotes.set(provider {
|
||||
changelog.run {
|
||||
getOrNull(properties("pluginVersion")) ?: getLatest()
|
||||
}.toHTML()
|
||||
})
|
||||
}
|
||||
|
||||
runPluginVerifier {
|
||||
ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty))
|
||||
// Configure UI tests plugin
|
||||
// Read more: https://github.com/JetBrains/intellij-ui-test-robot
|
||||
runIdeForUiTests {
|
||||
systemProperty("robot-server.port", "8082")
|
||||
systemProperty("ide.mac.message.dialogs.as.sheets", "false")
|
||||
systemProperty("jb.privacy.policy.text", "<!--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 {
|
||||
|
@ -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
|
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 9.2 KiB |
BIN
example/images/key-annotation.PNG
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
example/images/key-completion.PNG
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
example/images/key-edit.PNG
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
example/images/settings.PNG
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
example/images/table-view.PNG
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
example/images/tree-view.PNG
Normal file
After Width: | Height: | Size: 11 KiB |
@ -3,24 +3,29 @@
|
||||
|
||||
pluginGroup = de.marhali.easyi18n
|
||||
pluginName = easy-i18n
|
||||
pluginVersion = 1.5.1
|
||||
# SemVer format -> https://semver.org
|
||||
pluginVersion = 1.6.0
|
||||
|
||||
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||
# for insight into build numbers and IntelliJ Platform versions.
|
||||
pluginSinceBuild = 202
|
||||
pluginUntilBuild = 212.*
|
||||
|
||||
# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl
|
||||
# See https://jb.gg/intellij-platform-builds-list for available build versions
|
||||
pluginVerifierIdeVersions = 2020.2.4, 2020.3.4, 2021.2
|
||||
pluginSinceBuild = 203
|
||||
pluginUntilBuild = 213.*
|
||||
|
||||
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
|
||||
platformType = IC
|
||||
platformVersion = 2021.2
|
||||
platformDownloadSources = true
|
||||
platformVersion = 2020.3.4
|
||||
|
||||
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
|
||||
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
|
||||
platformPlugins = org.jetbrains.kotlin
|
||||
|
||||
# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
|
||||
javaVersion = 11
|
||||
|
||||
# Gradle Releases -> https://github.com/gradle/gradle/releases
|
||||
gradleVersion = 7.3
|
||||
|
||||
# Opt-out flag for bundling Kotlin standard library.
|
||||
# See https://kotlinlang.org/docs/reference/using-gradle.html#dependency-on-the-standard-library for details.
|
||||
# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
|
||||
# suppress inspection "UnusedProperty"
|
||||
kotlin.stdlib.default.dependency = false
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
2
gradlew
vendored
@ -72,7 +72,7 @@ case "`uname`" in
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
|
6
qodana.yml
Normal file
@ -0,0 +1,6 @@
|
||||
# Qodana configuration:
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html
|
||||
|
||||
version: 1.0
|
||||
profile:
|
||||
name: qodana.recommended
|
55
src/main/java/de/marhali/easyi18n/DataBus.java
Normal 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
118
src/main/java/de/marhali/easyi18n/DataStore.java
Normal 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");
|
||||
}
|
||||
}
|
76
src/main/java/de/marhali/easyi18n/InstanceManager.java
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
|
||||
import de.marhali.easyi18n.service.WindowManager;
|
||||
import de.marhali.easyi18n.dialog.AddDialog;
|
||||
import de.marhali.easyi18n.util.PathUtil;
|
||||
import de.marhali.easyi18n.util.TreeUtil;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -42,7 +43,7 @@ public class AddAction extends AnAction {
|
||||
TreePath path = manager.getTreeView().getTree().getSelectionPath();
|
||||
|
||||
if(path != null) {
|
||||
return TreeUtil.getFullPath(path) + ".";
|
||||
return TreeUtil.getFullPath(path) + PathUtil.DELIMITER;
|
||||
}
|
||||
|
||||
} else { // Table View
|
||||
|
@ -4,7 +4,7 @@ import com.intellij.icons.AllIcons;
|
||||
import com.intellij.openapi.actionSystem.AnAction;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -23,6 +23,9 @@ public class ReloadAction extends AnAction {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
||||
DataStore.getInstance(e.getProject()).reloadFromDisk();
|
||||
InstanceManager manager = InstanceManager.get(e.getProject());
|
||||
manager.store().loadFromPersistenceLayer((success) -> {
|
||||
manager.bus().propagate().onUpdateData(manager.store().getData());
|
||||
});
|
||||
}
|
||||
}
|
@ -7,8 +7,9 @@ import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.ui.components.JBScrollPane;
|
||||
import com.intellij.ui.components.JBTextField;
|
||||
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.KeyedTranslation;
|
||||
import de.marhali.easyi18n.model.Translation;
|
||||
import de.marhali.easyi18n.model.TranslationCreate;
|
||||
|
||||
import javax.swing.*;
|
||||
@ -48,16 +49,16 @@ public class AddDialog {
|
||||
}
|
||||
|
||||
private void saveTranslation() {
|
||||
Map<String, String> messages = new HashMap<>();
|
||||
Translation translation = new Translation();
|
||||
|
||||
valueTextFields.forEach((k, v) -> {
|
||||
if(!v.getText().isEmpty()) {
|
||||
messages.put(k, v.getText());
|
||||
translation.put(k, v.getText());
|
||||
}
|
||||
});
|
||||
|
||||
TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), messages));
|
||||
DataStore.getInstance(project).processUpdate(creation);
|
||||
TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), translation));
|
||||
InstanceManager.get(project).processUpdate(creation);
|
||||
}
|
||||
|
||||
private DialogBuilder prepare() {
|
||||
@ -75,7 +76,8 @@ public class AddDialog {
|
||||
|
||||
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
|
||||
valueTextFields = new HashMap<>();
|
||||
for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
|
||||
|
||||
for(String locale : InstanceManager.get(project).store().getData().getLocales()) {
|
||||
JBLabel localeLabel = new JBLabel(locale);
|
||||
JBTextField localeText = new JBTextField();
|
||||
localeLabel.setLabelFor(localeText);
|
||||
|
@ -6,11 +6,12 @@ import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.ui.components.JBScrollPane;
|
||||
import com.intellij.ui.components.JBTextField;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.KeyedTranslation;
|
||||
import de.marhali.easyi18n.model.Translation;
|
||||
import de.marhali.easyi18n.model.TranslationDelete;
|
||||
import de.marhali.easyi18n.model.TranslationUpdate;
|
||||
import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor;
|
||||
import de.marhali.easyi18n.model.TranslationUpdate;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EtchedBorder;
|
||||
@ -40,23 +41,22 @@ public class EditDialog {
|
||||
int code = prepare().show();
|
||||
|
||||
if(code == DialogWrapper.OK_EXIT_CODE) { // Edit
|
||||
DataStore.getInstance(project).processUpdate(new TranslationUpdate(origin, getChanges()));
|
||||
|
||||
InstanceManager.get(project).processUpdate(new TranslationUpdate(origin, getChanges()));
|
||||
} else if(code == DeleteActionDescriptor.EXIT_CODE) { // Delete
|
||||
DataStore.getInstance(project).processUpdate(new TranslationDelete(origin));
|
||||
InstanceManager.get(project).processUpdate(new TranslationDelete(origin));
|
||||
}
|
||||
}
|
||||
|
||||
private KeyedTranslation getChanges() {
|
||||
Map<String, String> messages = new HashMap<>();
|
||||
Translation translation = new Translation();
|
||||
|
||||
valueTextFields.forEach((k, v) -> {
|
||||
if(!v.getText().isEmpty()) {
|
||||
messages.put(k, v.getText());
|
||||
translation.put(k, v.getText());
|
||||
}
|
||||
});
|
||||
|
||||
return new KeyedTranslation(keyTextField.getText(), messages);
|
||||
return new KeyedTranslation(keyTextField.getText(), translation);
|
||||
}
|
||||
|
||||
private DialogBuilder prepare() {
|
||||
@ -74,9 +74,10 @@ public class EditDialog {
|
||||
|
||||
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
|
||||
valueTextFields = new HashMap<>();
|
||||
for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
|
||||
|
||||
for(String locale : InstanceManager.get(project).store().getData().getLocales()) {
|
||||
JBLabel localeLabel = new JBLabel(locale);
|
||||
JBTextField localeText = new JBTextField(this.origin.getTranslations().get(locale));
|
||||
JBTextField localeText = new JBTextField(this.origin.getTranslation().get(locale));
|
||||
localeLabel.setLabelFor(localeText);
|
||||
|
||||
valuePanel.add(localeLabel);
|
||||
|
@ -9,8 +9,9 @@ import com.intellij.ui.components.JBCheckBox;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.ui.components.JBTextField;
|
||||
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.SettingsState;
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@ -28,6 +29,8 @@ public class SettingsDialog {
|
||||
private JBTextField filePatternText;
|
||||
private JBTextField previewLocaleText;
|
||||
private JBTextField pathPrefixText;
|
||||
private JBCheckBox sortKeysCheckbox;
|
||||
private JBCheckBox nestedKeysCheckbox;
|
||||
private JBCheckBox codeAssistanceCheckbox;
|
||||
|
||||
public SettingsDialog(Project project) {
|
||||
@ -35,30 +38,31 @@ public class SettingsDialog {
|
||||
}
|
||||
|
||||
public void showAndHandle() {
|
||||
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
|
||||
String filePattern = SettingsService.getInstance(project).getState().getFilePattern();
|
||||
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
|
||||
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
|
||||
boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance();
|
||||
SettingsState state = SettingsService.getInstance(project).getState();
|
||||
|
||||
if(prepare(localesPath, filePattern, previewLocale, pathPrefix, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
|
||||
SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText());
|
||||
SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText());
|
||||
SettingsService.getInstance(project).getState().setPreviewLocale(previewLocaleText.getText());
|
||||
SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected());
|
||||
SettingsService.getInstance(project).getState().setPathPrefix(pathPrefixText.getText());
|
||||
if(prepare(state).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
|
||||
state.setLocalesPath(pathText.getText());
|
||||
state.setFilePattern(filePatternText.getText());
|
||||
state.setPreviewLocale(previewLocaleText.getText());
|
||||
state.setPathPrefix(pathPrefixText.getText());
|
||||
state.setSortKeys(sortKeysCheckbox.isSelected());
|
||||
state.setNestedKeys(nestedKeysCheckbox.isSelected());
|
||||
state.setCodeAssistance(codeAssistanceCheckbox.isSelected());
|
||||
|
||||
// Reload instance
|
||||
DataStore.getInstance(project).reloadFromDisk();
|
||||
InstanceManager manager = InstanceManager.get(project);
|
||||
manager.store().loadFromPersistenceLayer((success) -> {
|
||||
manager.bus().propagate().onUpdateData(manager.store().getData());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, String pathPrefix, boolean codeAssistance) {
|
||||
private DialogBuilder prepare(SettingsState state) {
|
||||
JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
|
||||
|
||||
/* path */
|
||||
JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text"));
|
||||
pathText = new TextFieldWithBrowseButton(new JTextField(localesPath));
|
||||
pathText = new TextFieldWithBrowseButton(new JTextField(state.getLocalesPath()));
|
||||
|
||||
pathLabel.setLabelFor(pathText);
|
||||
pathText.addBrowseFolderListener(ResourceBundle.getBundle("messages").getString("settings.path.title"), null, project, new FileChooserDescriptor(
|
||||
@ -69,14 +73,14 @@ public class SettingsDialog {
|
||||
|
||||
/* file pattern */
|
||||
JBLabel filePatternLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.file-pattern"));
|
||||
filePatternText = new JBTextField(filePattern);
|
||||
filePatternText = new JBTextField(state.getFilePattern());
|
||||
|
||||
rootPanel.add(filePatternLabel);
|
||||
rootPanel.add(filePatternText);
|
||||
|
||||
/* preview locale */
|
||||
JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview"));
|
||||
previewLocaleText = new JBTextField(previewLocale);
|
||||
previewLocaleText = new JBTextField(state.getPreviewLocale());
|
||||
previewLocaleLabel.setLabelFor(previewLocaleText);
|
||||
|
||||
rootPanel.add(previewLocaleLabel);
|
||||
@ -84,14 +88,26 @@ public class SettingsDialog {
|
||||
|
||||
/* path prefix */
|
||||
JBLabel pathPrefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.prefix"));
|
||||
pathPrefixText = new JBTextField(pathPrefix);
|
||||
pathPrefixText = new JBTextField(state.getPathPrefix());
|
||||
|
||||
rootPanel.add(pathPrefixLabel);
|
||||
rootPanel.add(pathPrefixText);
|
||||
|
||||
/* sort keys */
|
||||
sortKeysCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.keys.sort"));
|
||||
sortKeysCheckbox.setSelected(state.isSortKeys());
|
||||
|
||||
rootPanel.add(sortKeysCheckbox);
|
||||
|
||||
/* nested keys */
|
||||
nestedKeysCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.keys.nested"));
|
||||
nestedKeysCheckbox.setSelected(state.isNestedKeys());
|
||||
|
||||
rootPanel.add(nestedKeysCheckbox);
|
||||
|
||||
/* code assistance */
|
||||
codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance"));
|
||||
codeAssistanceCheckbox.setSelected(codeAssistance);
|
||||
codeAssistanceCheckbox.setSelected(state.isCodeAssistance());
|
||||
|
||||
rootPanel.add(codeAssistanceCheckbox);
|
||||
|
||||
|
@ -4,8 +4,8 @@ import com.intellij.lang.annotation.AnnotationHolder;
|
||||
import com.intellij.lang.annotation.HighlightSeverity;
|
||||
import com.intellij.openapi.project.Project;
|
||||
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.TranslationNode;
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -39,7 +39,7 @@ public class KeyAnnotator {
|
||||
searchKey = searchKey.substring(1);
|
||||
}
|
||||
|
||||
LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(searchKey);
|
||||
TranslationNode node = InstanceManager.get(project).store().getData().getNode(searchKey);
|
||||
|
||||
if(node == null) { // Unknown translation. Just ignore it
|
||||
return;
|
||||
|
@ -5,9 +5,11 @@ import com.intellij.codeInsight.lookup.*;
|
||||
import com.intellij.icons.AllIcons;
|
||||
import com.intellij.openapi.project.*;
|
||||
import com.intellij.util.*;
|
||||
import de.marhali.easyi18n.model.*;
|
||||
import de.marhali.easyi18n.DataStore;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.Translation;
|
||||
import de.marhali.easyi18n.service.*;
|
||||
import de.marhali.easyi18n.util.TranslationsUtil;
|
||||
import de.marhali.easyi18n.util.PathUtil;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.util.*;
|
||||
@ -29,7 +31,8 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
|
||||
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 pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
|
||||
|
||||
@ -54,7 +57,7 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
|
||||
pathPrefix += ".";
|
||||
}
|
||||
|
||||
List<String> fullKeys = store.getTranslations().getFullKeys();
|
||||
Set<String> fullKeys = store.getData().getFullKeys();
|
||||
|
||||
int sections = path.split("\\.").length;
|
||||
int maxSectionForwardLookup = 5;
|
||||
@ -65,19 +68,20 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
|
||||
String[] keySections = key.split("\\.");
|
||||
|
||||
if(keySections.length > sections + maxSectionForwardLookup) { // Key is too deep nested
|
||||
String shrinkKey = TranslationsUtil.sectionsToFullPath(Arrays.asList(
|
||||
Arrays.copyOf(keySections, sections + maxSectionForwardLookup)));
|
||||
String shrinkKey = pathUtil.concat(Arrays.asList(
|
||||
Arrays.copyOf(keySections, sections + maxSectionForwardLookup)
|
||||
));
|
||||
|
||||
result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey)
|
||||
.appendTailText(" I18n([])", true));
|
||||
|
||||
} else {
|
||||
LocalizedNode node = store.getTranslations().getNode(key);
|
||||
String translation = node != null ? node.getValue().get(previewLocale) : null;
|
||||
Translation translation = store.getData().getTranslation(key);
|
||||
String content = translation.get(previewLocale);
|
||||
|
||||
result.addElement(LookupElementBuilder.create(pathPrefix + key)
|
||||
.withIcon(AllIcons.Actions.PreserveCaseHover)
|
||||
.appendTailText(" I18n(" + previewLocale + ": " + translation + ")", true)
|
||||
.appendTailText(" I18n(" + previewLocale + ": " + content + ")", true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.impl.FakePsiElement;
|
||||
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.dialog.AddDialog;
|
||||
import de.marhali.easyi18n.dialog.EditDialog;
|
||||
import de.marhali.easyi18n.model.KeyedTranslation;
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
|
||||
import de.marhali.easyi18n.model.KeyedTranslation;
|
||||
import de.marhali.easyi18n.model.Translation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -52,10 +52,10 @@ public class KeyReference extends PsiReferenceBase<PsiElement> {
|
||||
|
||||
@Override
|
||||
public void navigate(boolean requestFocus) {
|
||||
LocalizedNode node = DataStore.getInstance(getProject()).getTranslations().getNode(getKey());
|
||||
Translation translation = InstanceManager.get(getProject()).store().getData().getTranslation(getKey());
|
||||
|
||||
if(node != null) {
|
||||
new EditDialog(getProject(), new KeyedTranslation(getKey(), node.getValue())).showAndHandle();
|
||||
if(translation != null) {
|
||||
new EditDialog(getProject(), new KeyedTranslation(getKey(), translation)).showAndHandle();
|
||||
} else {
|
||||
new AddDialog(getProject(), getKey()).showAndHandle();
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import com.intellij.patterns.PlatformPatterns;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.editor.KeyReference;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -38,7 +38,7 @@ public class GenericKeyReferenceContributor extends PsiReferenceContributor {
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
|
||||
if(InstanceManager.get(element.getProject()).store().getData().getTranslation(value) == null) {
|
||||
if(!KeyReference.isReferencable(value)) { // Creation policy
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import com.intellij.psi.*;
|
||||
|
||||
import com.intellij.util.ProcessingContext;
|
||||
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.editor.KeyReference;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -45,7 +45,7 @@ public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
|
||||
if(InstanceManager.get(element.getProject()).store().getData().getNode(value) == null) {
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
package de.marhali.easyi18n.io;
|
||||
|
||||
import de.marhali.easyi18n.util.StringUtil;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
@ -10,11 +11,13 @@ import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility methods for simple array support.
|
||||
* Simple array support for translation values.
|
||||
* Some i18n systems allows the user to define array values for some translations.
|
||||
* We support array values by wrapping them into: '!arr[valueA;valueB]'.
|
||||
*
|
||||
* @author marhali
|
||||
*/
|
||||
public abstract class ArrayUtil {
|
||||
|
||||
public abstract class ArrayMapper {
|
||||
static final String PREFIX = "!arr[";
|
||||
static final String SUFFIX = "]";
|
||||
static final char DELIMITER = ';';
|
||||
@ -22,7 +25,7 @@ public abstract class ArrayUtil {
|
||||
static final String SPLITERATOR_REGEX =
|
||||
MessageFormat.format("(?<!\\\\){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);
|
||||
|
||||
int i = 0;
|
||||
@ -43,7 +46,7 @@ public abstract class ArrayUtil {
|
||||
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());
|
||||
|
||||
for(String element : concat.split(SPLITERATOR_REGEX)) {
|
62
src/main/java/de/marhali/easyi18n/io/IOStrategy.java
Normal 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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
package de.marhali.easyi18n.io.json;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import de.marhali.easyi18n.io.ArrayMapper;
|
||||
|
||||
/**
|
||||
* Utility methods to read and write json arrays.
|
||||
* Map json array values.
|
||||
* @author marhali
|
||||
*/
|
||||
public class JsonArrayUtil extends ArrayUtil {
|
||||
public class JsonArrayMapper extends ArrayMapper {
|
||||
public static String read(JsonArray array) {
|
||||
return read(array.iterator(), JsonElement::getAsString);
|
||||
}
|
118
src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
73
src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
package de.marhali.easyi18n.io.yaml;
|
||||
|
||||
import de.marhali.easyi18n.io.ArrayMapper;
|
||||
|
||||
import thito.nodeflow.config.ListSection;
|
||||
|
||||
/**
|
||||
* Utility methods to read and write yaml lists.
|
||||
* Map for yaml array values.
|
||||
* @author marhali
|
||||
*/
|
||||
public class YamlArrayUtil extends ArrayUtil {
|
||||
|
||||
public class YamlArrayMapper extends ArrayMapper {
|
||||
public static String read(ListSection list) {
|
||||
return read(list.iterator(), Object::toString);
|
||||
}
|
121
src/main/java/de/marhali/easyi18n/io/yaml/YamlIOStrategy.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
72
src/main/java/de/marhali/easyi18n/io/yaml/YamlMapper.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -1,42 +1,43 @@
|
||||
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
|
||||
*/
|
||||
public class KeyedTranslation {
|
||||
|
||||
private String key;
|
||||
private Map<String, String> translations;
|
||||
private @NotNull String key;
|
||||
private @Nullable Translation translation;
|
||||
|
||||
public KeyedTranslation(String key, Map<String, String> translations) {
|
||||
public KeyedTranslation(@NotNull String key, @Nullable Translation translation) {
|
||||
this.key = key;
|
||||
this.translations = translations;
|
||||
this.translation = translation;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
public @NotNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
public void setKey(@NotNull String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Map<String, String> getTranslations() {
|
||||
return translations;
|
||||
public @Nullable Translation getTranslation() {
|
||||
return translation;
|
||||
}
|
||||
|
||||
public void setTranslations(Map<String, String> translations) {
|
||||
this.translations = translations;
|
||||
public void setTranslation(@NotNull Translation translation) {
|
||||
this.translation = translation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KeyedTranslation{" +
|
||||
"key='" + key + '\'' +
|
||||
", translations=" + translations +
|
||||
", translation=" + translation +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -12,12 +12,16 @@ public class SettingsState {
|
||||
public static final String DEFAULT_PREVIEW_LOCALE = "en";
|
||||
public static final String DEFAULT_FILE_PATTERN = ".*";
|
||||
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;
|
||||
|
||||
private String localesPath;
|
||||
private String filePattern;
|
||||
private String previewLocale;
|
||||
private String pathPrefix;
|
||||
private Boolean sortKeys;
|
||||
private Boolean nestedKeys;
|
||||
private Boolean codeAssistance;
|
||||
|
||||
public SettingsState() {}
|
||||
@ -54,6 +58,22 @@ public class SettingsState {
|
||||
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() {
|
||||
return codeAssistance == null ? DEFAULT_CODE_ASSISTANCE : codeAssistance;
|
||||
}
|
||||
|
29
src/main/java/de/marhali/easyi18n/model/Translation.java
Normal 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();
|
||||
}
|
||||
}
|
186
src/main/java/de/marhali/easyi18n/model/TranslationData.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
117
src/main/java/de/marhali/easyi18n/model/TranslationNode.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -3,7 +3,9 @@ package de.marhali.easyi18n.model;
|
||||
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
|
||||
*/
|
||||
public class TranslationUpdate {
|
||||
@ -16,24 +18,24 @@ public class TranslationUpdate {
|
||||
this.change = change;
|
||||
}
|
||||
|
||||
public KeyedTranslation getOrigin() {
|
||||
public @Nullable KeyedTranslation getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
public KeyedTranslation getChange() {
|
||||
public @Nullable KeyedTranslation getChange() {
|
||||
return change;
|
||||
}
|
||||
|
||||
public boolean isCreation() {
|
||||
return origin == null;
|
||||
return this.origin == null;
|
||||
}
|
||||
|
||||
public boolean isDeletion() {
|
||||
return change == null;
|
||||
return this.change == null;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.marhali.easyi18n;
|
||||
package de.marhali.easyi18n.service;
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnAction;
|
||||
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.ContentFactory;
|
||||
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.service.WindowManager;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.action.*;
|
||||
import de.marhali.easyi18n.tabs.TableView;
|
||||
import de.marhali.easyi18n.tabs.TreeView;
|
||||
@ -48,16 +47,16 @@ public class TranslatorToolWindowFactory implements ToolWindowFactory {
|
||||
actions.add(new AddAction());
|
||||
actions.add(new ReloadAction());
|
||||
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);
|
||||
|
||||
// Initialize Window Manager
|
||||
WindowManager.getInstance().initialize(toolWindow, treeView, tableView);
|
||||
|
||||
// Synchronize ui with underlying data
|
||||
DataStore store = DataStore.getInstance(project);
|
||||
store.addSynchronizer(treeView);
|
||||
store.addSynchronizer(tableView);
|
||||
store.synchronize(null, null);
|
||||
InstanceManager manager = InstanceManager.get(project);
|
||||
manager.bus().addListener(treeView);
|
||||
manager.bus().addListener(tableView);
|
||||
manager.bus().propagate().onUpdateData(manager.store().getData());
|
||||
}
|
||||
}
|
@ -5,6 +5,10 @@ import com.intellij.openapi.wm.ToolWindow;
|
||||
import de.marhali.easyi18n.tabs.TableView;
|
||||
import de.marhali.easyi18n.tabs.TreeView;
|
||||
|
||||
/**
|
||||
* Provides access to the plugin's own tool-window.
|
||||
* @author marhali
|
||||
*/
|
||||
public class WindowManager {
|
||||
|
||||
private static WindowManager INSTANCE;
|
||||
|
@ -4,17 +4,14 @@ import com.intellij.openapi.project.Project;
|
||||
import com.intellij.ui.components.JBScrollPane;
|
||||
import com.intellij.ui.table.JBTable;
|
||||
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
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.TranslationDelete;
|
||||
import de.marhali.easyi18n.model.table.TableModelTranslator;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
import de.marhali.easyi18n.model.*;
|
||||
import de.marhali.easyi18n.dialog.EditDialog;
|
||||
import de.marhali.easyi18n.listener.DeleteKeyListener;
|
||||
import de.marhali.easyi18n.listener.PopupClickListener;
|
||||
import de.marhali.easyi18n.model.bus.BusListener;
|
||||
import de.marhali.easyi18n.renderer.TableRenderer;
|
||||
import de.marhali.easyi18n.tabs.mapper.TableModelMapper;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -28,10 +25,12 @@ import java.util.ResourceBundle;
|
||||
* Shows translation state as table.
|
||||
* @author marhali
|
||||
*/
|
||||
public class TableView implements DataSynchronizer {
|
||||
public class TableView implements BusListener {
|
||||
|
||||
private final Project project;
|
||||
|
||||
private TableModelMapper currentMapper;
|
||||
|
||||
private JPanel rootPanel;
|
||||
private JPanel containerPanel;
|
||||
|
||||
@ -54,10 +53,10 @@ public class TableView implements DataSynchronizer {
|
||||
|
||||
if(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) {
|
||||
new EditDialog(project, new KeyedTranslation(fullPath, node.getValue())).showAndHandle();
|
||||
if(translation != null) {
|
||||
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,33 +66,41 @@ public class TableView implements DataSynchronizer {
|
||||
for (int selectedRow : table.getSelectedRows()) {
|
||||
String fullPath = String.valueOf(table.getValueAt(selectedRow, 0));
|
||||
|
||||
DataStore.getInstance(project).processUpdate(
|
||||
new TranslationDelete(new KeyedTranslation(fullPath, null)));
|
||||
InstanceManager.get(project).processUpdate(
|
||||
new TranslationDelete(new KeyedTranslation(fullPath, null))
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronize(@NotNull Translations translations,
|
||||
@Nullable String searchQuery, @Nullable String scrollTo) {
|
||||
public void onUpdateData(@NotNull TranslationData data) {
|
||||
table.setModel(this.currentMapper = new TableModelMapper(data, update ->
|
||||
InstanceManager.get(project).processUpdate(update)));
|
||||
}
|
||||
|
||||
table.setModel(new TableModelTranslator(translations, searchQuery, update ->
|
||||
DataStore.getInstance(project).processUpdate(update)));
|
||||
|
||||
if(scrollTo != null) {
|
||||
@Override
|
||||
public void onFocusKey(@Nullable String key) {
|
||||
int row = -1;
|
||||
|
||||
for (int i = 0; i < table.getRowCount(); i++) {
|
||||
if (String.valueOf(table.getValueAt(i, 0)).equals(scrollTo)) {
|
||||
if (String.valueOf(table.getValueAt(i, 0)).equals(key)) {
|
||||
row = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (row > -1) { // Matched @scrollTo
|
||||
if (row > -1) { // Matched @key
|
||||
table.scrollRectToVisible(
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public JPanel getRootPanel() {
|
||||
|
@ -8,19 +8,20 @@ import com.intellij.openapi.project.Project;
|
||||
import com.intellij.ui.components.JBScrollPane;
|
||||
import com.intellij.ui.treeStructure.Tree;
|
||||
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
import de.marhali.easyi18n.model.DataSynchronizer;
|
||||
import de.marhali.easyi18n.model.Translations;
|
||||
import de.marhali.easyi18n.InstanceManager;
|
||||
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.tree.TreeModelTranslator;
|
||||
import de.marhali.easyi18n.model.bus.BusListener;
|
||||
import de.marhali.easyi18n.action.treeview.CollapseTreeViewAction;
|
||||
import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction;
|
||||
import de.marhali.easyi18n.dialog.EditDialog;
|
||||
import de.marhali.easyi18n.listener.DeleteKeyListener;
|
||||
import de.marhali.easyi18n.listener.PopupClickListener;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
@ -36,10 +37,12 @@ import java.util.ResourceBundle;
|
||||
* Show translation state as tree.
|
||||
* @author marhali
|
||||
*/
|
||||
public class TreeView implements DataSynchronizer {
|
||||
public class TreeView implements BusListener {
|
||||
|
||||
private final Project project;
|
||||
|
||||
private TreeModelMapper currentMapper;
|
||||
|
||||
private JPanel rootPanel;
|
||||
private JPanel toolBarPanel;
|
||||
private JPanel containerPanel;
|
||||
@ -77,18 +80,28 @@ public class TreeView implements DataSynchronizer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void synchronize(@NotNull Translations translations,
|
||||
@Nullable String searchQuery, @Nullable String scrollTo) {
|
||||
|
||||
TreeModelTranslator model = new TreeModelTranslator(project, translations, searchQuery);
|
||||
tree.setModel(model);
|
||||
|
||||
if(searchQuery != null && !searchQuery.isEmpty()) {
|
||||
expandAll().run();
|
||||
public void onUpdateData(@NotNull TranslationData data) {
|
||||
tree.setModel(this.currentMapper = new TreeModelMapper(data, SettingsService.getInstance(project).getState()));
|
||||
}
|
||||
|
||||
if(scrollTo != null) {
|
||||
tree.scrollPathToVisible(model.findTreePath(scrollTo));
|
||||
@Override
|
||||
public void onFocusKey(@Nullable String key) {
|
||||
if(key != null && currentMapper != null) {
|
||||
TreePath path = currentMapper.findTreePath(key);
|
||||
this.tree.scrollPathToVisible(path);
|
||||
|
||||
if(this.tree.isCollapsed(path)) {
|
||||
this.tree.expandPath(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
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) {
|
||||
new EditDialog(project,new KeyedTranslation(fullPath, localizedNode.getValue())).showAndHandle();
|
||||
if(translation != null) {
|
||||
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -120,8 +133,9 @@ public class TreeView implements DataSynchronizer {
|
||||
for (TreePath path : tree.getSelectionPaths()) {
|
||||
String fullPath = TreeUtil.getFullPath(path);
|
||||
|
||||
DataStore.getInstance(project).processUpdate(
|
||||
new TranslationDelete(new KeyedTranslation(fullPath, null)));
|
||||
InstanceManager.get(project).processUpdate(
|
||||
new TranslationDelete(new KeyedTranslation(fullPath, null))
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
73
src/main/java/de/marhali/easyi18n/util/PathUtil.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package de.marhali.easyi18n.util;
|
||||
|
||||
import com.intellij.ide.projectView.PresentationData;
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.TreePath;
|
||||
@ -20,19 +19,18 @@ public class TreeUtil {
|
||||
public static String getFullPath(TreePath path) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
|
||||
for (Object obj : path.getPath()) {
|
||||
DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj;
|
||||
Object value = node.getUserObject();
|
||||
String section = value instanceof PresentationData ?
|
||||
((PresentationData) value).getPresentableText() : String.valueOf(value);
|
||||
|
||||
if(section == null || section.equals(LocalizedNode.ROOT_KEY)) { // Skip root node
|
||||
if(value == null) { // Skip empty sections
|
||||
continue;
|
||||
}
|
||||
|
||||
if(builder.length() != 0) {
|
||||
builder.append(".");
|
||||
builder.append(PathUtil.DELIMITER);
|
||||
}
|
||||
|
||||
builder.append(section);
|
||||
|
@ -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">
|
||||
<id>de.marhali.easyi18n</id>
|
||||
<name>Easy I18n</name>
|
||||
@ -10,7 +11,7 @@
|
||||
<depends optional="true" config-file="de.marhali.easyi18n-kotlin.xml">org.jetbrains.kotlin</depends>
|
||||
|
||||
<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" />
|
||||
|
||||
<completion.contributor language="any"
|
||||
|
@ -7,7 +7,7 @@ action.add=Add Translation
|
||||
action.edit=Edit Translation
|
||||
action.reload=Reload From Disk
|
||||
action.settings=Settings
|
||||
action.search=Search Key...
|
||||
action.search=Search...
|
||||
action.delete=Delete
|
||||
translation.key=Key
|
||||
translation.locales=Locales
|
||||
@ -16,4 +16,6 @@ settings.path.text=Locales directory
|
||||
settings.path.file-pattern=Translation file pattern
|
||||
settings.path.prefix=Path prefix
|
||||
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
|
270
src/test/java/de/marhali/easyi18n/TranslationDataTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
159
src/test/java/de/marhali/easyi18n/mapper/JsonMapperTest.java
Normal 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"));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
158
src/test/java/de/marhali/easyi18n/mapper/YamlMapperTest.java
Normal 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"));
|
||||
}
|
||||
}
|