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

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

View File

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

View File

@ -1,111 +1,53 @@
# GitHub Actions Workflow created for testing and preparing the plugin release in following steps:
# - validate Gradle Wrapper,
# - run test and verifyPlugin tasks,
# - run buildPlugin task and prepare artifact for the further tests,
# - run IntelliJ Plugin Verifier,
# - run 'test' and 'verifyPlugin' tasks,
# - run Qodana inspections,
# - run 'buildPlugin' task and prepare artifact for the further tests,
# - run 'runPluginVerifier' task,
# - create a draft release.
#
# Workflow is triggered on push and pull_request events.
#
# Docs:
# - GitHub Actions: https://help.github.com/en/actions
# - IntelliJ Plugin Verifier GitHub Action: https://github.com/ChrisCarini/intellij-platform-plugin-verifier-action
# GitHub Actions reference: https://help.github.com/en/actions
#
## JBIJPPTPL
name: Build
on: [push, pull_request]
on:
# Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests)
push:
branches: [main]
# Trigger the workflow on any pull request
pull_request:
jobs:
# Run Gradle Wrapper Validation Action to verify the wrapper's checksum
gradleValidation:
name: Gradle Wrapper
# Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks
# Build plugin and provide the artifact for the next workflow jobs
build:
name: Build
runs-on: ubuntu-latest
outputs:
version: ${{ steps.properties.outputs.version }}
changelog: ${{ steps.properties.outputs.changelog }}
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
uses: actions/checkout@v2.4.0
# Validate wrapper
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1.0.3
uses: gradle/wrapper-validation-action@v1.0.4
# Run verifyPlugin and test Gradle tasks
test:
name: Test
needs: gradleValidation
runs-on: ubuntu-latest
steps:
# Setup Java 1.8 environment for the next steps
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
# Cache Gradle dependencies
- name: Setup Gradle Dependencies Cache
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
# Cache Gradle Wrapper
- name: Setup Gradle Wrapper Cache
uses: actions/cache@v2
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
# Run detekt, ktlint and tests
- name: Run Linters and Test
run: ./gradlew check
# Run verifyPlugin Gradle task
- name: Verify Plugin
run: ./gradlew verifyPlugin
# Build plugin with buildPlugin Gradle task and provide the artifact for the next workflow jobs
# Requires test job to be passed
build:
name: Build
needs: test
runs-on: ubuntu-latest
outputs:
name: ${{ steps.properties.outputs.name }}
version: ${{ steps.properties.outputs.version }}
changelog: ${{ steps.properties.outputs.changelog }}
artifact: ${{ steps.properties.outputs.artifact }}
steps:
# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
# Cache Gradle Dependencies
- name: Setup Gradle Dependencies Cache
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
# Cache Gradle Wrapper
- name: Setup Gradle Wrapper Cache
uses: actions/cache@v2
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
cache: gradle
# Set environment variables
- name: Export Properties
@ -119,128 +61,99 @@ jobs:
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
ARTIFACT="${NAME}-${VERSION}.zip"
echo "::set-output name=version::$VERSION"
echo "::set-output name=name::$NAME"
echo "::set-output name=changelog::$CHANGELOG"
echo "::set-output name=artifact::$ARTIFACT"
# Build artifact using buildPlugin Gradle task
- name: Build Plugin
run: ./gradlew buildPlugin
# Upload plugin artifact to make it available in the next jobs
- name: Upload artifact
uses: actions/upload-artifact@v1
with:
name: plugin-artifact
path: ./build/distributions/${{ steps.properties.outputs.artifact }}
# Verify built plugin using IntelliJ Plugin Verifier tool
# Requires build job to be passed
verify:
name: Verify
needs: build
runs-on: ubuntu-latest
steps:
# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
# Cache Gradle Dependencies
- name: Setup Gradle Dependencies Cache
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }}
# Cache Gradle Wrapper
- name: Setup Gradle Wrapper Cache
uses: actions/cache@v2
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
# Set environment variables
- name: Export Properties
id: properties
shell: bash
run: |
PROPERTIES="$(./gradlew properties --console=plain -q)"
IDE_VERSIONS="$(echo "$PROPERTIES" | grep "^pluginVerifierIdeVersions:" | base64)"
echo "::set-output name=ideVersions::$IDE_VERSIONS"
echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier"
./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier
# Run tests
- name: Run Tests
run: ./gradlew test
# Collect Tests Result of failed tests
- name: Collect Tests Result
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: tests-result
path: ${{ github.workspace }}/build/reports/tests
# Cache Plugin Verifier IDEs
- name: Setup Plugin Verifier IDEs Cache
uses: actions/cache@v2
uses: actions/cache@v2.1.6
with:
path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides
key: ${{ runner.os }}-plugin-verifier-${{ steps.properties.outputs.ideVersions }}
key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
# Run IntelliJ Plugin Verifier action using GitHub Action
- name: Verify Plugin
# Run Verify Plugin task and IntelliJ Plugin Verifier tool
- name: Run Plugin Verification tasks
run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }}
# Collect Plugin Verifier Result
- name: Collect Plugin Verifier Result
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: pluginVerifier-result
path: ${{ github.workspace }}/build/reports/pluginVerifier
# Run Qodana inspections
- name: Qodana - Code Inspection
uses: JetBrains/qodana-action@v2.1-eap
# Collect Qodana Result
- name: Collect Qodana Result
uses: actions/upload-artifact@v2
with:
name: qodana-result
path: ${{ github.workspace }}/qodana
# Prepare plugin archive content for creating artifact
- name: Prepare Plugin Artifact
id: artifact
shell: bash
run: |
cd ${{ github.workspace }}/build/distributions
FILENAME=`ls *.zip`
unzip "$FILENAME" -d content
echo "::set-output name=filename::$FILENAME"
# Store already-built plugin as an artifact for downloading
- name: Upload artifact
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ steps.artifact.outputs.filename }}
path: ./build/distributions/content/*/*
# Prepare a draft release for GitHub Releases page for the manual verification
# If accepted and published, release workflow would be triggered
releaseDraft:
name: Release Draft
if: github.event_name != 'pull_request'
needs: [build, verify]
needs: build
runs-on: ubuntu-latest
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
uses: actions/checkout@v2.4.0
# Remove old release drafts by using the curl request for the available releases with draft flag
- name: Remove Old Release Drafts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases \
| tr '\r\n' ' ' \
| jq '.[] | select(.draft == true) | .id' \
| xargs -I '{}' \
curl -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPOSITORY/releases/{}
gh api repos/{owner}/{repo}/releases \
--jq '.[] | select(.draft == true) | .id' \
| xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{}
# Create new release draft - which is not publicly visible and requires manual acceptance
- name: Create Release Draft
id: createDraft
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ needs.build.outputs.version }}
release_name: v${{ needs.build.outputs.version }}
body: ${{ needs.build.outputs.changelog }}
draft: true
# Download plugin artifact provided by the previous job
- name: Download Artifact
uses: actions/download-artifact@v2
with:
name: plugin-artifact
# Upload artifact as a release asset
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.createDraft.outputs.upload_url }}
asset_path: ./${{ needs.build.outputs.artifact }}
asset_name: ${{ needs.build.outputs.artifact }}
asset_content_type: application/zip
run: |
gh release create v${{ needs.build.outputs.version }} \
--draft \
--title "v${{ needs.build.outputs.version }}" \
--notes "$(cat << 'EOM'
${{ needs.build.outputs.changelog }}
EOM
)"

View File

@ -14,57 +14,66 @@ jobs:
runs-on: ubuntu-latest
steps:
# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
uses: actions/checkout@v2.4.0
with:
ref: ${{ github.event.release.tag_name }}
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Set environment variables
- name: Export Properties
id: properties
shell: bash
run: |
CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
${{ github.event.release.body }}
EOM
)"
echo "::set-output name=changelog::$CHANGELOG"
# Update Unreleased section with the current release note
- name: Patch Changelog
if: ${{ steps.properties.outputs.changelog != '' }}
run: |
./gradlew patchChangelog --release-note "$(cat << 'EOM'
${{ steps.properties.outputs.changelog }}
EOM
)"
# Publish the plugin to the Marketplace
- name: Publish Plugin
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: ./gradlew publishPlugin
# Patch changelog, commit and push to the current repository
changelog:
name: Update Changelog
needs: release
runs-on: ubuntu-latest
steps:
# Upload artifact as a release asset
- name: Upload Release Asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.tag_name }}
# Update Unreleased section with the current version
- name: Patch Changelog
run: ./gradlew patchChangelog
# Commit patched Changelog
- name: Commit files
# Create pull request
- name: Create Pull Request
if: ${{ steps.properties.outputs.changelog != '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -m "Update changelog" -a
# Push changes
- name: Push changes
uses: ad-m/github-push-action@master
with:
branch: main
github_token: ${{ secrets.GITHUB_TOKEN }}
VERSION="${{ github.event.release.tag_name }}"
BRANCH="changelog-update-$VERSION"
git config user.email "action@github.com"
git config user.name "GitHub Action"
git checkout -b $BRANCH
git commit -am "Changelog update - $VERSION"
git push --set-upstream origin $BRANCH
gh pr create \
--title "Changelog update - \`$VERSION\`" \
--body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
--base main \
--head $BRANCH

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

@ -0,0 +1,60 @@
# GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps:
# - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI
# - wait for IDE to start
# - run UI tests with separate Gradle task
#
# Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform
#
# Workflow is triggered manually.
name: Run UI Tests
on:
workflow_dispatch
jobs:
testUI:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
runIde: |
export DISPLAY=:99.0
Xvfb -ac :99 -screen 0 1920x1080x16 &
gradle runIdeForUiTests &
- os: windows-latest
runIde: start gradlew.bat runIdeForUiTests
- os: macos-latest
runIde: ./gradlew runIdeForUiTests &
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.4.0
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Run IDEA prepared for UI testing
- name: Run IDE
run: ${{ matrix.runIde }}
# Wait for IDEA to be started
- name: Health Check
uses: jtalk/url-health-check-action@v2
with:
url: http://127.0.0.1:8082
max-attempts: 15
retry-delay: 30s
# Run tests
- name: Tests
run: ./gradlew test

1
.gitignore vendored
View File

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

View File

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

View File

@ -11,7 +11,7 @@
</option>
<option name="taskNames">
<list>
<option value="check" />
<option value="test" />
</list>
</option>
<option name="vmOptions" value="" />

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

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

View File

@ -3,6 +3,20 @@
# easy-i18n Changelog
## [Unreleased]
### Added
- The search function now supports full-text-search
- Automatically reload translation data on file system change
- Sorting of translation keys can now be disabled via configuration
- Key section nesting can be disabled via configuration
- Numbers will be stored as number type whenever possible
- Code signing of plugin source
### Changed
- Better focus keys in tree-view after edit
- Optimized internal data structure (io, cache, events)
- Adjusted compatibility matrix to 2020.3 - 2021.3
- Updated dependencies and improved README file
## [1.5.1]
### Fixed
- Exception on key annotation if path-prefix is undefined

View File

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

View File

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

View File

@ -1,8 +0,0 @@
# Default detekt configuration:
# https://github.com/detekt/detekt/blob/master/detekt-core/src/main/resources/default-detekt-config.yml
formatting:
Indentation:
continuationIndentSize: 8
ParameterListWrapping:
indentSize: 8

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
example/images/settings.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -3,24 +3,29 @@
pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n
pluginVersion = 1.5.1
# SemVer format -> https://semver.org
pluginVersion = 1.6.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild = 202
pluginUntilBuild = 212.*
# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl
# See https://jb.gg/intellij-platform-builds-list for available build versions
pluginVerifierIdeVersions = 2020.2.4, 2020.3.4, 2021.2
pluginSinceBuild = 203
pluginUntilBuild = 213.*
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
platformType = IC
platformVersion = 2021.2
platformDownloadSources = true
platformVersion = 2020.3.4
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = org.jetbrains.kotlin
# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
javaVersion = 11
# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 7.3
# Opt-out flag for bundling Kotlin standard library.
# See https://kotlinlang.org/docs/reference/using-gradle.html#dependency-on-the-standard-library for details.
# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
# suppress inspection "UnusedProperty"
kotlin.stdlib.default.dependency = false

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
gradlew vendored
View File

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

6
qodana.yml Normal file
View File

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

View File

@ -0,0 +1,55 @@
package de.marhali.easyi18n;
import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
/**
* Data-bus which is used to distribute changes regarding translations or ui tools to the participating components.
* @author marhali
*/
public class DataBus {
private final Set<BusListener> listener;
protected DataBus() {
this.listener = new HashSet<>();
}
/**
* Adds a participant to the event bus. Every participant needs to be added manually.
* @param listener Bus listener
*/
public void addListener(BusListener listener) {
this.listener.add(listener);
}
/**
* Fires the called events on the returned prototype.
* The event will be distributed to all participants which were registered at execution time.
* @return Listener prototype
*/
public BusListener propagate() {
return new BusListener() {
@Override
public void onUpdateData(@NotNull TranslationData data) {
listener.forEach(li -> li.onUpdateData(data));
}
@Override
public void onFocusKey(@Nullable String key) {
listener.forEach(li -> li.onFocusKey(key));
}
@Override
public void onSearchQuery(@Nullable String query) {
listener.forEach(li -> li.onSearchQuery(query));
}
};
}
}

