diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f90255f6..7da1304a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -17,6 +17,8 @@ jobs: build-jar: name: "Build JAR" runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v4 name: "Checkout" @@ -29,7 +31,11 @@ jobs: - name: "Build JAR" run: mvn -DskipTests -Pfatjar clean package - name: 'Get version' - run: echo "VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV + id: get-version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: 'Upload artifact' uses: actions/upload-artifact@v4 with: @@ -56,11 +62,12 @@ jobs: label: 'linux-arm64' filename: 'simplelocalize-cli' downloadFilename: 'simplelocalize-cli-linux-arm64' - - os: 'macos-13' + # https://github.blog/changelog/2026-02-26-macos-26-is-now-generally-available-for-github-hosted-runners/ + - os: 'macos-26-intel' label: 'mac' filename: 'simplelocalize-cli' downloadFilename: 'simplelocalize-cli-mac' - - os: 'macos-14' + - os: 'macos-26' label: 'mac-arm64' filename: 'simplelocalize-cli' downloadFilename: 'simplelocalize-cli-mac-arm64' @@ -99,3 +106,30 @@ jobs: files: './target/${{ matrix.downloadFilename }}' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + bundle-release: + name: "Bundle all files into a single ZIP" + needs: [ build-jar, build-executable ] + runs-on: ubuntu-latest + steps: + - name: "Download all artifacts" + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - name: "Create ZIP archive" + run: | + cd dist + zip -j "../simplelocalize-cli-${{ needs.build-jar.outputs.version }}.zip" * + - name: "Upload bundle artifact" + uses: actions/upload-artifact@v4 + with: + name: 'simplelocalize-cli-${{ needs.build-jar.outputs.version }}' + path: 'simplelocalize-cli-${{ needs.build-jar.outputs.version }}.zip' + - name: 'Upload bundle to Release' + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: 'simplelocalize-cli-${{ needs.build-jar.outputs.version }}.zip' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index cfa38921..5f893538 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,26 @@ simplelocalize upload [GitHub Wiki: Configuration file](/simplelocalize/simplelocalize-cli/wiki/Configuration-file) +### Language mapping + +The `mappings.lang` option (available only in the YAML configuration file) maps SimpleLocalize language keys +(`languageKey`) to the language names used in the file system (`placeholder`, the `{lang}` placeholder). Unlike a +generic `source`/`target` pair, these names stay unambiguous regardless of direction: on `download` a SimpleLocalize +language key is translated to the file system language name, and on `upload` the file system language name is +translated back to the SimpleLocalize language key. This lets you customize where files are downloaded, for example +to download `en` into `/values/` instead of `/values-en/`. + +```yaml +downloadPath: ./res/values{lang}/strings.xml +downloadFormat: android +mappings: + lang: + - languageKey: "en" + placeholder: "" # download 'en' to ./res/values + - languageKey: "de" + placeholder: "-de" # download 'de' to ./res/values-de +``` + ## Proxy support SimpleLocalize CLI supports HTTP and HTTPS proxies, and it respects the `http_proxy`, `https_proxy` environment diff --git a/junit/download-retry/file.json b/junit/download-retry/file.json new file mode 100644 index 00000000..c486ff24 --- /dev/null +++ b/junit/download-retry/file.json @@ -0,0 +1 @@ +{ "key": "value" } \ No newline at end of file diff --git a/junit/download-test/file.json b/junit/download-test/file.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/junit/download-test/file.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/junit/lang-region-codes/en.json b/junit/lang-region-codes/en.json new file mode 100644 index 00000000..7099fd25 --- /dev/null +++ b/junit/lang-region-codes/en.json @@ -0,0 +1,3 @@ +{ + "title": "English" +} diff --git a/junit/lang-region-codes/es_419.json b/junit/lang-region-codes/es_419.json new file mode 100644 index 00000000..bd8c90b4 --- /dev/null +++ b/junit/lang-region-codes/es_419.json @@ -0,0 +1,3 @@ +{ + "title": "Spanish (Latin America)" +} diff --git a/junit/lang-region-codes/pt-BR.json b/junit/lang-region-codes/pt-BR.json new file mode 100644 index 00000000..c506673d --- /dev/null +++ b/junit/lang-region-codes/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Portuguese (Brazil)" +} diff --git a/junit/lang-region-codes/zh-Hant-TW.json b/junit/lang-region-codes/zh-Hant-TW.json new file mode 100644 index 00000000..58a54d85 --- /dev/null +++ b/junit/lang-region-codes/zh-Hant-TW.json @@ -0,0 +1,3 @@ +{ + "title": "Chinese (Traditional, Taiwan)" +} diff --git a/junit/mock-server/test.json b/junit/mock-server/test.json index eee0e58b..be8aef11 100644 --- a/junit/mock-server/test.json +++ b/junit/mock-server/test.json @@ -1,4 +1,4 @@ -Translation Management for software projects · SimpleLocalize

Translation management
for software projects

Elegant and simple web-based translation editor. It helps small and growing teams save time on handling localization files and translation strings.

Manage my translations
No credit card required14-day free trialAI-powered
SimpleLocalize translation editor in side-by-side view
  • Auto-translation
  • Screenshots with OCR
  • AI-powered adjustments
  • Built-in Automations
  • Markdown support
  • Variables highlighting
  • Bulk Actions
  • Context-aware translations
  • Acceptance statuses
  • Customizable view
  • Spreadsheet view
  • Text summaries

Auto-translation

Translate your application into multiple languages with just a few clicks. Choose from OpenAI ChatGPT, Google Translate or DeepL translation providers to translate your texts. Adding support for new languages has never been easier.

Learn more about auto-translation
Auto-translation tab in SimpleLocalize

Translation Editor

Online translation editor that helps you to manage your translations in one place. With numerous features like auto-translation, comments, and notifications, you can easily collaborate with your team and keep your translations up to date.

See translation editor features
SimpleLocalize Translation Editor

Command-line tool

With SimpleLocalize CLI you can manage your translations from the terminal. It's a powerful tool that helps you to automate the translation process in your project. You can easily synchronize translation files between you local project and SimpleLocalize Translation Editor, start auto-translation or publish changes to the production environment.

CLI documentation
Terminal
# upload strings
+            
GitHubGitHub integration is now available!|Install now

Translation management
for software projects

A simple, elegant web-based translation editor for software projects that helps small teams manage translations and keep content in sync.

Get started
No credit card required14-day free trialTracking-free service
SimpleLocalize - translation editor

