From 28ca29bd46969d2be9ffbeaa321edbd7b7e14cab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:27:14 +0200 Subject: [PATCH 01/12] chore(deps): bump undici from 7.16.0 to 7.24.1 (#1006) --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5d3ad99a..f753eed59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6520,11 +6520,10 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", + "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", "dev": true, - "license": "MIT", "engines": { "node": ">=20.18.1" } @@ -11084,9 +11083,9 @@ "optional": true }, "undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", + "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", "dev": true }, "unicode-emoji-modifier-base": { From a24ecee8385215114af724654bc50bcc520fccc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:27:38 +0200 Subject: [PATCH 02/12] chore(deps): bump yauzl from 3.2.0 to 3.2.1 (#1007) --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index f753eed59..88ebd5c23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6733,10 +6733,9 @@ } }, "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", - "license": "MIT", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", + "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" @@ -11236,9 +11235,9 @@ "dev": true }, "yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", + "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", "requires": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" From 6f06f4799f47fa26d42b0750aaea76a50f212e9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:45:40 +0200 Subject: [PATCH 03/12] chore(deps): bump picomatch (#1010) --- package-lock.json | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88ebd5c23..3805803a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5089,9 +5089,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "engines": { "node": ">=8.6" }, @@ -6441,11 +6441,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -10120,9 +10119,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" }, "pify": { "version": "3.0.0", @@ -11033,9 +11032,9 @@ "requires": {} }, "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true } } From d608b11982da6a3c7e3b750762fee8b31c36f251 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Fri, 27 Mar 2026 00:36:23 -0700 Subject: [PATCH 04/12] fix: disabled target languages warning (#1008) --- .../java/com/crowdin/cli/client/Client.java | 9 +++- .../actions/UploadTranslationsAction.java | 28 ++++++++-- .../resources/messages/messages.properties | 1 + .../actions/UploadTranslationsActionTest.java | 54 ++++++++++++++++--- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/crowdin/cli/client/Client.java b/src/main/java/com/crowdin/cli/client/Client.java index 6e116e146..f90774c91 100644 --- a/src/main/java/com/crowdin/cli/client/Client.java +++ b/src/main/java/com/crowdin/cli/client/Client.java @@ -4,11 +4,13 @@ import com.crowdin.cli.commands.picocli.ExitCodeExceptionMapper; import com.crowdin.cli.utils.console.ConsoleSpinner; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import static com.crowdin.cli.BaseCli.RESOURCE_BUNDLE; import static com.crowdin.cli.utils.console.ExecutionStatus.ERROR; +import static com.crowdin.cli.utils.console.ExecutionStatus.WARNING; public interface Client { @@ -77,8 +79,9 @@ static S executeAsyncActionWithoutSpinner( Function getStatus, Function getProgress, Function getId, + Set> passthroughExceptions, boolean isVerbose - ) { + ) throws ResponseException { try { if (initMessage != null) { var initMsg = RESOURCE_BUNDLE.containsKey(initMessage) ? RESOURCE_BUNDLE.getString(initMessage) : initMessage; @@ -114,6 +117,10 @@ static S executeAsyncActionWithoutSpinner( return status; } catch (Exception e) { + if (passthroughExceptions != null && passthroughExceptions.stream().anyMatch(c -> c.isInstance(e))) { + ConsoleSpinner.stop(WARNING); + throw (ResponseException) e; + } ConsoleSpinner.stop(ERROR); throw ExitCodeExceptionMapper.remap( e, diff --git a/src/main/java/com/crowdin/cli/commands/actions/UploadTranslationsAction.java b/src/main/java/com/crowdin/cli/commands/actions/UploadTranslationsAction.java index 834b64372..80a9470fd 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/UploadTranslationsAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/UploadTranslationsAction.java @@ -1,8 +1,6 @@ package com.crowdin.cli.commands.actions; -import com.crowdin.cli.client.CrowdinProjectFull; -import com.crowdin.cli.client.LanguageMapping; -import com.crowdin.cli.client.ProjectClient; +import com.crowdin.cli.client.*; import com.crowdin.cli.commands.NewAction; import com.crowdin.cli.commands.Outputter; import com.crowdin.cli.commands.functionality.*; @@ -167,8 +165,20 @@ public void act(Outputter out, PropertiesWithFiles pb, ProjectClient client) { ImportTranslationsStatus::getStatus, ImportTranslationsStatus::getProgress, ImportTranslationsStatus::getIdentifier, + Set.of(WrongLanguageException.class), isVerbose ); + } catch (WrongLanguageException e) { + String languageNames = langs.stream() + .map(Language::getName) + .collect(Collectors.joining("/")); + out.println(WARNING.withIcon(String.format( + RESOURCE_BUNDLE.getString("message.warning.file_not_uploaded_cause_of_language"), + StringUtils.removeStart(translationFile.getAbsolutePath(), pb.getBasePath()), languageNames))); + return; + } catch (ResponseException e) { + containsErrors.set(true); + throw new RuntimeException(e); } catch (Exception e) { containsErrors.set(true); throw e; @@ -228,8 +238,20 @@ public void act(Outputter out, PropertiesWithFiles pb, ProjectClient client) { ImportTranslationsStringsBasedStatus::getStatus, ImportTranslationsStringsBasedStatus::getProgress, ImportTranslationsStringsBasedStatus::getIdentifier, + null, isVerbose ); + } catch (WrongLanguageException e) { + String languageNames = langs.stream() + .map(Language::getName) + .collect(Collectors.joining("/")); + out.println(WARNING.withIcon(String.format( + RESOURCE_BUNDLE.getString("message.warning.file_not_uploaded_cause_of_language"), + StringUtils.removeStart(translationFile.getAbsolutePath(), pb.getBasePath()), languageNames))); + return; + } catch (ResponseException e) { + containsErrors.set(true); + throw new RuntimeException(e); } catch (Exception e) { containsErrors.set(true); throw e; diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index 1b377954f..926810f82 100755 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -916,6 +916,7 @@ messages.context.strings_reset_success=Updated strings %d/%d message.warning.not_yml=File '%s' is not a YAML or YML file message.warning.browser_not_found=\nError opening a web browser. Please open the following link manually:\n%s +message.warning.file_not_uploaded_cause_of_language=Translation file @|yellow '%s' hasn't been uploaded|@ since the following target language(s) are not enabled for the source file in your Crowdin project: %s message.warning.auto_approve_option_with_mt='--auto-approve-option' is used only for the TM Pre-Translation method message.warning.no_file_to_download=Couldn't find any file to download message.warning.no_file_to_download_skipuntranslated=Couldn't find any file to download. Since you are using the 'Skip untranslated files' option, please make sure you have fully translated files diff --git a/src/test/java/com/crowdin/cli/commands/actions/UploadTranslationsActionTest.java b/src/test/java/com/crowdin/cli/commands/actions/UploadTranslationsActionTest.java index 501b978f2..d1a59aec7 100644 --- a/src/test/java/com/crowdin/cli/commands/actions/UploadTranslationsActionTest.java +++ b/src/test/java/com/crowdin/cli/commands/actions/UploadTranslationsActionTest.java @@ -1,9 +1,6 @@ package com.crowdin.cli.commands.actions; -import com.crowdin.cli.client.CrowdinProjectFull; -import com.crowdin.cli.client.ProjectClient; -import com.crowdin.cli.client.ProjectBuilder; -import com.crowdin.cli.client.ResponseException; +import com.crowdin.cli.client.*; import com.crowdin.cli.commands.NewAction; import com.crowdin.cli.commands.Outputter; import com.crowdin.cli.properties.PropertiesWithFiles; @@ -23,10 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class UploadTranslationsActionTest { @@ -279,4 +273,48 @@ public void testUploadOneOfTwoTranslation_StringBasedProject() throws ResponseEx verify(client).importTranslations(eq(importTranslationsStringsBasedRequest)); verifyNoMoreInteractions(client); } + + @Test + public void testUploadTranslation_WrongLanguage_ShowsWarningAndContinues() throws ResponseException { + project.addFile(Utils.normalizePath("first.po"), "Hello, World!"); + project.addFile(Utils.normalizePath("first.po-CR-uk-UA"), "Hello, World!"); + + NewPropertiesWithFilesUtilBuilder pbBuilder = NewPropertiesWithFilesUtilBuilder + .minimalBuiltPropertiesBean( + "*", + Utils.PATH_SEPARATOR + "%original_file_name%-CR-%locale%", + Arrays.asList("*-CR-*") + ) + .setBasePath(project.getBasePath()); + PropertiesWithFiles pb = pbBuilder.build(); + + ProjectClient client = mock(ProjectClient.class); + Outputter out = mock(Outputter.class); + + CrowdinProjectFull build = ProjectBuilder.emptyProject(Long.parseLong(pb.getProjectId())) + .addFile("first.po", "gettext", 301L, null, null) + .build(); + build.setType(Type.FILES_BASED); + + when(client.downloadFullProject(null)).thenReturn(build); + when(client.uploadStorage(eq("first.po-CR-uk-UA"), any())).thenReturn(1L); + doAnswer(invocation -> { + throw new WrongLanguageException(); + }).when(client).importTranslations(any(ImportTranslationsRequest.class)); + + NewAction action = + new UploadTranslationsAction(false, null, null, false, false, false, false, false, false); + + assertDoesNotThrow(() -> action.act(out, pb, client)); + + verify(client).downloadFullProject(null); + verify(client).uploadStorage(eq("first.po-CR-uk-UA"), any()); + verify(client).importTranslations(any(ImportTranslationsRequest.class)); + + verify(out).println(argThat(msg -> + msg.contains("first.po-CR-uk-UA") && msg.contains("Ukrainian") + )); + + verifyNoMoreInteractions(client); + } } From 1c9848c8e73c43cac1c1f1fd5cd66cda31c3804b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:36:35 +0200 Subject: [PATCH 05/12] chore(deps-dev): bump handlebars from 4.7.8 to 4.7.9 (#1011) --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3805803a7..77ce8efe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2154,11 +2154,10 @@ "dev": true }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -8177,9 +8176,9 @@ "dev": true }, "handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "requires": { "minimist": "^1.2.5", From f962126a6bb774c7679d7338f9cc6914e9ff0725 Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <29282228+andrii-bodnar@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:51:39 +0300 Subject: [PATCH 06/12] ci: winget support (#996) --- .github/workflows/publish.yml | 25 +++++++++++++ .../winget/Crowdin.CrowdinCLI.installer.yaml | 23 ++++++++++++ .../Crowdin.CrowdinCLI.locale.en-US.yaml | 35 +++++++++++++++++++ packages/winget/Crowdin.CrowdinCLI.yaml | 8 +++++ website/docs/installation.md | 20 +++++++++++ 5 files changed, 111 insertions(+) create mode 100644 packages/winget/Crowdin.CrowdinCLI.installer.yaml create mode 100644 packages/winget/Crowdin.CrowdinCLI.locale.en-US.yaml create mode 100644 packages/winget/Crowdin.CrowdinCLI.yaml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 59c9c88b1..ba490cfdb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ on: - 'homebrew' - 'chocolatey' - 'docker' + - 'winget' release: types: [released] @@ -221,6 +222,30 @@ jobs: "${{ secrets.DOCKER_HUB_REGISTRY_IMAGE }}:${{ github.ref_name }}" "${{ secrets.DOCKER_HUB_REGISTRY_IMAGE }}:latest" + winget: + # Runs automatically on release, after the exe job has built and uploaded the installer. + runs-on: windows-latest + if: github.event_name == 'release' + needs: exe + steps: + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: Crowdin.CrowdinCLI + installers-regex: '\.exe$' + token: ${{ secrets.GH_WINGET_TOKEN }} + + winget-manual: + # Runs on manual dispatch only. No exe build - the action picks up the .exe + # from the latest existing GitHub release, so the release must already exist. + runs-on: windows-latest + if: github.event_name == 'workflow_dispatch' && (github.event.inputs.package == 'all' || github.event.inputs.package == 'winget') + steps: + - uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: Crowdin.CrowdinCLI + installers-regex: '\.exe$' + token: ${{ secrets.GH_WINGET_TOKEN }} + exe: runs-on: windows-latest if: github.event_name == 'release' diff --git a/packages/winget/Crowdin.CrowdinCLI.installer.yaml b/packages/winget/Crowdin.CrowdinCLI.installer.yaml new file mode 100644 index 000000000..63e92299f --- /dev/null +++ b/packages/winget/Crowdin.CrowdinCLI.installer.yaml @@ -0,0 +1,23 @@ +# Updated to winget manifest schema 1.10.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json + +PackageIdentifier: Crowdin.CrowdinCLI +PackageVersion: 4.14.1 +MinimumOSVersion: 10.0.0.0 +InstallerType: inno +InstallModes: + - interactive + - silent +InstallerSwitches: + Silent: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- + SilentWithProgress: /SILENT /SUPPRESSMSGBOXES /NORESTART /SP- +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/crowdin/crowdin-cli/releases/download/4.14.1/crowdin.exe + InstallerSha256: e912b4db3693fd9681d88cde83bcd1f8d955ba332aaf6c509cafd0a1b7f15541 + ProductCode: '{52B80417-16B8-4EFE-B118-6FA64B25CC0F}_is1' +Dependencies: + PackageDependencies: + - PackageIdentifier: EclipseAdoptium.Temurin.17.JRE +ManifestType: installer +ManifestVersion: 1.10.0 diff --git a/packages/winget/Crowdin.CrowdinCLI.locale.en-US.yaml b/packages/winget/Crowdin.CrowdinCLI.locale.en-US.yaml new file mode 100644 index 000000000..7179553b2 --- /dev/null +++ b/packages/winget/Crowdin.CrowdinCLI.locale.en-US.yaml @@ -0,0 +1,35 @@ +# Updated to winget manifest schema 1.10.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json + +PackageIdentifier: Crowdin.CrowdinCLI +PackageVersion: 4.14.1 +PackageLocale: en-US +Publisher: Crowdin +PublisherUrl: https://crowdin.com +PublisherSupportUrl: https://github.com/crowdin/crowdin-cli/issues +PrivacyUrl: https://crowdin.com/page/privacy-policy +Author: Crowdin +PackageName: Crowdin CLI +PackageUrl: https://crowdin.github.io/crowdin-cli +License: MIT +LicenseUrl: https://github.com/crowdin/crowdin-cli/blob/main/LICENSE +Copyright: © Crowdin +ShortDescription: A command line tool that allows you to manage and synchronize localization resources with your Crowdin project. +Description: |- + Crowdin CLI is a command line tool that allows you to manage and synchronize your localization resources with your Crowdin project. Using CLI, you can: + - Automate the process of updating your source files in your Crowdin project + - Download translations from Crowdin and automatically save them in the correct locations + - Upload all your existing translations to Crowdin in minutes + - Manage your localization resources without leaving the terminal + - Integrate Crowdin CLI with GitHub, GitLab, Jenkins, CircleCI, and other software +Moniker: crowdin +Tags: + - cli + - crowdin + - i18n + - l10n + - localization + - translation +ReleaseNotesUrl: https://github.com/crowdin/crowdin-cli/releases/tag/4.14.1 +ManifestType: defaultLocale +ManifestVersion: 1.10.0 diff --git a/packages/winget/Crowdin.CrowdinCLI.yaml b/packages/winget/Crowdin.CrowdinCLI.yaml new file mode 100644 index 000000000..af69aa887 --- /dev/null +++ b/packages/winget/Crowdin.CrowdinCLI.yaml @@ -0,0 +1,8 @@ +# Updated to winget manifest schema 1.10.0 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json + +PackageIdentifier: Crowdin.CrowdinCLI +PackageVersion: 4.14.1 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.10.0 diff --git a/website/docs/installation.md b/website/docs/installation.md index 40a0ac98a..5b38683f3 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -36,6 +36,26 @@ docker pull crowdin/cli ## Windows +### WinGet + +```bash +winget install Crowdin.CrowdinCLI +``` + +```bash +winget upgrade Crowdin.CrowdinCLI +``` + +```bash +winget uninstall Crowdin.CrowdinCLI +``` + +You can also search for the package: + +```bash +winget search crowdin +``` + ### Chocolatey ```bash From 095c134da106f2f10b56e2e1cb6f75abaf5711ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:48:44 +0300 Subject: [PATCH 07/12] chore(deps-dev): bump lodash-es from 4.17.23 to 4.18.1 (#1012) --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77ce8efe5..4f95945b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2604,9 +2604,9 @@ "dev": true }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true }, "node_modules/lodash.capitalize": { @@ -8493,9 +8493,9 @@ "dev": true }, "lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true }, "lodash.capitalize": { From 1cd3ec5ecc59a56db6d84ca186c20092d13ecb08 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Wed, 15 Apr 2026 04:35:35 -0700 Subject: [PATCH 08/12] chore: crowdin distribution deprecations (#1017) --- .../cli/commands/actions/DistributionAddAction.java | 8 ++++++++ .../cli/commands/actions/DistributionEditAction.java | 7 +++++++ .../cli/commands/picocli/DistributionAddSubcommand.java | 4 ++-- .../cli/commands/picocli/DistributionEditSubcommand.java | 4 ++-- src/main/resources/messages/messages.properties | 2 ++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/crowdin/cli/commands/actions/DistributionAddAction.java b/src/main/java/com/crowdin/cli/commands/actions/DistributionAddAction.java index b5b5cee68..c645102f4 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/DistributionAddAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/DistributionAddAction.java @@ -28,6 +28,7 @@ import static com.crowdin.cli.BaseCli.RESOURCE_BUNDLE; import static com.crowdin.cli.utils.console.ExecutionStatus.OK; +import static com.crowdin.cli.utils.console.ExecutionStatus.WARNING; import static com.crowdin.client.distributions.model.ExportMode.DEFAULT; @AllArgsConstructor @@ -53,6 +54,13 @@ public void act(Outputter out, ProjectProperties pb, ClientDistribution client) ); boolean isStringsBasedProject = Objects.equals(project.getType(), Type.STRINGS_BASED); + if (exportMode != null) { + out.println(WARNING.withIcon(RESOURCE_BUNDLE.getString("warning.distribution.deprecated_export_mode"))); + } + if (files != null) { + out.println(WARNING.withIcon(RESOURCE_BUNDLE.getString("warning.distribution.deprecated_file"))); + } + if (isStringsBasedProject && exportMode != null) { throw new ExitCodeExceptionMapper.ValidationException(RESOURCE_BUNDLE.getString("error.distribution.strings_based_export_mode")); } diff --git a/src/main/java/com/crowdin/cli/commands/actions/DistributionEditAction.java b/src/main/java/com/crowdin/cli/commands/actions/DistributionEditAction.java index 23f32df23..4872db69e 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/DistributionEditAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/DistributionEditAction.java @@ -64,6 +64,13 @@ public void act(Outputter out, ProjectProperties pb, ClientDistribution client) } String existingExportMode = foundDistribution.get().getExportMode(); + if (exportMode != null) { + out.println(ExecutionStatus.WARNING.withIcon(RESOURCE_BUNDLE.getString("warning.distribution.deprecated_export_mode"))); + } + if (files != null) { + out.println(ExecutionStatus.WARNING.withIcon(RESOURCE_BUNDLE.getString("warning.distribution.deprecated_file"))); + } + List requests = new ArrayList<>(); if (name != null) { diff --git a/src/main/java/com/crowdin/cli/commands/picocli/DistributionAddSubcommand.java b/src/main/java/com/crowdin/cli/commands/picocli/DistributionAddSubcommand.java index 5bc55ecee..c2ea10910 100644 --- a/src/main/java/com/crowdin/cli/commands/picocli/DistributionAddSubcommand.java +++ b/src/main/java/com/crowdin/cli/commands/picocli/DistributionAddSubcommand.java @@ -23,10 +23,10 @@ class DistributionAddSubcommand extends ActCommandDistribution { @CommandLine.Parameters(descriptionKey = "crowdin.distribution.add.name") protected String name; - @CommandLine.Option(names = {"--export-mode"}, paramLabel = "...", descriptionKey = "crowdin.distribution.add.export-mode", defaultValue = "default", order = -2) + @CommandLine.Option(names = {"--export-mode"}, paramLabel = "...", descriptionKey = "crowdin.distribution.add.export-mode", order = -2, hidden = true) protected ExportMode exportMode; - @CommandLine.Option(names = {"--file"}, paramLabel = "...", descriptionKey = "crowdin.distribution.add.file", order = -2) + @CommandLine.Option(names = {"--file"}, paramLabel = "...", descriptionKey = "crowdin.distribution.add.file", order = -2, hidden = true) protected List files; @CommandLine.Option(names = {"--bundle-id"}, paramLabel = "...", descriptionKey = "crowdin.distribution.add.bundle-id", order = -2) diff --git a/src/main/java/com/crowdin/cli/commands/picocli/DistributionEditSubcommand.java b/src/main/java/com/crowdin/cli/commands/picocli/DistributionEditSubcommand.java index d45cf48da..a69d6834d 100644 --- a/src/main/java/com/crowdin/cli/commands/picocli/DistributionEditSubcommand.java +++ b/src/main/java/com/crowdin/cli/commands/picocli/DistributionEditSubcommand.java @@ -22,10 +22,10 @@ class DistributionEditSubcommand extends ActCommandDistribution { @CommandLine.Option(names = {"--name"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.name", order = -2) protected String name; - @CommandLine.Option(names = {"--export-mode"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.export-mode", order = -2) + @CommandLine.Option(names = {"--export-mode"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.export-mode", order = -2, hidden = true) protected ExportMode exportMode; - @CommandLine.Option(names = {"--file"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.file", order = -2) + @CommandLine.Option(names = {"--file"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.file", order = -2, hidden = true) protected List files; @CommandLine.Option(names = {"--bundle-id"}, paramLabel = "...", descriptionKey = "crowdin.distribution.edit.bundle-id", order = -2) diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index 926810f82..19001e526 100755 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -682,6 +682,8 @@ error.distribution.incorrect_bundle_id_command_usage=The '--bundle-id' is used o error.distribution.strings_based_export_mode=String-based projects do not support export mode setting error.distribution.no_edit=Specify the parameters to edit the distribution error.distribution.not_found=Couldn't find distribution with the specified hash %s +warning.distribution.deprecated_export_mode=The '--export-mode' option is deprecated and will be removed in the next major release. Use '--bundle-id' instead +warning.distribution.deprecated_file=The '--file' option is deprecated and will be removed in the next major release. Use '--bundle-id' instead error.pre_translate.directory_or_file_only=Either '--file' or '--directory' can be specified error.pre_translate.engine_id=Machine Translation should be used with the '--engine-id' parameter From 6487425a05f02635376942bec70b30267f980beb Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <29282228+andrii-bodnar@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:27:07 +0300 Subject: [PATCH 09/12] ci: switch GitHub workflows to Node 24 (#1019) --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/bump-version.yml | 2 +- .github/workflows/docs-release.yml | 2 +- .github/workflows/publish.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a272a960d..b0b512d51 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -85,7 +85,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 cache: 'npm' - uses: actions/setup-java@v5 @@ -134,7 +134,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 - name: Install the latest version of the npm CLI run: npm install -g npm@latest diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 9bf0f3d56..d1b78ad87 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 cache: 'npm' - name: Install dependencies diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index 4447c47ee..0a3b06ded 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 cache: 'npm' - uses: actions/setup-java@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ba490cfdb..3e4bb6973 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -101,7 +101,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 registry-url: https://registry.npmjs.org/ - name: Install the latest version of the npm CLI From 7a2202287f090047b2a8d36e750c42fad3f10044 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Thu, 23 Apr 2026 01:40:07 -0700 Subject: [PATCH 10/12] fix: asterisks in sources (#1015) --- .../actions/DownloadSourcesAction.java | 27 ++++++++++---- .../actions/DownloadSourcesActionTest.java | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/crowdin/cli/commands/actions/DownloadSourcesAction.java b/src/main/java/com/crowdin/cli/commands/actions/DownloadSourcesAction.java index 033e25960..995598902 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/DownloadSourcesAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/DownloadSourcesAction.java @@ -134,12 +134,21 @@ public void act(Outputter out, PropertiesWithFiles properties, ProjectClient cli if (project.isManagerAccess()) { filePaths2 = new ArrayList<>(); if (properties.getPreserveHierarchy()) { + Predicate destPredicate = fileBean.getDest() != null + ? Pattern.compile("^" + PlaceholderUtil.formatSourcePatternForRegex(Utils.noSepAtStart(Utils.normalizePath(fileBean.getDest()))) + "$").asPredicate() + : null; for (String filePathKey : filePaths.keySet()) { - String exportPattern = Utils.normalizePath(ProjectFilesUtils.getExportPattern(((File) filePaths.get(filePathKey)).getExportOptions())); - String translationPattern = TranslationsUtils.replaceDoubleAsterisk(fileBean.getSource(), fileBean.getTranslation(), filePathKey); - if (exportPattern == null || translationPattern.endsWith(exportPattern)) { + boolean matchesFileBean; + if (destPredicate != null) { + matchesFileBean = destPredicate.test(Utils.noSepAtStart(Utils.normalizePath(filePathKey))); + } else { + String exportPattern = Utils.normalizePath(ProjectFilesUtils.getExportPattern(((File) filePaths.get(filePathKey)).getExportOptions())); + String translationPattern = TranslationsUtils.replaceDoubleAsterisk(fileBean.getSource(), fileBean.getTranslation(), filePathKey); + matchesFileBean = exportPattern == null || translationPattern.endsWith(exportPattern); + } + if (matchesFileBean) { String sourceName = new java.io.File(fileBean.getSource()).getName(); - if(sourceName.equals(new java.io.File(filePathKey).getName()) || SourcesUtils.containsPattern(sourceName) || searchPattern.contains(filePathKey)) { + if (sourceName.equals(new java.io.File(filePathKey).getName()) || SourcesUtils.containsPattern(sourceName) || searchPattern.contains(filePathKey)) { filePaths2.add(filePathKey); } } @@ -176,8 +185,14 @@ public void act(Outputter out, PropertiesWithFiles properties, ProjectClient cli .map(filePath -> () -> { String fileDestination; if (fileBean.getDest() != null) { - fileDestination = SourcesUtils.replaceUnaryAsterisk(fileBean.getSource(), filePath); - fileDestination = placeholderUtil.replaceFileDependentPlaceholders(fileDestination, new java.io.File(filePath)); + if (!fileBean.getDest().contains("**") + && !fileBean.getDest().contains(PlaceholderUtil.PLACEHOLDER_ORIGINAL_PATH) + && fileBean.getSource().contains("**")) { + fileDestination = placeholderUtil.replaceFileDependentPlaceholders(fileBean.getDest(), new java.io.File(filePath)); + } else { + fileDestination = SourcesUtils.replaceUnaryAsterisk(fileBean.getSource(), filePath); + fileDestination = placeholderUtil.replaceFileDependentPlaceholders(fileDestination, new java.io.File(filePath)); + } } else { fileDestination = filePath; } diff --git a/src/test/java/com/crowdin/cli/commands/actions/DownloadSourcesActionTest.java b/src/test/java/com/crowdin/cli/commands/actions/DownloadSourcesActionTest.java index e846397c4..48012f09c 100644 --- a/src/test/java/com/crowdin/cli/commands/actions/DownloadSourcesActionTest.java +++ b/src/test/java/com/crowdin/cli/commands/actions/DownloadSourcesActionTest.java @@ -170,6 +170,41 @@ public void testDest_doubleAsterisks() throws IOException { verifyNoMoreInteractions(files); } + @Test + public void testDest_doubleAsterisksInSourceOnly() throws IOException { + PropertiesWithFiles pb = NewPropertiesWithFilesUtilBuilder + .minimalBuiltPropertiesBean( + Utils.normalizePath("/**/src/main/res/values/*trings.xml"), + Utils.normalizePath("/**/src/main/res/values-%two_letters_code%/%original_file_name%"), + null, Utils.normalizePath("/Android/%original_file_name%")) + .setBasePath(project.getBasePath()) + .setPreserveHierarchy(true) + .build(); + + ProjectClient client = mock(ProjectClient.class); + when(client.downloadFullProject(null)) + .thenReturn(ProjectBuilder.emptyProject(Long.parseLong(pb.getProjectId())) + .addDirectory("Android", 201L, null, null) + .addFile("strings.xml", "gettext", 101L, 201L, null, + Utils.normalizePath("/euro/src/main/res/values-%two_letters_code%/%original_file_name%")).build()); + URL urlMock = MockitoUtils.getMockUrl(getClass()); + when(client.downloadFile(eq(101L))) + .thenReturn(urlMock); + + FilesInterface files = mock(FilesInterface.class); + + NewAction action = + new DownloadSourcesAction(files, false, false, null, true, false, false); + action.act(Outputter.getDefault(), pb, client); + + verify(client).downloadFullProject(null); + verify(client).downloadFile(eq(101L)); + verifyNoMoreInteractions(client); + + verify(files).writeToFile(eq(Utils.joinPaths(project.getBasePath(), "Android/strings.xml")), any()); + verifyNoMoreInteractions(files); + } + @Test public void testDestAndUnaryAsterisk() throws IOException { PropertiesWithFiles pb = NewPropertiesWithFilesUtilBuilder From cfa052c9a4ab661efa4b8dced8487259c39902d3 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Thu, 23 Apr 2026 02:23:48 -0700 Subject: [PATCH 11/12] fix: show translation status 100% when nothing to translate (#1018) --- .../cli/commands/actions/StatusAction.java | 28 ++++-- .../commands/actions/StatusActionTest.java | 86 +++++++++++++++++++ 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/crowdin/cli/commands/actions/StatusAction.java b/src/main/java/com/crowdin/cli/commands/actions/StatusAction.java index 9a970ae26..17a580e6d 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/StatusAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/StatusAction.java @@ -84,13 +84,13 @@ public void act(Outputter out, ProjectProperties pb, ProjectClient client) { project.findLanguageById(pr.getLanguageId()).get().getName(), pr.getLanguageId())); if (showTranslated) { out.println(String.format(RESOURCE_BUNDLE.getString("message.translation_progress"), - pr.getTranslationProgress(), + getTranslationProgress(pr), pr.getWords().getTranslated(), pr.getWords().getTotal(), pr.getPhrases().getTranslated(), pr.getPhrases().getTotal())); } if (showApproved) { out.println(String.format(RESOURCE_BUNDLE.getString("message.approval_progress"), - pr.getApprovalProgress(), + getApprovalProgress(pr), pr.getWords().getApproved(), pr.getWords().getTotal(), pr.getPhrases().getApproved(), pr.getPhrases().getTotal())); } @@ -103,9 +103,9 @@ public void act(Outputter out, ProjectProperties pb, ProjectClient client) { progresses.forEach(pr -> { if (!plainView) { out.println(String.format(RESOURCE_BUNDLE.getString("message.item_list_with_percents"), - pr.getLanguageId(), pr.getTranslationProgress())); + pr.getLanguageId(), getTranslationProgress(pr))); } else { - out.println(String.format("%s %d", pr.getLanguageId(), pr.getTranslationProgress())); + out.println(String.format("%s %d", pr.getLanguageId(), getTranslationProgress(pr))); } }); } @@ -116,9 +116,9 @@ public void act(Outputter out, ProjectProperties pb, ProjectClient client) { progresses.forEach(pr -> { if (!plainView) { out.println(String.format(RESOURCE_BUNDLE.getString("message.item_list_with_percents"), - pr.getLanguageId(), pr.getApprovalProgress())); + pr.getLanguageId(), getApprovalProgress(pr))); } else { - out.println(String.format("%s %d", pr.getLanguageId(), pr.getApprovalProgress())); + out.println(String.format("%s %d", pr.getLanguageId(), getApprovalProgress(pr))); } }); } @@ -126,6 +126,18 @@ public void act(Outputter out, ProjectProperties pb, ProjectClient client) { } } + private static boolean hasNothingToTranslate(LanguageProgress pr) { + return pr.getWords() != null && pr.getWords().getTotal() != null && pr.getWords().getTotal() == 0; + } + + private static int getTranslationProgress(LanguageProgress pr) { + return hasNothingToTranslate(pr) ? 100 : pr.getTranslationProgress(); + } + + private static int getApprovalProgress(LanguageProgress pr) { + return hasNothingToTranslate(pr) ? 100 : pr.getApprovalProgress(); + } + private Consumer throwException(String msg) { throw new RuntimeException(msg); } @@ -135,13 +147,13 @@ private void throwExceptionIfIncomplete(Stream failedLanguageP if (showApproved && failIfIncomplete) { for (LanguageProgress p : collect) { - if (p.getApprovalProgress() < 100) { + if (getApprovalProgress(p) < 100) { throwException(RESOURCE_BUNDLE.getString("error.project_is_incomplete")); } } } else if (showTranslated && failIfIncomplete) { for (LanguageProgress p : collect) { - if (p.getTranslationProgress() < 100) { + if (getTranslationProgress(p) < 100) { throwException(RESOURCE_BUNDLE.getString("error.project_is_incomplete")); } } diff --git a/src/test/java/com/crowdin/cli/commands/actions/StatusActionTest.java b/src/test/java/com/crowdin/cli/commands/actions/StatusActionTest.java index d6e703954..193091d3f 100644 --- a/src/test/java/com/crowdin/cli/commands/actions/StatusActionTest.java +++ b/src/test/java/com/crowdin/cli/commands/actions/StatusActionTest.java @@ -70,6 +70,92 @@ public void testStatus(){ verifyNoMoreInteractions(projectClient); } + @Test + public void testStatusBranchWithNothingToTranslate() { + boolean noProgress = true; + String branchName = "release"; + boolean isVerbose = false; + boolean showTranslated = true; + boolean showApproved = false; + boolean failIfIncomplete = true; + ProjectClient projectClient = mock(ProjectClient.class); + ProjectProperties projectProperties = mock(ProjectProperties.class); + LanguageProgress languageProgress = mock(LanguageProgress.class); + Branch branch = mock(Branch.class); + NewPropertiesWithFilesUtilBuilder pbBuilder = NewPropertiesWithFilesUtilBuilder + .minimalBuiltPropertiesBean("*", Utils.PATH_SEPARATOR + "%original_file_name%-CR-%locale%") + .setBasePath(Utils.PATH_SEPARATOR); + PropertiesWithFiles pb = pbBuilder.build(); + ProjectBuilder projectBuilder = ProjectBuilder.emptyProject(Long.parseLong(pb.getProjectId())); + + when(projectClient.downloadFullProject("release")) + .thenReturn(projectBuilder.build()); + when(languageProgress.getTranslationProgress()).thenReturn(0); + when(languageProgress.getLanguageId()).thenReturn("fr-CA"); + when(languageProgress.getWords()).thenReturn(getEmptyWords()); + when(languageProgress.getPhrases()).thenReturn(getEmptyWords()); + when(branch.getId()).thenReturn(456L); + when(branch.getName()).thenReturn("release"); + when(projectClient.getBranchProgress(456L)) + .thenReturn(Collections.singletonList(languageProgress)); + when(projectClient.listBranches()) + .thenReturn(Collections.singletonList(branch)); + + Outputter out = mock(Outputter.class); + statusAction = new StatusAction(noProgress, branchName, null, null, null, isVerbose, showTranslated, showApproved, failIfIncomplete, true); + statusAction.act(out, projectProperties, projectClient); + + verify(out).println(argThat(s -> s.contains("fr-CA") && s.contains("100"))); + verify(projectClient).downloadFullProject("release"); + verify(projectClient).listBranches(); + verify(projectClient).getBranchProgress(456L); + verifyNoMoreInteractions(projectClient); + } + + @Test + public void testStatusBranchWithNothingToApprove() { + ProjectClient projectClient = mock(ProjectClient.class); + ProjectProperties projectProperties = mock(ProjectProperties.class); + LanguageProgress languageProgress = mock(LanguageProgress.class); + Branch branch = mock(Branch.class); + NewPropertiesWithFilesUtilBuilder pbBuilder = NewPropertiesWithFilesUtilBuilder + .minimalBuiltPropertiesBean("*", Utils.PATH_SEPARATOR + "%original_file_name%-CR-%locale%") + .setBasePath(Utils.PATH_SEPARATOR); + PropertiesWithFiles pb = pbBuilder.build(); + ProjectBuilder projectBuilder = ProjectBuilder.emptyProject(Long.parseLong(pb.getProjectId())); + + when(projectClient.downloadFullProject("release")) + .thenReturn(projectBuilder.build()); + when(languageProgress.getApprovalProgress()).thenReturn(0); + when(languageProgress.getLanguageId()).thenReturn("fr-CA"); + when(languageProgress.getWords()).thenReturn(getEmptyWords()); + when(languageProgress.getPhrases()).thenReturn(getEmptyWords()); + when(branch.getId()).thenReturn(456L); + when(branch.getName()).thenReturn("release"); + when(projectClient.getBranchProgress(456L)) + .thenReturn(Collections.singletonList(languageProgress)); + when(projectClient.listBranches()) + .thenReturn(Collections.singletonList(branch)); + + Outputter out = mock(Outputter.class); + statusAction = new StatusAction(true, "release", null, null, null, false, false, true, true, true); + statusAction.act(out, projectProperties, projectClient); + + verify(out).println(argThat(s -> s.contains("fr-CA") && s.contains("100"))); + verify(projectClient).downloadFullProject("release"); + verify(projectClient).listBranches(); + verify(projectClient).getBranchProgress(456L); + verifyNoMoreInteractions(projectClient); + } + + private Progress.Words getEmptyWords() { + Progress.Words words = new Progress.Words(); + words.setApproved(0); + words.setTotal(0); + words.setTranslated(0); + return words; + } + private Progress.Words getWords(){ Progress.Words words = new Progress.Words(); words.setApproved(1); From 430c34ed3fdfe3442a1bb88c609a76b21648f311 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 23 Apr 2026 09:44:19 +0000 Subject: [PATCH 12/12] chore(release): version 4.14.2 [skip ci] --- CHANGELOG.md | 9 +++++++++ build.gradle | 2 +- package-lock.json | 4 ++-- package.json | 2 +- packages/aur/pkgbuild/PKGBUILD | 2 +- packages/chocolatey/crowdin-cli.nuspec | 2 +- packages/chocolatey/tools/chocolateyinstall.ps1 | 2 +- packages/exe/CrowdinCLIInstaller.iss | 2 +- src/main/resources/crowdin.properties | 2 +- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5964d2ba4..f0881eda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [4.14.2](https://github.com/crowdin/crowdin-cli/compare/4.14.1...4.14.2) (2026-04-23) + + +### Bug Fixes + +* asterisks in sources ([#1015](https://github.com/crowdin/crowdin-cli/issues/1015)) ([7a22022](https://github.com/crowdin/crowdin-cli/commit/7a2202287f090047b2a8d36e750c42fad3f10044)) +* disabled target languages warning ([#1008](https://github.com/crowdin/crowdin-cli/issues/1008)) ([d608b11](https://github.com/crowdin/crowdin-cli/commit/d608b11982da6a3c7e3b750762fee8b31c36f251)) +* show translation status 100% when nothing to translate ([#1018](https://github.com/crowdin/crowdin-cli/issues/1018)) ([cfa052c](https://github.com/crowdin/crowdin-cli/commit/cfa052c9a4ab661efa4b8dced8487259c39902d3)) + ## [4.14.1](https://github.com/crowdin/crowdin-cli/compare/4.14.0...4.14.1) (2026-03-13) diff --git a/build.gradle b/build.gradle index 2dae8178f..7c87ef6d2 100755 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group 'com.crowdin' -version '4.14.1' +version '4.14.2' sourceCompatibility = '17' diff --git a/package-lock.json b/package-lock.json index 4f95945b9..b6e7490c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@crowdin/cli", - "version": "4.14.1", + "version": "4.14.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@crowdin/cli", - "version": "4.14.1", + "version": "4.14.2", "license": "MIT", "dependencies": { "command-exists-promise": "^2.0.2", diff --git a/package.json b/package.json index 7dafc6e5b..48640193b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "type": "git", "url": "git+https://github.com/crowdin/crowdin-cli.git" }, - "version": "4.14.1", + "version": "4.14.2", "bin": { "crowdin": "jdeploy-bundle/jdeploy.js" }, diff --git a/packages/aur/pkgbuild/PKGBUILD b/packages/aur/pkgbuild/PKGBUILD index cc34885a9..c1b32ac76 100644 --- a/packages/aur/pkgbuild/PKGBUILD +++ b/packages/aur/pkgbuild/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Senya pkgname=crowdin-cli -pkgver=4.14.1 +pkgver=4.14.2 pkgrel=1 pkgdesc="Command line tool that allows you to manage and synchronize localization resources with your Crowdin project" url="https://support.crowdin.com/cli-tool/" diff --git a/packages/chocolatey/crowdin-cli.nuspec b/packages/chocolatey/crowdin-cli.nuspec index b5e37804b..7a63025e4 100644 --- a/packages/chocolatey/crowdin-cli.nuspec +++ b/packages/chocolatey/crowdin-cli.nuspec @@ -2,7 +2,7 @@ crowdin-cli - 4.14.1 + 4.14.2 https://github.com/crowdin/crowdin-cli/tree/main/packages/chocolatey crowdin Crowdin CLI (Portable) diff --git a/packages/chocolatey/tools/chocolateyinstall.ps1 b/packages/chocolatey/tools/chocolateyinstall.ps1 index ec765ceb1..bbf3abfee 100644 --- a/packages/chocolatey/tools/chocolateyinstall.ps1 +++ b/packages/chocolatey/tools/chocolateyinstall.ps1 @@ -6,7 +6,7 @@ $packageVersion = $env:ChocolateyPackageVersion $packageArgs = @{ packageName = $packageName unzipLocation = $toolsDir - url = 'https://github.com/crowdin/crowdin-cli/releases/download/4.14.1/crowdin-cli.zip' + url = 'https://github.com/crowdin/crowdin-cli/releases/download/4.14.2/crowdin-cli.zip' checksum = '820f5c04dd0de0a1875ba8b9eae3b9f9413560914c6b6690f27255f093d2be65' checksumType = 'sha256' } diff --git a/packages/exe/CrowdinCLIInstaller.iss b/packages/exe/CrowdinCLIInstaller.iss index d528f5f22..7db3b9861 100644 --- a/packages/exe/CrowdinCLIInstaller.iss +++ b/packages/exe/CrowdinCLIInstaller.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Crowdin" -#define MyAppVersion "4.14.1" +#define MyAppVersion "4.14.2" #define MyAppPublisher "OU Crowdin" #define MyAppURL "https://crowdin.github.io/crowdin-cli" #define MyAppExeName "crowdin-cli.jar" diff --git a/src/main/resources/crowdin.properties b/src/main/resources/crowdin.properties index ce1b468bb..272305f5a 100755 --- a/src/main/resources/crowdin.properties +++ b/src/main/resources/crowdin.properties @@ -1,4 +1,4 @@ application.name=crowdin-cli -application.version=4.14.1 +application.version=4.14.2 application.base_url=https://api.crowdin.com application.version_file_url=https://github.com/crowdin/crowdin-cli/releases/latest/download/version.txt