View File

@ -0,0 +1,118 @@
package de.marhali.easyi18n;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.*;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.io.json.JsonIOStrategy;
import de.marhali.easyi18n.io.json.ModularizedJsonIOStrategy;
import de.marhali.easyi18n.io.properties.PropertiesIOStrategy;
import de.marhali.easyi18n.io.yaml.YamlIOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.service.FileChangeListener;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Consumer;
/**
* Responsible for loading, saving and updating translation files.
* Provides access to the cached translation data which is used in the whole project.
* @author marhali
*/
public class DataStore {
private static final Set<IOStrategy> STRATEGIES = new LinkedHashSet<>(Arrays.asList(
new JsonIOStrategy(), new ModularizedJsonIOStrategy(),
new YamlIOStrategy("yaml"), new YamlIOStrategy("yml"),
new PropertiesIOStrategy()
));
private final @NotNull Project project;
private final @NotNull FileChangeListener changeListener;
private @NotNull TranslationData data;
protected DataStore(@NotNull Project project) {
this.project = project;
this.data = new TranslationData(true, true); // Initialize with hard-coded configuration
this.changeListener = new FileChangeListener(project);
VirtualFileManager.getInstance().addAsyncFileListener(
this.changeListener, Disposer.newDisposable("EasyI18n"));
}
public @NotNull TranslationData getData() {
return data;
}
/**
* Loads the translation data into cache and overwrites any previous cached data.
* If the configuration does not fit an empty translation instance will be populated.
* @param successResult Consumer will inform if operation was successful
*/
public void loadFromPersistenceLayer(@NotNull Consumer<Boolean> successResult) {
SettingsState state = SettingsService.getInstance(this.project).getState();
String localesPath = state.getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) { // Populate empty instance
this.data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
return;
}
this.changeListener.updateLocalesPath(localesPath);
IOStrategy strategy = this.determineStrategy(state, localesPath);
strategy.read(this.project, localesPath, state, (data) -> {
this.data = data == null
? new TranslationData(state.isSortKeys(), state.isNestedKeys())
: data;
successResult.accept(data != null);
});
}
/**
* Saves the cached translation data to the underlying io system.
* @param successResult Consumer will inform if operation was successful
*/
public void saveToPersistenceLayer(@NotNull Consumer<Boolean> successResult) {
SettingsState state = SettingsService.getInstance(this.project).getState();
String localesPath = state.getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) { // Cannot save without valid path
successResult.accept(false);
return;
}
IOStrategy strategy = this.determineStrategy(state, localesPath);
strategy.write(this.project, localesPath, state, this.data, successResult);
}
/**
* Chooses the right strategy for the opened project. An exception might be thrown on
* runtime if the project configuration (e.g. locale files does not fit in any strategy).
* @param state Plugin configuration
* @param localesPath Locales directory
* @return matching {@link IOStrategy}
*/
public @NotNull IOStrategy determineStrategy(@NotNull SettingsState state, @NotNull String localesPath) {
for(IOStrategy strategy : STRATEGIES) {
if(strategy.canUse(this.project, localesPath, state)) {
return strategy;
}
}
throw new IllegalArgumentException("Could not determine i18n strategy. " +
"At least one locale file must be defined. " +
"For examples please visit https://github.com/marhali/easy-i18n");
}
}