We power your product localization
From first commit to global launch

Used at IntersportUsed at IKEAUsed at InvisalignUsed at AmdocsUsed at OSRAMUsed at project44Used at nglUsed at IQAirUsed at walcuUsed at UNICEFUsed at AstraZenecaUsed at nabooUsed at SagemathUsed at Brite

What is SimpleLocalize?
It's a platform for managing translations and automating localization workflows.

It simplifies complex localization workflows, keeps your content in sync across all products, and makes continuous localization easy, without unnecessary complexity.

Continuous localization

All-in-one localization platform

Manage translations and automate workflows
across all your digital products

AI Auto-translation

Auto-translation, uses AI and traditional machine translation to instantly convert your content into multiple languages, saving time, reducing costs, and simplifying localization.

Auto-translation in SimpleLocalize

No-code automations

Automations set up in minutes, translations that run themselves, keeping your content always on time and perfectly in sync.

Translation automations in SimpleLocalize

User-generated translations

Crowdsourced translations: let your users, community, and contributors translate your content, keeping it on-brand.

Crowdsource translations in SimpleLocalize

Content Delivery Network

Translation Hosting that keeps your content always available, always up to date, and new translations delivered at lightning speed.

Translation hosting in SimpleLocalize

Universal command-line tool

Localization CLI makes managing your translations more configurable, flexible, and automated than ever before.

Terminal
# upload source translations +$ simplelocalize upload + +# auto-translate strings +$ simplelocalize auto-translate + +# publish translations +$ simplelocalize publish + +# download translated files +$ simplelocalize download
# upload source translations
 $ simplelocalize upload
 
 # auto-translate strings
 $ simplelocalize auto-translate
 
-# download all translations
-$ simplelocalize download

Translation Hosting

Get your translations lightning fast with our CDN. We provide a simple way to host your translations and serve them safely to your users.

Get started with translation hosting
cdn.simplelocalize.io
{
-  "en": {
-    "CREATE_ACCOUNT": "Create account",
-    "SIGN_IN": "Sign in",
-    "WELCOME_TO_THE_JUNGLE" : "Welcome to the jungle"
-  },
-  "es": {
-    "CREATE_ACCOUNT": "Crear cuenta",
-    "SIGN_IN": "Iniciar",
-    "WELCOME_TO_THE_JUNGLE": "Bienvenido a la jungla"
-  }
-}
-

Better oversight

Team analytics

Managers can see an in-depth overview of team activity

Team analytics

Activity tracking

Understand how your team is progressing with translations

Activity tracking
Coming soon

Language analytics

Understand which languages are most popular with your users

Language analytics

Why SimpleLocalize?

Most translation editors are overkill for most teams. SimpleLocalize is easy
to use and understand with no training or prior experience.

Simple and customizable

SimpleLocalize is easy to get started with and focuses on the features that you and your client need the most. You can customize the editor to your needs choosing from a variety of translation editor options.

Flat pricing

No hidden costs at any scale. SimpleLocalize offers a simple and affordable plans for you and your projects. Within easy integration options and intuitive translation editor, it creates a wonderful selection.

Team members

Every paid plan comes with a number of users you can add to your project. You don't need to worry about number of users as the price does not increase with every user.

AI & Auto-translation included

Every paid plan comes with a number of auto-translation characters that are added to your account every month. You can use them to translate your project without any additional costs. You can increase the limit at any time.

Tools for Developers

SimpleLocalize offers a command-line tool, REST API and Webhooks for developers to easily integrate their projects no matter which plan you choose. It enables you to make automations to synchronize translations with your projects.

Personal support

From “how do I get started” to “I need XYZ” requests, when you need that extra helping hand, we’re here for you. We are providing personal support to ensure your experience with SimpleLocalize is as smooth as possible.

A man smiling for a picture.
"Super EASY to use, great documentation to connect through API.
Very nice dashboard to manage translators and their work.
I personally really do like the export/import of excel files
where I can update translations key en masse"
Marcin Krzaczkowski Managing Director
companycompany
"The product
and support
are fantastic."
Laars Buur CTO
"The support is
blazing fast,
thank you Jakub!"
Stefan Developer
"Interface that
makes any dev
feel at home!"
Dario De Cianni CTO
"Excellent app,
saves my time
and money"
Dmitry Melnik Developer

Trusted by many

Our Translation Hosting is used by over 100 million people every month.

Analytics
simplelocalize cloudflare request count
  • Amazon S3
  • Azure Blob Storage
  • Google Cloud Storage
  • Figma
  • Cloudflare Workers
  • GitHub
  • GitLab
  • Bitbucket
  • Android
  • iOS
  • macOS
  • i18next
  • next-i18next
  • next-translate
  • FormatJS
  • FormatJS CLI
  • Laravel
  • Vue
  • React
  • Angular
  • GatsbyJS
  • Unity
  • React Native
  • Expo
  • Ionic
  • Cordova
  • Spring Boot
  • Ruby on Rails
  • NodeJS
  • Swift language
  • Java language
  • Kotlin language
  • Jekyll
  • JavaScript
  • TypeScript
  • OpenAI
  • DeepL
  • Webhooks
  • Google Translate
  • Excel
  • GitHub
  • Slack
  • Discord
  • Microsoft Teams
  • Microsoft Teams
  • Discord
  • Slack
  • GitHub
  • Excel
  • Google Translate
  • Webhooks
  • DeepL
  • OpenAI
  • TypeScript
  • JavaScript
  • Jekyll
  • Kotlin language
  • Java language
  • Swift language
  • NodeJS
  • Ruby on Rails
  • Spring Boot
  • Cordova
  • Ionic
  • Expo
  • React Native
  • Unity
  • GatsbyJS
  • Angular
  • React
  • Vue
  • Laravel
  • FormatJS CLI
  • FormatJS
  • next-translate
  • next-i18next
  • i18next
  • macOS
  • iOS
  • Android
  • Bitbucket
  • GitLab
  • GitHub
  • Cloudflare Workers
  • Figma
  • Google Cloud Storage
  • Azure Blob Storage
  • Amazon S3
  • OpenAI
  • DeepL
  • Webhooks
  • Google Translate
  • Excel
  • GitHub
  • Slack
  • Discord
  • Microsoft Teams
  • Amazon S3
  • Azure Blob Storage
  • Google Cloud Storage
  • Figma
  • Cloudflare Workers
  • GitHub
  • GitLab
  • Bitbucket
  • Android
  • iOS
  • macOS
  • i18next
  • next-i18next
  • next-translate
  • FormatJS
  • FormatJS CLI
  • Laravel
  • Vue
  • React
  • Angular
  • GatsbyJS
  • Unity
  • React Native
  • Expo
  • Ionic
  • Cordova
  • Spring Boot
  • Ruby on Rails
  • NodeJS
  • Swift language
  • Java language
  • Kotlin language
  • Jekyll
  • JavaScript
  • TypeScript

