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

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|

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