View File

@ -0,0 +1,76 @@
package de.marhali.easyi18n;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.TranslationUpdate;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Central singleton component for managing an easy-i18n instance for a specific project.
* @author marhali
*/
public class InstanceManager {
private static final Map<Project, InstanceManager> INSTANCES = new WeakHashMap<>();
private final DataStore store;
private final DataBus bus;
public static InstanceManager get(@NotNull Project project) {
InstanceManager instance = INSTANCES.get(project);
if(instance == null){
instance = new InstanceManager(project);
INSTANCES.put(project, instance);
}
return instance;
}
private InstanceManager(@NotNull Project project) {
this.store = new DataStore(project);
this.bus = new DataBus();
// Load data after first initialization
ApplicationManager.getApplication().invokeLater(() -> {
this.store.loadFromPersistenceLayer((success) -> {
this.bus.propagate().onUpdateData(this.store.getData());
});
});
}
public DataStore store() {
return this.store;
}
public DataBus bus() {
return this.bus;
}
public void processUpdate(TranslationUpdate update) {
if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation
this.store.getData().setTranslation(update.getOrigin().getKey(), null);
}
if(!update.isDeletion()) { // Create or re-create translation with changed data
this.store.getData().setTranslation(update.getChange().getKey(), update.getChange().getTranslation());
}
this.store.saveToPersistenceLayer(success -> {
if(success) {
this.bus.propagate().onUpdateData(this.store.getData());
if(!update.isDeletion()) {
this.bus.propagate().onFocusKey(update.getChange().getKey());
} else {
this.bus.propagate().onFocusKey(update.getOrigin().getKey());
}
}
});
}
}