Integrated with your favourite tools

Have you already integrated i18n library? You can now connect SimpleLocalize without doing much changes in the application code. We don't bound you with our own 'yet another i18n library'.

Browse integrations
Ready to say
|

Greet your customers
in their mother language

Start 14-day trial
No credit card required
country flags

Latest news

Stay up to date with the latest news

Namespaces in software localization

Namespaces in software localization

Kinga Pomykała
6 min read

Namespaces are a great way to organize translations in software localization. Learn what namespaces are, why they are important, and how to use them in your localization process.

Introducing: Statistics

Introducing: Statistics

Kinga Pomykała
3 min read

Check your project's translation progress with the new statistics feature in SimpleLocalize. Monitor progress, untranslated keys, review status, and team performance.

Introducing: Translation comments

Introducing: Translation comments

Kinga Pomykała
3 min read

Communicate with your team members and translators using comments. Learn how to use comments feature in the translation management system.

\ No newline at end of file +# publish translations +$ simplelocalize publish + +# download translated files +$ simplelocalize download

Zero-dependency required.
Works natively on Windows, macOS, and Linux.

How it works?

Import, auto-translate, and export translations
for your app or host them in the cloud.

how simplelocalize helps you in your translation workflow

Localization solutions
for modern dev teams

From startup MVPs to enterprise ecosystems, SimpleLocalize provides the infrastructure to manage, translate, and deliver your content at scale.

Mobile App Localization

Automate iOS, Android, and Flutter localization. Manage ARB, Strings, and XML files with ease via CLI or GitHub App.

Website Localization

Translate your website with full international SEO support. Works with Next.js, Gatsby, Nuxt, Shopify, Webflow, and any headless CMS.

Software & Web Frameworks

Easy integration with React, Next.js, Vue, and Angular. Sync your translation files directly via CLI or GitHub Actions.

AI-Powered Automation

Scale faster with DeepL, OpenAI and local LLM integrations. Auto-translate new keys instantly and keep your product multi-lingual on autopilot.

SimpleLocalize connects your code to professional translations

Why SimpleLocalize?

Most tools are overkill.
SimpleLocalize is fast, simple, and works out of the box.

Fast and customizable

SimpleLocalize is easy to get started with and focuses on the features that you and your team need the most. You can customize the editor to your needs choosing from a variety of options.

Flat pricing

No hidden costs! SimpleLocalize offers a simple and affordable plans for you and your projects. Within easy integration options and intuitive translation editor, it creates a wonderful selection.

Team members included

Every paid plan comes with a number of users you can add to your project. You don't need to worry about number of users as the price does not increase with every user.

Auto-translation included

Every paid plan comes with auto-translation characters that you receive every month. You can also use bring your own API keys and use them with SimpleLocalize in any plan (even free!).

Tools for Developers

SimpleLocalize gives developers the tools they need to automate and integrate localization into any workflow, such as CLI tool, VS Code extension, IntelliJ plugin, Figma integration, and more.

Personal support

From “how do I get started” to “I need XYZ” requests, when you need that extra helping hand, we’re here for you. We are here to make sure you get the most out of SimpleLocalize.

Customers

Behind every multilingual product,
there's a story of cooperation.

Marcin Krzaczkowski
Marcin Krzaczkowski | Managing Director

"Super EASY to use, great documentation to connect through API. +Very nice dashboard to manage translators and their work. +I personally really do like the export/import of Excel files +where I can update translations key en masse"

Marcin Krzaczkowski
Harald Schilly
Harald Schilly | CTO at Sagemath, Inc.

"Without Simplelocalize, it would not have been possible to offer that many languages. We strived to support all major languages, where we also know that there are users."

Harald Schilly
Víctor Gutiérrez
Víctor Gutiérrez | Co-founder and CEO

"The translation process is much more efficient. There are fewer errors, and we no longer have to worry about missing translations. Everything is managed from within the application itself, which makes it straightforward and efficient for our team."

Víctor Gutiérrez

Get started with SimpleLocalize

  • All-in-one localization platform
  • Web-based translation editor for your team
  • Auto-translation, QA-checks, AI and more
  • See how easily you can start localizing your product.
  • Powerful API, hosting, integrations and developer tools
  • Unmatched customer support
Start for free
No credit card required5-minute setup
"The product
and support
are fantastic."
Laars Buur|CTO
"The support is
blazing fast,
thank you Jakub!"
Stefan|Developer
"Interface that
makes any dev
feel at home!"
Dario De Cianni|CTO
"Excellent app,
saves my time
and money"
Dmitry Melnik|Developer

Latest news

See what we're up to
and get tips on localization

Python and Django i18n: A practical guide for developers

How Django's built-in i18n system works: gettext, message files, plurals, locale URLs, and syncing translations with a TMS.

API-Driven localization: Designing scalable translation infrastructure

How API-driven localization works: architecture patterns, caching, rate limits, and when to use an API instead of CDN hosting.

Environment-based localization: Managing translations across dev, staging, and production

Learn how to manage translations differently across dev, staging, and production. Environment setup, missing key behavior, CI/CD promotion, and real workflow examples with SimpleLocalize.

Tone of voice and formality across cultures

A translated string can be accurate and still sound wrong. Learn how tone and formality shift across languages and cultures, with real examples.

Angular i18n: The complete guide to localizing Angular apps (2026)

A practical guide to Angular internationalization. Compare Angular's built-in i18n with ngx-translate and Transloco, handle pluralization, locale routing, and CI/CD.

i18n vs localization: Who owns what on your team?

Learn how to define ownership across engineering, product, and localization teams for scalable translation workflows.