View File

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

View File

@ -4,7 +4,7 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.NotNull;
@ -23,6 +23,9 @@ public class ReloadAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
DataStore.getInstance(e.getProject()).reloadFromDisk();
InstanceManager manager = InstanceManager.get(e.getProject());
manager.store().loadFromPersistenceLayer((success) -> {
manager.bus().propagate().onUpdateData(manager.store().getData());
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,12 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -52,10 +52,10 @@ public class KeyReference extends PsiReferenceBase<PsiElement> {
@Override
public void navigate(boolean requestFocus) {
LocalizedNode node = DataStore.getInstance(getProject()).getTranslations().getNode(getKey());
Translation translation = InstanceManager.get(getProject()).store().getData().getTranslation(getKey());
if(node != null) {
new EditDialog(getProject(), new KeyedTranslation(getKey(), node.getValue())).showAndHandle();
if(translation != null) {
new EditDialog(getProject(), new KeyedTranslation(getKey(), translation)).showAndHandle();
} else {
new AddDialog(getProject(), getKey()).showAndHandle();
}

View File

@ -4,8 +4,8 @@ import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
@ -38,7 +38,7 @@ public class GenericKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY;
}
if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
if(InstanceManager.get(element.getProject()).store().getData().getTranslation(value) == null) {
if(!KeyReference.isReferencable(value)) { // Creation policy
return PsiReference.EMPTY_ARRAY;
}

View File

@ -5,8 +5,8 @@ import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.service.DataStore;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
@ -45,7 +45,7 @@ public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY;
}
if(DataStore.getInstance(element.getProject()).getTranslations().getNode(value) == null) {
if(InstanceManager.get(element.getProject()).store().getData().getNode(value) == null) {
return PsiReference.EMPTY_ARRAY;
}

View File

@ -1,6 +1,7 @@
package de.marhali.easyi18n.util.array;
package de.marhali.easyi18n.io;
import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils;
import java.text.MessageFormat;
@ -10,11 +11,13 @@ import java.util.function.Function;
import java.util.regex.Pattern;
/**
* Utility methods for simple array support.
* Simple array support for translation values.
* Some i18n systems allows the user to define array values for some translations.
* We support array values by wrapping them into: '!arr[valueA;valueB]'.
*
* @author marhali
*/
public abstract class ArrayUtil {
public abstract class ArrayMapper {
static final String PREFIX = "!arr[";
static final String SUFFIX = "]";
static final char DELIMITER = ';';
@ -22,7 +25,7 @@ public abstract class ArrayUtil {
static final String SPLITERATOR_REGEX =
MessageFormat.format("(?<!\\\\){0}", Pattern.quote(String.valueOf(DELIMITER)));
static <T> String read(Iterator<T> elements, Function<T, String> stringFactory) {
protected static <T> String read(Iterator<T> elements, Function<T, String> stringFactory) {
StringBuilder builder = new StringBuilder(PREFIX);
int i = 0;
@ -43,7 +46,7 @@ public abstract class ArrayUtil {
return builder.toString();
}
static void write(String concat, Consumer<String> writeElement) {
protected static void write(String concat, Consumer<String> writeElement) {
concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length());
for(String element : concat.split(SPLITERATOR_REGEX)) {

View File

@ -0,0 +1,62 @@
package de.marhali.easyi18n.io;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
/**
* Primary interface for the exchange of translation data with the underlying IO system.
* The selection of the right IO strategy is done by the @canUse method (first match).
* Every strategy needs to be registered inside {@link de.marhali.easyi18n.DataStore}
*
* @author marhali
*/
public interface IOStrategy {
/**
* Decides whether this strategy should be applied or not. First matching one will be used.
* @param project IntelliJ project context
* @param localesPath Root directory which leads to all i18n files
* @param state Plugin configuration
* @return true if strategy is responsible for the found structure
*/
boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state);
/**
* Loads the translation files and passes them in the result consumer.
* Result payload might be null if operation failed.
* @param project IntelliJ project context
* @param localesPath Root directory which leads to all i18n files
* @param state Plugin configuration
* @param result Passes loaded data
*/
void read(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state,
@NotNull Consumer<@Nullable TranslationData> result);
/**
* Writes the provided translation data to the IO system.
* @param project InteliJ project context
* @param localesPath Root directory which leads to all i18n files
* @param state Plugin configuration
* @param data Translations to save
* @param result Indicates whether the operation was successful
*/
void write(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state,
@NotNull TranslationData data, @NotNull Consumer<Boolean> result);
/**
* Checks if the provided file should be processed for translation data
* @param state Plugin configuration
* @param file File to check
* @return true if file matches pattern
*/
default boolean isFileRelevant(@NotNull SettingsState state, @NotNull VirtualFile file) {
return file.getName().matches(state.getFilePattern());
}
}

View File

@ -1,35 +0,0 @@
package de.marhali.easyi18n.io;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.Translations;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
/**
* Interface to retrieve and save localized messages.
* Can be implemented by various standards. Such as JSON, Properties-Bundle and so on.
* @author marhali
*/
public interface TranslatorIO {
/**
* Reads localized messages from the persistence layer.
* @param project Opened intellij project
* @param directoryPath The full path for the directory which holds all locale files
* @param callback Contains loaded translations. Will be called after io operation. Content might be null on failure.
*/
void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback);
/**
* Writes the provided messages (translations) to the persistence layer.
* @param project Opened intellij project
* @param translations Translations instance to save
* @param directoryPath The full path for the directory which holds all locale files
* @param callback Will be called after io operation. Can be used to determine if action was successful(true) or not
*/
void save(@NotNull Project project, @NotNull Translations translations,
@NotNull String directoryPath, @NotNull Consumer<Boolean> callback);
}

View File

@ -1,100 +0,0 @@
package de.marhali.easyi18n.io.implementation;
import com.google.gson.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.util.IOUtil;
import de.marhali.easyi18n.util.JsonUtil;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.function.Consumer;
/**
* Implementation for JSON translation files.
* @author marhali
*/
public class JsonTranslatorIO implements TranslatorIO {
private static final String FILE_EXTENSION = "json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@Override
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
JsonObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(),
file.getCharset()), JsonObject.class);
JsonUtil.readTree(file.getNameWithoutExtension(), tree, nodes);
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
@Override
public void save(@NotNull Project project, @NotNull Translations translations,
@NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
JsonObject content = new JsonObject();
JsonUtil.writeTree(locale, content, translations.getNodes());
String fullPath = directoryPath + "/" + locale + "." + FILE_EXTENSION;
File file = new File(fullPath);
boolean created = file.createNewFile();
VirtualFile vf = created ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
}

View File

@ -1,121 +0,0 @@
package de.marhali.easyi18n.io.implementation;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.util.IOUtil;
import de.marhali.easyi18n.util.JsonUtil;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* IO operations for splitted / modularized json files. Each locale can have multiple translation files.
* @author marhali
*/
public class ModularizedJsonTranslatorIO implements TranslatorIO {
private static final String FILE_EXTENSION = "json";
@Override
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] localeDirectories = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile localeDir : localeDirectories) {
String locale = localeDir.getName();
locales.add(locale);
// Read all json modules
for(VirtualFile module : localeDir.getChildren()) {
if(!IOUtil.isFileRelevant(project, module)) { // File does not matches pattern
continue;
}
JsonObject tree = JsonParser.parseReader(new InputStreamReader(module.getInputStream(),
module.getCharset())).getAsJsonObject();
String moduleName = module.getNameWithoutExtension();
LocalizedNode moduleNode = nodes.getChildren(moduleName);
if(moduleNode == null) { // Create module / sub node
moduleNode = new LocalizedNode(moduleName, new ArrayList<>());
nodes.addChildren(moduleNode);
}
JsonUtil.readTree(locale, tree, moduleNode);
}
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
@Override
public void save(@NotNull Project project, @NotNull Translations translations,
@NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
// Use top level children as modules
for (LocalizedNode module : translations.getNodes().getChildren()) {
JsonObject content = new JsonObject();
JsonUtil.writeTree(locale, content, module);
String fullPath = directoryPath + "/" + locale + "/" + module.getKey() + "." + FILE_EXTENSION;
File file = new File(fullPath);
boolean created = file.createNewFile();
VirtualFile vf = created ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
vf.setBinaryContent(gson.toJson(content).getBytes(vf.getCharset()));
}
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
}

View File

@ -1,136 +0,0 @@
package de.marhali.easyi18n.io.implementation;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.TranslatorIO;
import de.marhali.easyi18n.model.LocalizedNode;
import de.marhali.easyi18n.model.Translations;
import de.marhali.easyi18n.util.IOUtil;
import de.marhali.easyi18n.util.SortedProperties;
import de.marhali.easyi18n.util.StringUtil;
import de.marhali.easyi18n.util.TranslationsUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.*;
import java.util.function.Consumer;
/**
* Implementation for properties translation files.
* @author marhali
*/
public class PropertiesTranslatorIO implements TranslatorIO {
public static final String FILE_EXTENSION = "properties";
@Override
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for (VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
SortedProperties properties = new SortedProperties();
properties.load(new InputStreamReader(file.getInputStream(), file.getCharset()));
readProperties(file.getNameWithoutExtension(), properties, nodes);
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
@Override
public void save(@NotNull Project project, @NotNull Translations translations,
@NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
SortedProperties properties = new SortedProperties();
writeProperties(locale, properties, translations.getNodes(), "");
String fullPath = directoryPath + "/" + locale + "." + FILE_EXTENSION;
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
StringWriter content = new StringWriter();
properties.store(content, "I18n " + locale + " keys");
file.setBinaryContent(content.toString().getBytes(file.getCharset()));
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
private void writeProperties(String locale, Properties props, LocalizedNode node, String parentPath) {
if(node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
if(node.getValue().get(locale) != null) { // Translation is defined - track it
String value = StringEscapeUtils.unescapeJava(node.getValue().get(locale));
props.setProperty(parentPath, value);
}
} else {
for(LocalizedNode children : node.getChildren()) {
writeProperties(locale, props, children,
parentPath + (parentPath.isEmpty() ? "" : ".") + children.getKey());
}
}
}
private void readProperties(String locale, Properties props, LocalizedNode parent) {
props.forEach((key, value) -> {
List<String> sections = TranslationsUtil.getSections(String.valueOf(key));
LocalizedNode node = parent;
for (String section : sections) {
LocalizedNode subNode = node.getChildren(section);
if(subNode == null) {
subNode = new LocalizedNode(section, new ArrayList<>());
node.addChildren(subNode);
}
node = subNode;
}
Map<String, String> messages = node.getValue();
String escapedValue = StringUtil.escapeControls(String.valueOf(value), true);
messages.put(locale, escapedValue);
node.setValue(messages);
});
}
}

View File

@ -1,125 +0,0 @@
package de.marhali.easyi18n.io.implementation;
import com.intellij.openapi.application.*;
import com.intellij.openapi.project.*;
import com.intellij.openapi.vfs.*;
import de.marhali.easyi18n.io.*;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.util.*;
import de.marhali.easyi18n.util.array.YamlArrayUtil;
import org.jetbrains.annotations.*;
import thito.nodeflow.config.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
public class YamlTranslatorIO implements TranslatorIO {
@Override
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
}
VirtualFile[] files = directory.getChildren();
List<String> locales = new ArrayList<>();
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
try {
for(VirtualFile file : files) {
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
continue;
}
locales.add(file.getNameWithoutExtension());
try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
Section section = Section.parseToMap(reader);
load(file.getNameWithoutExtension(), nodes, section);
}
}
callback.accept(new Translations(locales, nodes));
} catch(IOException e) {
e.printStackTrace();
callback.accept(null);
}
});
}
private void load(String locale, LocalizedNode node, Section section) {
if (section instanceof MapSection) {
for (String key : section.getKeys()) {
LocalizedNode child = node.getChildren(key);
if (child == null) {
node.addChildren(child = new LocalizedNode(key, new ArrayList<>()));
}
LocalizedNode finalChild = child;
MapSection map = section.getMap(key).orElse(null);
if (map != null) {
load(locale, finalChild, map);
} else {
if(section.isList(key) && section.getList(key).isPresent()) {
child.getValue().put(locale, YamlArrayUtil.read(section.getList(key).get()));
} else {
String value = section.getString(key).orElse(null);
if (value != null) {
child.getValue().put(locale, value);
}
}
}
}
}
}
private void save(LocalizedNode node, String locale, Section section, String path) {
if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
String value = node.getValue().get(locale);
if (value != null) {
section.set(path, YamlArrayUtil.isArray(value) ? YamlArrayUtil.write(value) : value);
}
} else {
for (LocalizedNode child : node.getChildren()) {
save(child, locale, section, path == null ? child.getKey() : path + "." + child.getKey());
}
}
}
@Override
public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : translations.getLocales()) {
Section section = new MapSection();
save(translations.getNodes(), locale, section, null);
String fullPath = directoryPath + "/" + locale + ".yml";
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
file.setBinaryContent(Section.toString(section).getBytes(file.getCharset()));
}
// Successfully saved
callback.accept(true);
} catch(IOException e) {
e.printStackTrace();
callback.accept(false);
}
});
}
}

View File

@ -1,13 +1,14 @@
package de.marhali.easyi18n.util.array;
package de.marhali.easyi18n.io.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import de.marhali.easyi18n.io.ArrayMapper;
/**
* Utility methods to read and write json arrays.
* Map json array values.
* @author marhali
*/
public class JsonArrayUtil extends ArrayUtil {
public class JsonArrayMapper extends ArrayMapper {
public static String read(JsonArray array) {
return read(array.iterator(), JsonElement::getAsString);
}

View File

@ -0,0 +1,118 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Consumer;
/**
* Strategy for simple json locale files. Each locale has its own file.
* For example localesPath/en.json, localesPath/de.json.
* @author marhali
*/
public class JsonIOStrategy implements IOStrategy {
private static final String FILE_EXTENSION = "json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@Override
public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
return false;
}
for(VirtualFile children : directory.getChildren()) {
if(!children.isDirectory() && isFileRelevant(state, children)) {
if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
return true;
}
}
}
return false;
}
@Override
public void read(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
}
TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
try {
for(VirtualFile file : directory.getChildren()) {
if(file.isDirectory() || !isFileRelevant(state, file)) {
continue;
}
String locale = file.getNameWithoutExtension();
data.addLocale(locale);
JsonObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(), file.getCharset()),
JsonObject.class);
JsonMapper.read(locale, tree, data.getRootNode());
}
result.accept(data);
} catch(IOException e) {
e.printStackTrace();
result.accept(null);
}
});
}
@Override
public void write(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
JsonObject content = new JsonObject();
JsonMapper.write(locale, content, data.getRootNode());
File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION);
boolean exists = file.createNewFile();
VirtualFile vf = exists
? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
}
result.accept(true);
} catch(IOException e) {
e.printStackTrace();
result.accept(false);
}
});
}
}