\ No newline at end of file diff --git a/junit/multi-lang-nested/en/app/settings/home.json b/junit/multi-lang-nested/en/app/settings/home.json new file mode 100644 index 00000000..117b3803 --- /dev/null +++ b/junit/multi-lang-nested/en/app/settings/home.json @@ -0,0 +1,3 @@ +{ + "title": "Home" +} diff --git a/junit/multi-lang-nested/en/toast.json b/junit/multi-lang-nested/en/toast.json new file mode 100644 index 00000000..ba5fa06f --- /dev/null +++ b/junit/multi-lang-nested/en/toast.json @@ -0,0 +1,3 @@ +{ + "title": "Toast" +} diff --git a/junit/multi-lang-nested/fr/app/settings/home.json b/junit/multi-lang-nested/fr/app/settings/home.json new file mode 100644 index 00000000..117b3803 --- /dev/null +++ b/junit/multi-lang-nested/fr/app/settings/home.json @@ -0,0 +1,3 @@ +{ + "title": "Home" +} diff --git a/junit/multi-lang-nested/fr/toast.json b/junit/multi-lang-nested/fr/toast.json new file mode 100644 index 00000000..ba5fa06f --- /dev/null +++ b/junit/multi-lang-nested/fr/toast.json @@ -0,0 +1,3 @@ +{ + "title": "Toast" +} diff --git a/junit/nested-namespaces/en/app/settings/home.json b/junit/nested-namespaces/en/app/settings/home.json new file mode 100644 index 00000000..117b3803 --- /dev/null +++ b/junit/nested-namespaces/en/app/settings/home.json @@ -0,0 +1,3 @@ +{ + "title": "Home" +} diff --git a/junit/nested-namespaces/en/components/impact/ProjectItem.json b/junit/nested-namespaces/en/components/impact/ProjectItem.json new file mode 100644 index 00000000..2beb8a82 --- /dev/null +++ b/junit/nested-namespaces/en/components/impact/ProjectItem.json @@ -0,0 +1,3 @@ +{ + "title": "Project item" +} diff --git a/junit/nested-namespaces/en/toast.json b/junit/nested-namespaces/en/toast.json new file mode 100644 index 00000000..ba5fa06f --- /dev/null +++ b/junit/nested-namespaces/en/toast.json @@ -0,0 +1,3 @@ +{ + "title": "Toast" +} diff --git a/junit/ns-dir-lang-suffix/account/settings/messages_en.properties b/junit/ns-dir-lang-suffix/account/settings/messages_en.properties new file mode 100644 index 00000000..e5ea862e --- /dev/null +++ b/junit/ns-dir-lang-suffix/account/settings/messages_en.properties @@ -0,0 +1 @@ +title=Settings diff --git a/junit/ns-dir-lang-suffix/account/settings/messages_pl.properties b/junit/ns-dir-lang-suffix/account/settings/messages_pl.properties new file mode 100644 index 00000000..e5ea862e --- /dev/null +++ b/junit/ns-dir-lang-suffix/account/settings/messages_pl.properties @@ -0,0 +1 @@ +title=Settings diff --git a/junit/ns-dir-lang-suffix/home/messages_en.properties b/junit/ns-dir-lang-suffix/home/messages_en.properties new file mode 100644 index 00000000..d1796940 --- /dev/null +++ b/junit/ns-dir-lang-suffix/home/messages_en.properties @@ -0,0 +1 @@ +title=Home diff --git a/junit/ns-dir-lang-suffix/home/messages_pl.properties b/junit/ns-dir-lang-suffix/home/messages_pl.properties new file mode 100644 index 00000000..d1796940 --- /dev/null +++ b/junit/ns-dir-lang-suffix/home/messages_pl.properties @@ -0,0 +1 @@ +title=Home diff --git a/junit/ns-nested-lang-filename/common/en.json b/junit/ns-nested-lang-filename/common/en.json new file mode 100644 index 00000000..e2c10f90 --- /dev/null +++ b/junit/ns-nested-lang-filename/common/en.json @@ -0,0 +1,3 @@ +{ + "title": "Common" +} diff --git a/junit/ns-nested-lang-filename/common/pl.json b/junit/ns-nested-lang-filename/common/pl.json new file mode 100644 index 00000000..e2c10f90 --- /dev/null +++ b/junit/ns-nested-lang-filename/common/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Common" +} diff --git a/junit/ns-nested-lang-filename/mobile/settings/en.json b/junit/ns-nested-lang-filename/mobile/settings/en.json new file mode 100644 index 00000000..1ed130b5 --- /dev/null +++ b/junit/ns-nested-lang-filename/mobile/settings/en.json @@ -0,0 +1,3 @@ +{ + "title": "Settings" +} diff --git a/junit/ns-nested-lang-filename/mobile/settings/pl.json b/junit/ns-nested-lang-filename/mobile/settings/pl.json new file mode 100644 index 00000000..1ed130b5 --- /dev/null +++ b/junit/ns-nested-lang-filename/mobile/settings/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Settings" +} diff --git a/pom.xml b/pom.xml index fbb2150e..1e7f3b76 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.simplelocalize simplelocalize-cli - 2.10.0 + 2.11.0 jar simplelocalize-cli Official SimpleLocalize Command Line Interface diff --git a/simplelocalize-sample.yml b/simplelocalize-sample.yml index fcb6a02c..90474117 100644 --- a/simplelocalize-sample.yml +++ b/simplelocalize-sample.yml @@ -41,6 +41,23 @@ downloadOptions: - 'WRITE_NESTED' # write nested JSON +################################### +# Language mapping (YAML only) +################################### + +# mappings.lang maps SimpleLocalize language keys ('languageKey') to the language name used in the file system / {lang} placeholder ('placeholder'). +# On download the SimpleLocalize language key is translated to the file system language name ({lang}), +# on upload the file system language name ({lang}) is translated back to the SimpleLocalize language key. +# This can be used to customize where files are downloaded, e.g. to download 'en' to '/values/' instead of '/values-en/'. +# This option can be configured only via this YAML configuration file. +mappings: + lang: + - languageKey: "en" + placeholder: "" # SimpleLocalize 'en' is downloaded to '{lang}' = '' (e.g. ./res/values{lang} -> ./res/values) + - languageKey: "de" + placeholder: "-de" # SimpleLocalize 'de' is downloaded to '{lang}' = '-de' (e.g. ./res/values{lang} -> ./res/values-de) + + ######################################### # 'simplelocalize auto-translate' command ######################################### @@ -67,5 +84,3 @@ projectType: yahoo/react-intl ignoreKeys: - 'WELCOME' - 'ABOUT-US' - - diff --git a/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java b/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java index c30f993e..ea8dcd6b 100644 --- a/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java +++ b/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java @@ -198,7 +198,7 @@ public void download( @Option(names = {"--downloadOptions", "--options"}, split = ",", description = "(Optional) Download options, use comma separated values for multiple options") List downloadOptions, @Option(names = {"--downloadTags", "--tags"}, split = ",", description = "(Optional) Download translation keys with given tags") List tags, @Option(names = {"--downloadSort", "--sort"}, description = "(Optional) Download sorting") String downloadSort, - @Option(names = {"--downloadLanguageKey", "--languageKey"}, split = ",", description = "(Optional) Download translations only for given language key, use comma separated values for multiple keys") List languageKeys, + @Option(names = {"--downloadLanguageKey", "--downloadLanguageKeys", "--languageKey", "--languageKeys"}, split = ",", description = "(Optional) Download translations only for given language key, use comma separated values for multiple keys") List languageKeys, @Option(names = {"--downloadCustomerId", "--customerId"}, description = "(Optional) Download translations only for given customerId") String customerId, @Option(names = {"--downloadNamespace", "--namespace"}, description = "(Optional) Download translations only for given namespace") String namespace, @Option(names = {"--baseUrl"}, description = "(Optional) Set custom server URL") String baseUrl diff --git a/src/main/java/io/simplelocalize/cli/Version.java b/src/main/java/io/simplelocalize/cli/Version.java index 3d9f685d..eef50f34 100644 --- a/src/main/java/io/simplelocalize/cli/Version.java +++ b/src/main/java/io/simplelocalize/cli/Version.java @@ -3,7 +3,7 @@ public class Version { - public static final String NUMBER = "2.10.0"; + public static final String NUMBER = "2.11.0"; private Version() { diff --git a/src/main/java/io/simplelocalize/cli/client/dto/proxy/Configuration.java b/src/main/java/io/simplelocalize/cli/client/dto/proxy/Configuration.java index 7632bbec..49d0d3a8 100644 --- a/src/main/java/io/simplelocalize/cli/client/dto/proxy/Configuration.java +++ b/src/main/java/io/simplelocalize/cli/client/dto/proxy/Configuration.java @@ -80,6 +80,16 @@ public class Configuration private String downloadCustomerId; private String downloadNamespace; + /** + * Mappings (configurable only via YAML configuration file). + * mappings.lang maps SimpleLocalize language keys (languageKey) to the language name used in the file + * system / {lang} placeholder (placeholder). + * On download: SimpleLocalize language key is translated to the file system language name ({lang}). + * On upload: the file system language name ({lang}) is translated back to the SimpleLocalize language key (reverse mapping). + */ + @Builder.Default + private Mappings mappings = new Mappings(); + /** * Auto-translate command */ diff --git a/src/main/java/io/simplelocalize/cli/client/dto/proxy/LanguageTransform.java b/src/main/java/io/simplelocalize/cli/client/dto/proxy/LanguageTransform.java new file mode 100644 index 00000000..e63ecbca --- /dev/null +++ b/src/main/java/io/simplelocalize/cli/client/dto/proxy/LanguageTransform.java @@ -0,0 +1,20 @@ +package io.simplelocalize.cli.client.dto.proxy; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Single language mapping entry (configurable only via YAML configuration file, under mappings.lang). + * Maps a SimpleLocalize language key ({@code languageKey}) to the language name used in the + * file system / {lang} placeholder ({@code placeholder}). Unlike 'source'/'target', these names stay + * unambiguous regardless of direction (download or upload). + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LanguageTransform +{ + private String languageKey; + private String placeholder; +} diff --git a/src/main/java/io/simplelocalize/cli/client/dto/proxy/Mappings.java b/src/main/java/io/simplelocalize/cli/client/dto/proxy/Mappings.java new file mode 100644 index 00000000..029b1c71 --- /dev/null +++ b/src/main/java/io/simplelocalize/cli/client/dto/proxy/Mappings.java @@ -0,0 +1,19 @@ +package io.simplelocalize.cli.client.dto.proxy; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Mappings +{ + @Builder.Default + private List lang = new ArrayList<>(); +} diff --git a/src/main/java/io/simplelocalize/cli/command/DownloadCommand.java b/src/main/java/io/simplelocalize/cli/command/DownloadCommand.java index bf482ce0..3ef0fcb4 100644 --- a/src/main/java/io/simplelocalize/cli/command/DownloadCommand.java +++ b/src/main/java/io/simplelocalize/cli/command/DownloadCommand.java @@ -5,7 +5,9 @@ import io.simplelocalize.cli.client.dto.ExportRequest; import io.simplelocalize.cli.client.dto.proxy.Configuration; import io.simplelocalize.cli.client.dto.proxy.DownloadableFile; +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; import io.simplelocalize.cli.configuration.ConfigurationValidatorUtil; +import io.simplelocalize.cli.util.LanguageMappingUtil; import io.simplelocalize.cli.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,12 +101,37 @@ public void invoke() throws IOException, InterruptedException .withSort(sort) .withTags(tags) .build(); + final List languageTransforms = configuration.getMappings().getLang(); + if (!languageTransforms.isEmpty()) + { + log.info("Language mapping: {}", languageTransforms); + } + final List downloadableFiles = client.exportFiles(exportRequest); for (DownloadableFile downloadableFile : downloadableFiles) { - client.downloadFile(downloadableFile, downloadPath); + DownloadableFile fileToDownload = mapLanguage(downloadableFile, languageTransforms); + client.downloadFile(fileToDownload, downloadPath); } log.info("Successfully downloaded all files"); } + private DownloadableFile mapLanguage(DownloadableFile downloadableFile, List languageTransforms) + { + final String originalLanguage = downloadableFile.language(); + final String mappedLanguage = LanguageMappingUtil.toFileSystemLanguage(languageTransforms, originalLanguage); + if (Objects.equals(originalLanguage, mappedLanguage)) + { + return downloadableFile; + } + return DownloadableFile.builder() + .withUrl(downloadableFile.url()) + .withNamespace(downloadableFile.namespace()) + .withLanguage(mappedLanguage) + .withCustomer(downloadableFile.customer()) + .withTranslationKey(downloadableFile.translationKey()) + .withRemotePath(downloadableFile.remotePath()) + .build(); + } + } diff --git a/src/main/java/io/simplelocalize/cli/command/UploadCommand.java b/src/main/java/io/simplelocalize/cli/command/UploadCommand.java index 4d13ccfd..c01a2f41 100644 --- a/src/main/java/io/simplelocalize/cli/command/UploadCommand.java +++ b/src/main/java/io/simplelocalize/cli/command/UploadCommand.java @@ -5,9 +5,11 @@ import io.simplelocalize.cli.client.dto.FileToUpload; import io.simplelocalize.cli.client.dto.UploadRequest; import io.simplelocalize.cli.client.dto.proxy.Configuration; +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; import io.simplelocalize.cli.configuration.ConfigurationValidatorUtil; import io.simplelocalize.cli.exception.ConfigurationException; import io.simplelocalize.cli.io.FileListReader; +import io.simplelocalize.cli.util.LanguageMappingUtil; import io.simplelocalize.cli.util.StringUtils; import io.simplelocalize.cli.util.SystemUtils; import org.slf4j.Logger; @@ -88,6 +90,12 @@ public void invoke() throws IOException, InterruptedException log.info("Options: {}", uploadOptions); } + final List languageTransforms = configuration.getMappings().getLang(); + if (!languageTransforms.isEmpty()) + { + log.info("Language mapping: {}", languageTransforms); + } + final List filesToUpload = fileListReader.findFilesToUpload(uploadPath); if (filesToUpload.size() > 1) { @@ -111,7 +119,8 @@ public void invoke() throws IOException, InterruptedException } final String fileLanguage = fileToUpload.language(); - final String effectiveLanguageKey = hasDefinedLanguageKey ? uploadLanguageKey : fileLanguage; + final String mappedFileLanguage = LanguageMappingUtil.toSimpleLocalizeLanguage(languageTransforms, fileLanguage); + final String effectiveLanguageKey = hasDefinedLanguageKey ? uploadLanguageKey : mappedFileLanguage; final String fileNamespace = fileToUpload.namespace(); final String effectiveNamespace = hasDefinedNamespace ? uploadNamespace : fileNamespace; diff --git a/src/main/java/io/simplelocalize/cli/io/FileListReader.java b/src/main/java/io/simplelocalize/cli/io/FileListReader.java index 6699cbae..6e1680d6 100644 --- a/src/main/java/io/simplelocalize/cli/io/FileListReader.java +++ b/src/main/java/io/simplelocalize/cli/io/FileListReader.java @@ -71,7 +71,9 @@ private Matcher getMatcher(String input, String pattern) .replace("\\", "\\\\") // escape backslash .replace(NAMESPACE_TEMPLATE_KEY, "(?.*)") .replace(TRANSLATION_KEY_TEMPLATE_KEY, "(?.*)") - .replace(LANGUAGE_TEMPLATE_KEY, "(?.*)"); + // language never spans multiple directories, so it must not cross a path separator, + // otherwise it would greedily swallow nested namespace directories (e.g. en/components/impact) + .replace(LANGUAGE_TEMPLATE_KEY, "(?[^/\\\\]*)"); Pattern regex = Pattern.compile(replace); return regex.matcher(input); } diff --git a/src/main/java/io/simplelocalize/cli/util/LanguageMappingUtil.java b/src/main/java/io/simplelocalize/cli/util/LanguageMappingUtil.java new file mode 100644 index 00000000..8d807687 --- /dev/null +++ b/src/main/java/io/simplelocalize/cli/util/LanguageMappingUtil.java @@ -0,0 +1,59 @@ +package io.simplelocalize.cli.util; + +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; + +import java.util.List; + +/** + * Utility for translating language keys between SimpleLocalize and the file system representation ({lang}). + * The mapping is configured only via the YAML configuration file, under mappings.lang, where 'languageKey' is + * the SimpleLocalize language key and 'placeholder' is the name used in the file system. + */ +public final class LanguageMappingUtil +{ + + private LanguageMappingUtil() + { + } + + /** + * Translates a SimpleLocalize language key into the file system language name used in the {lang} placeholder. + * Used on download. When there is no mapping for the given key, the original key is returned unchanged. + */ + public static String toFileSystemLanguage(List languageTransforms, String languageKey) + { + if (languageTransforms == null || languageTransforms.isEmpty() || languageKey == null) + { + return languageKey; + } + for (LanguageTransform languageTransform : languageTransforms) + { + if (languageKey.equals(languageTransform.getLanguageKey())) + { + return languageTransform.getPlaceholder(); + } + } + return languageKey; + } + + /** + * Translates a file system language name (found in the {lang} placeholder) back into the SimpleLocalize language key. + * Used on upload. This is the reverse of {@link #toFileSystemLanguage(List, String)}. + * When there is no mapping for the given file system name, the original value is returned unchanged. + */ + public static String toSimpleLocalizeLanguage(List languageTransforms, String fileSystemLanguage) + { + if (languageTransforms == null || languageTransforms.isEmpty() || fileSystemLanguage == null) + { + return fileSystemLanguage; + } + for (LanguageTransform languageTransform : languageTransforms) + { + if (fileSystemLanguage.equals(languageTransform.getPlaceholder())) + { + return languageTransform.getLanguageKey(); + } + } + return fileSystemLanguage; + } +} diff --git a/src/test/java/io/simplelocalize/cli/command/DownloadCommandTest.java b/src/test/java/io/simplelocalize/cli/command/DownloadCommandTest.java index 2335bff7..d6274e04 100644 --- a/src/test/java/io/simplelocalize/cli/command/DownloadCommandTest.java +++ b/src/test/java/io/simplelocalize/cli/command/DownloadCommandTest.java @@ -4,8 +4,12 @@ import io.simplelocalize.cli.client.dto.ExportRequest; import io.simplelocalize.cli.client.dto.proxy.Configuration; import io.simplelocalize.cli.client.dto.proxy.DownloadableFile; +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; +import io.simplelocalize.cli.client.dto.proxy.Mappings; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -56,4 +60,82 @@ void shouldDownloadMultipleFiles() throws Exception DownloadableFile.builder().withNamespace("common").withUrl("https://s3.simplelocalize.io/file2.xml").build(), "./my-project-path"); } + + @Test + void shouldMapSimpleLocalizeLanguageKeyToFileSystemLanguageOnDownload() throws Exception + { + //given + List languageTransforms = List.of( + new LanguageTransform("en", "english"), + new LanguageTransform("pl", "polish")); + + Configuration configuration = Configuration.defaultConfiguration(); + configuration.setApiKey("my-api-key"); + configuration.setDownloadPath("./messages/{lang}/strings.json"); + configuration.setDownloadFormat("single-language-json"); + configuration.setMappings(Mappings.builder().lang(languageTransforms).build()); + + //when + Mockito.when(client.exportFiles(Mockito.any(ExportRequest.class))) + .thenReturn(List.of( + DownloadableFile.builder().withLanguage("en").withUrl("https://s3.simplelocalize.io/en.json").build(), + DownloadableFile.builder().withLanguage("pl").withUrl("https://s3.simplelocalize.io/pl.json").build(), + DownloadableFile.builder().withLanguage("de").withUrl("https://s3.simplelocalize.io/de.json").build() + )); + + DownloadCommand downloadCommand = new DownloadCommand(client, configuration); + downloadCommand.invoke(); + + //then + Mockito.verify(client, Mockito.times(1)) + .downloadFile( + DownloadableFile.builder().withLanguage("english").withUrl("https://s3.simplelocalize.io/en.json").build(), + "./messages/{lang}/strings.json"); + Mockito.verify(client, Mockito.times(1)) + .downloadFile( + DownloadableFile.builder().withLanguage("polish").withUrl("https://s3.simplelocalize.io/pl.json").build(), + "./messages/{lang}/strings.json"); + // 'de' has no mapping, so it is passed through unchanged + Mockito.verify(client, Mockito.times(1)) + .downloadFile( + DownloadableFile.builder().withLanguage("de").withUrl("https://s3.simplelocalize.io/de.json").build(), + "./messages/{lang}/strings.json"); + } + + @ParameterizedTest + @ValueSource(strings = {"Linux", "Mac OS X", "Windows 10"}) + void shouldMapLanguageKeyOnDownloadRegardlessOfOperatingSystem(String osName) throws Exception + { + //given + String originalOsName = System.getProperty("os.name"); + System.setProperty("os.name", osName); + try + { + List languageTransforms = List.of(new LanguageTransform("en", "english")); + + Configuration configuration = Configuration.defaultConfiguration(); + configuration.setApiKey("my-api-key"); + configuration.setDownloadPath("./messages/values-{lang}/strings.json"); + configuration.setDownloadFormat("single-language-json"); + configuration.setMappings(Mappings.builder().lang(languageTransforms).build()); + + Mockito.when(client.exportFiles(Mockito.any(ExportRequest.class))) + .thenReturn(List.of( + DownloadableFile.builder().withLanguage("en").withUrl("https://s3.simplelocalize.io/en.json").build() + )); + + //when + DownloadCommand downloadCommand = new DownloadCommand(client, configuration); + downloadCommand.invoke(); + + //then + Mockito.verify(client, Mockito.times(1)) + .downloadFile( + DownloadableFile.builder().withLanguage("english").withUrl("https://s3.simplelocalize.io/en.json").build(), + "./messages/values-{lang}/strings.json"); + } finally + { + System.setProperty("os.name", originalOsName); + } + } } diff --git a/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java b/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java index 45874458..1cd5a558 100644 --- a/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java +++ b/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java @@ -3,10 +3,14 @@ import io.simplelocalize.cli.client.SimpleLocalizeClient; import io.simplelocalize.cli.client.dto.UploadRequest; import io.simplelocalize.cli.client.dto.proxy.Configuration; +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; +import io.simplelocalize.cli.client.dto.proxy.Mappings; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -201,4 +205,61 @@ public void shouldNotFindMatchingFilesWhenFakeWindows() throws Exception //then Mockito.verify(client, times(0)).uploadFile(any()); } + + @Test + public void shouldMapFileSystemLanguageToSimpleLocalizeLanguageKeyOnUpload() throws Exception + { + //given + List languageTransforms = List.of( + new LanguageTransform("en_US", "en"), // SimpleLocalize key 'en_US' is stored on disk as 'en' + new LanguageTransform("pl_PL", "pl")); // SimpleLocalize key 'pl_PL' is stored on disk as 'pl' + + Configuration configuration = Configuration.defaultConfiguration(); + configuration.setApiKey("my-api-key"); + configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml"); + configuration.setUploadFormat("android"); + configuration.setMappings(Mappings.builder().lang(languageTransforms).build()); + + //when + UploadCommand uploadCommand = new UploadCommand(client, configuration); + uploadCommand.invoke(); + + //then + ArgumentCaptor uploadRequestCaptor = ArgumentCaptor.forClass(UploadRequest.class); + Mockito.verify(client, times(12)).uploadFile(uploadRequestCaptor.capture()); + List uploadedLanguageKeys = uploadRequestCaptor.getAllValues().stream() + .map(UploadRequest::languageKey) + .toList(); + + Assertions.assertThat(uploadedLanguageKeys) + .contains("en_US", "pl_PL") // mapped values + .contains("de", "es", "fr", "it") // unmapped languages pass through unchanged + .doesNotContain("en", "pl"); // file system names should have been mapped away + } + + @Test + public void shouldUseLanguageMappingReverseValueForSingleMappedFile() throws Exception + { + //given + List languageTransforms = List.of(new LanguageTransform("en_US", "en")); + + Configuration configuration = Configuration.defaultConfiguration(); + configuration.setApiKey("my-api-key"); + configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml"); + configuration.setUploadFormat("android"); + configuration.setMappings(Mappings.builder().lang(languageTransforms).build()); + + //when + UploadCommand uploadCommand = new UploadCommand(client, configuration); + uploadCommand.invoke(); + + //then + ArgumentCaptor uploadRequestCaptor = ArgumentCaptor.forClass(UploadRequest.class); + Mockito.verify(client, times(12)).uploadFile(uploadRequestCaptor.capture()); + UploadRequest enUsRequest = uploadRequestCaptor.getAllValues().stream() + .filter(request -> "en_US".equals(request.languageKey())) + .findFirst() + .orElseThrow(); + Assertions.assertThat(enUsRequest.path().toString()).contains("values-en"); + } } diff --git a/src/test/java/io/simplelocalize/cli/configuration/ConfigurationLoaderTest.java b/src/test/java/io/simplelocalize/cli/configuration/ConfigurationLoaderTest.java index 86af8490..c3eaa16d 100644 --- a/src/test/java/io/simplelocalize/cli/configuration/ConfigurationLoaderTest.java +++ b/src/test/java/io/simplelocalize/cli/configuration/ConfigurationLoaderTest.java @@ -2,9 +2,11 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import io.simplelocalize.cli.client.dto.proxy.Configuration; +import io.simplelocalize.cli.client.dto.proxy.LanguageTransform; import io.simplelocalize.cli.exception.ConfigurationException; import io.simplelocalize.cli.util.TestLogEventFactory; import org.assertj.core.api.Assertions; +import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.Test; import java.nio.file.Path; @@ -96,6 +98,13 @@ void shouldLoadConfiguration() throws Exception Assertions.assertThat(configuration.getDownloadNamespace()).isEqualTo("login"); Assertions.assertThat(configuration.getDownloadOptions()).containsExactlyInAnyOrder("SPLIT_BY_NAMESPACES", "WRITE_NESTED"); + // language mapping + Assertions.assertThat(configuration.getMappings().getLang()) + .extracting(LanguageTransform::getLanguageKey, LanguageTransform::getPlaceholder) + .containsExactlyInAnyOrder( + Tuple.tuple("en", "english"), + Tuple.tuple("pl_PL", "polish")); + // auto-translate Assertions.assertThat(configuration.getAutoTranslateLanguageKeys()).isNotNull().containsExactlyInAnyOrder("en", "de"); diff --git a/src/test/java/io/simplelocalize/cli/util/FileListReaderTest.java b/src/test/java/io/simplelocalize/cli/util/FileListReaderTest.java index 89917134..a3aa447d 100644 --- a/src/test/java/io/simplelocalize/cli/util/FileListReaderTest.java +++ b/src/test/java/io/simplelocalize/cli/util/FileListReaderTest.java @@ -360,6 +360,36 @@ void shouldFindTranslationsWhenExtremeEdgeCases() throws IOException ); } + @Test + void shouldKeepLanguageAsSingleDirectoryAndUseSubdirectoriesAsNamespace() throws IOException + { + //given: customer layout where language is a single directory and the namespace spans nested directories + String path = "./junit/nested-namespaces/{lang}/{ns}.json"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/nested-namespaces/en/toast.json")) + .withLanguage("en") + .withNamespace("toast") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/nested-namespaces/en/app/settings/home.json")) + .withLanguage("en") + .withNamespace("app/settings/home") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/nested-namespaces/en/components/impact/ProjectItem.json")) + .withLanguage("en") + .withNamespace("components/impact/ProjectItem") + .build() + ); + } + @Test void langAsDirectoriesNsAsFilename() throws IOException { @@ -394,4 +424,202 @@ void langAsDirectoriesNsAsFilename() throws IOException .build() ); } + + @Test + void shouldTreatNestedDirectoriesAsNamespaceWhenLanguageIsFilename() throws IOException + { + //given: language is the file name while the namespace spans one or more nested directories + String path = "./junit/ns-nested-lang-filename/{ns}/{lang}.json"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-nested-lang-filename/common/en.json")) + .withLanguage("en") + .withNamespace("common") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-nested-lang-filename/common/pl.json")) + .withLanguage("pl") + .withNamespace("common") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-nested-lang-filename/mobile/settings/en.json")) + .withLanguage("en") + .withNamespace("mobile/settings") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-nested-lang-filename/mobile/settings/pl.json")) + .withLanguage("pl") + .withNamespace("mobile/settings") + .build() + ); + } + + @Test + void shouldMatchMultipleLanguagesEachWithNestedNamespaces() throws IOException + { + //given: several single-directory languages, each containing a deeply nested namespace tree + String path = "./junit/multi-lang-nested/{lang}/{ns}.json"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/en/toast.json")) + .withLanguage("en") + .withNamespace("toast") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/en/app/settings/home.json")) + .withLanguage("en") + .withNamespace("app/settings/home") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/fr/toast.json")) + .withLanguage("fr") + .withNamespace("toast") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/fr/app/settings/home.json")) + .withLanguage("fr") + .withNamespace("app/settings/home") + .build() + ); + } + + @Test + void shouldSupportLanguageCodesWithRegionScriptAndUnderscore() throws IOException + { + //given: language codes containing region, script subtags and underscores + String path = "./junit/lang-region-codes/{lang}.json"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/lang-region-codes/en.json")) + .withLanguage("en") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/lang-region-codes/pt-BR.json")) + .withLanguage("pt-BR") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/lang-region-codes/zh-Hant-TW.json")) + .withLanguage("zh-Hant-TW") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/lang-region-codes/es_419.json")) + .withLanguage("es_419") + .build() + ); + } + + @Test + void shouldMatchLanguageAsFilenameSuffixWithNestedNamespace() throws IOException + { + //given: language embedded as a suffix in the file name while the namespace spans nested directories + String path = "./junit/ns-dir-lang-suffix/{ns}/messages_{lang}.properties"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-dir-lang-suffix/home/messages_en.properties")) + .withLanguage("en") + .withNamespace("home") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-dir-lang-suffix/home/messages_pl.properties")) + .withLanguage("pl") + .withNamespace("home") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-dir-lang-suffix/account/settings/messages_en.properties")) + .withLanguage("en") + .withNamespace("account/settings") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/ns-dir-lang-suffix/account/settings/messages_pl.properties")) + .withLanguage("pl") + .withNamespace("account/settings") + .build() + ); + } + + @Test + void shouldMatchLanguageWithPrefixAndSuffixInDirectoryName() throws IOException + { + //given: language surrounded by a static prefix and suffix in the directory name; sibling dirs without the suffix must be ignored + String path = "./junit/download-test/values-{lang}-test/strings.xml"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-de-test/strings.xml")) + .withLanguage("de") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-en-test/strings.xml")) + .withLanguage("en") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-es-test/strings.xml")) + .withLanguage("es") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-fr-test/strings.xml")) + .withLanguage("fr") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-it-test/strings.xml")) + .withLanguage("it") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/download-test/values-pl-test/strings.xml")) + .withLanguage("pl") + .build() + ); + } + + @Test + void shouldCaptureNestedNamespaceWhenPatternHasNoLanguage() throws IOException + { + //given: no language placeholder, namespace alone must capture the whole nested path + String path = "./junit/multi-lang-nested/en/{ns}.json"; + + //when + List result = sut.findFilesToUpload(path); + + //then + Assertions.assertThat(result) + .containsExactlyInAnyOrder( + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/en/toast.json")) + .withNamespace("toast") + .build(), + FileToUpload.builder() + .withPath(Paths.get("./junit/multi-lang-nested/en/app/settings/home.json")) + .withNamespace("app/settings/home") + .build() + ); + } } diff --git a/src/test/resources/simplelocalize.yml b/src/test/resources/simplelocalize.yml index 4dbdc5f4..11d3f587 100644 --- a/src/test/resources/simplelocalize.yml +++ b/src/test/resources/simplelocalize.yml @@ -17,6 +17,13 @@ downloadOptions: - WRITE_NESTED - SPLIT_BY_NAMESPACES +mappings: + lang: + - languageKey: en + placeholder: english + - languageKey: pl_PL + placeholder: polish + autoTranslateLanguageKeys: - 'en' - 'de'