View File

@ -0,0 +1,73 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.util.Map;
/**
* Mapper for mapping json objects into translation nodes and backwards.
* @author marhali
*/
public class JsonMapper {
public static void read(String locale, JsonObject json, TranslationNode node) {
for(Map.Entry<String, JsonElement> entry : json.entrySet()) {
String key = entry.getKey();
JsonElement value = entry.getValue();
TranslationNode childNode = node.getOrCreateChildren(key);
if(value.isJsonObject()) {
// Nested element - run recursively
read(locale, value.getAsJsonObject(), childNode);
} else {
Translation translation = childNode.getValue();
String content = entry.getValue().isJsonArray()
? JsonArrayMapper.read(value.getAsJsonArray())
: StringUtil.escapeControls(value.getAsString(), true);
translation.put(locale, content);
childNode.setValue(translation);
}
}
}
public static void write(String locale, JsonObject json, TranslationNode node) {
for(Map.Entry<String, TranslationNode> entry : node.getChildren().entrySet()) {
String key = entry.getKey();
TranslationNode childNode = entry.getValue();
if(!childNode.isLeaf()) {
// Nested node - run recursively
JsonObject childJson = new JsonObject();
write(locale, childJson, childNode);
if(childJson.size() > 0) {
json.add(key, childJson);
}
} else {
Translation translation = childNode.getValue();
String content = translation.get(locale);
if(content != null) {
if(JsonArrayMapper.isArray(content)) {
json.add(key, JsonArrayMapper.write(content));
} else if(NumberUtils.isNumber(content)) {
json.add(key, new JsonPrimitive(NumberUtils.createNumber(content)));
} else {
json.add(key, new JsonPrimitive(StringEscapeUtils.unescapeJava(content)));
}
}
}
}
}
}

View File

@ -0,0 +1,149 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;
/**
* Strategy for distributed json files per locale. Each locale can have multiple modules. The file name
* of each module will be used as the key for the underlying translations. <br/>
* Full key example: <moduleFileName>.<username>.<title>
*
* @author marhali
*/
public class ModularizedJsonIOStrategy implements IOStrategy {
private static final String FILE_EXTENSION = "json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@Override
public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
return false;
}
// We expect something like this:
// <localesPath>/<localeDir>/<moduleFile>
for(VirtualFile children : directory.getChildren()) {
if(children.isDirectory()) { // Contains module folders
for(VirtualFile moduleFile : children.getChildren()) {
if(!moduleFile.isDirectory() && isFileRelevant(state, moduleFile)) {
if(moduleFile.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
return true;
}
}
}
}
}
return false;
}
@Override
public void read(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
}
TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
VirtualFile[] localeDirectories = directory.getChildren();
try {
for(VirtualFile localeDir : localeDirectories) {
String locale = localeDir.getNameWithoutExtension();
data.addLocale(locale);
// Read all underlying module files
for(VirtualFile module : localeDir.getChildren()) {
if(module.isDirectory() || !isFileRelevant(state, module)) {
continue;
}
String moduleName = module.getNameWithoutExtension();
TranslationNode moduleNode = data.getNode(moduleName) != null
? data.getNode(moduleName)
: new TranslationNode(state.isSortKeys() ? new TreeMap<>() : new LinkedHashMap<>());
JsonObject tree = GSON.fromJson(new InputStreamReader(module.getInputStream(),
module.getCharset()), JsonObject.class);
JsonMapper.read(locale, tree, moduleNode);
data.getRootNode().setChildren(moduleName, moduleNode);
}
}
result.accept(data);
} catch(IOException e) {
e.printStackTrace();
result.accept(null);
}
});
}
// TODO: there will be problems when adding translations via TranslationData with non-nested key mode
@Override
public void write(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
for(Map.Entry<String, TranslationNode> entry : data.getRootNode().getChildren().entrySet()) {
String module = entry.getKey();
JsonObject content = new JsonObject();
JsonMapper.write(locale, content, entry.getValue());
String fullPath = localesPath + "/" + locale + "/" + module + "." + FILE_EXTENSION;
File file = new File(fullPath);
boolean exists = file.createNewFile();
VirtualFile vf = exists
? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset()));
}
}
result.accept(true);
} catch(IOException e) {
e.printStackTrace();
result.accept(false);
}
});
}
}

View File

@ -0,0 +1,23 @@
package de.marhali.easyi18n.io.properties;
import de.marhali.easyi18n.io.ArrayMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Map for 'properties' array values.
* @author marhali
*/
public class PropertiesArrayMapper extends ArrayMapper {
public static String read(String[] list) {
return read(Arrays.stream(list).iterator(), Object::toString);
}
public static String[] write(String concat) {
List<String> list = new ArrayList<>();
write(concat, list::add);
return list.toArray(new String[0]);
}
}

View File

@ -0,0 +1,116 @@
package de.marhali.easyi18n.io.properties;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.function.Consumer;
/**
* Strategy for simple 'properties' locale files. Each locale has its own file.
* For example localesPath/en.properties, localesPath/de.properties.
* @author marhali
*/
public class PropertiesIOStrategy implements IOStrategy {
private static final String FILE_EXTENSION = "properties";
@Override
public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
return false;
}
for(VirtualFile children : directory.getChildren()) {
if(!children.isDirectory() && isFileRelevant(state, children)) {
if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
return true;
}
}
}
return false;
}
@Override
public void read(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
}
TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
try {
for(VirtualFile file : directory.getChildren()) {
if(file.isDirectory() || !isFileRelevant(state, file)) {
continue;
}
String locale = file.getNameWithoutExtension();
data.addLocale(locale);
SortableProperties properties = new SortableProperties(state.isSortKeys());
properties.load(new InputStreamReader(file.getInputStream(), file.getCharset()));
PropertiesMapper.read(locale, properties, data);
}
result.accept(data);
} catch(IOException e) {
e.printStackTrace();
result.accept(null);
}
});
}
@Override
public void write(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
SortableProperties properties = new SortableProperties(state.isSortKeys());
PropertiesMapper.write(locale, properties, data);
File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION);
boolean exists = file.createNewFile();
VirtualFile vf = exists
? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
StringWriter writer = new StringWriter();
properties.store(writer, null);
vf.setBinaryContent(writer.toString().getBytes(vf.getCharset()));
}
result.accept(true);
} catch(IOException e) {
e.printStackTrace();
result.accept(false);
}
});
}
}

View File

@ -0,0 +1,55 @@
package de.marhali.easyi18n.io.properties;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.math.NumberUtils;
import java.util.Map;
/**
* Mapper for mapping properties files into translation nodes and backwards.
* @author marhali
*/
public class PropertiesMapper {
public static void read(String locale, SortableProperties properties, TranslationData data) {
for(Map.Entry<Object, Object> entry : properties.entrySet()) {
String key = String.valueOf(entry.getKey());
Object value = entry.getValue();
Translation translation = data.getTranslation(key);
if(translation == null) {
translation = new Translation();
}
String content = value instanceof String[]
? PropertiesArrayMapper.read((String[]) value)
: StringUtil.escapeControls(String.valueOf(value), true);
translation.put(locale, content);
data.setTranslation(key, translation);
}
}
public static void write(String locale, SortableProperties properties, TranslationData data) {
for(String key : data.getFullKeys()) {
Translation translation = data.getTranslation(key);
if(translation != null && translation.containsKey(locale)) {
String content = translation.get(locale);
if(PropertiesArrayMapper.isArray(content)) {
properties.put(key, PropertiesArrayMapper.write(content));
} else if(NumberUtils.isNumber(content)) {
properties.put(key, NumberUtils.createNumber(content));
} else {
properties.put(key, content);
}
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,121 @@
package de.marhali.easyi18n.io.yaml;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import thito.nodeflow.config.MapSection;
import thito.nodeflow.config.Section;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.function.Consumer;
/**
* Strategy for simple yaml locale files. Each locale has its own file.
* For example localesPath/en.y(a)ml, localesPath/de.y(a)ml
* @author marhali
*/
public class YamlIOStrategy implements IOStrategy {
private final String FILE_EXTENSION;
public YamlIOStrategy(@NotNull String fileExtension) {
this.FILE_EXTENSION = fileExtension;
}
@Override
public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
return false;
}
for(VirtualFile children : directory.getChildren()) {
if(!children.isDirectory() && isFileRelevant(state, children)) {
if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) {
return true;
}
}
}
return false;
}
@Override
public void read(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
}
TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
try {
for(VirtualFile file : directory.getChildren()) {
if(file.isDirectory() || !isFileRelevant(state, file)) {
continue;
}
String locale = file.getNameWithoutExtension();
data.addLocale(locale);
try(Reader reader = new InputStreamReader(file.getInputStream(), file.getCharset())) {
Section section = Section.parseToMap(reader);
YamlMapper.read(locale, section, data.getRootNode());
}
}
result.accept(data);
} catch(IOException e) {
e.printStackTrace();
result.accept(null);
}
});
}
@Override
public void write(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer<Boolean> result) {
ApplicationManager.getApplication().runWriteAction(() -> {
try {
for(String locale : data.getLocales()) {
Section section = new MapSection();
YamlMapper.write(locale, section, data.getRootNode());
File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION);
boolean exists = file.createNewFile();
VirtualFile vf = exists
? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)
: LocalFileSystem.getInstance().findFileByIoFile(file);
vf.setBinaryContent(Section.toString(section).getBytes(vf.getCharset()));
}
result.accept(true);
} catch(IOException e) {
e.printStackTrace();
result.accept(false);
}
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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