diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 20eed5a..a046492 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -20,9 +20,9 @@ jobs:
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
name: "Checkout"
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v6
name: "Install Java"
with:
java-version: '21'
@@ -39,13 +39,13 @@ jobs:
- name: 'Smoke test JAR'
run: ./scripts/smoke-test.sh java -jar target/simplelocalize-cli-${{env.VERSION}}.jar
- name: 'Upload artifact'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: 'simplelocalize-cli-${{env.VERSION}}.jar'
path: 'target/simplelocalize-cli-${{env.VERSION}}.jar'
- name: 'Upload JAR to Release'
if: github.event_name == 'release'
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v3
with:
files: 'target/simplelocalize-cli-${{env.VERSION}}.jar'
env:
@@ -79,7 +79,7 @@ jobs:
downloadFilename: 'simplelocalize-cli-windows.exe'
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
name: "Checkout"
- name: "Print architecture"
run: uname -m
@@ -104,13 +104,13 @@ jobs:
codesign --force --deep --sign - ./target/${{ matrix.downloadFilename }}
codesign --verify --verbose ./target/${{ matrix.downloadFilename }}
- name: "Upload artifact"
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: simplelocalize-cli-${{ matrix.label }}
path: ./target/${{ matrix.downloadFilename }}
- name: 'Upload Executable to Release'
if: github.event_name == 'release'
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v3
with:
files: './target/${{ matrix.downloadFilename }}'
env:
@@ -122,7 +122,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Download all artifacts"
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
@@ -131,13 +131,13 @@ jobs:
cd dist
zip -j "../simplelocalize-cli-${{ needs.build-jar.outputs.version }}.zip" *
- name: "Upload bundle artifact"
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
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
+ uses: softprops/action-gh-release@v3
with:
files: 'simplelocalize-cli-${{ needs.build-jar.outputs.version }}.zip'
env:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 0f8b062..6d9745e 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -16,17 +16,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- - uses: actions/setup-java@v4
+ - uses: actions/setup-java@v6
name: "Install Java"
with:
java-version: '21'
distribution: 'corretto'
cache: 'maven'
- name: Cache SonarCloud packages
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
diff --git a/.github/workflows/test-executables.yml b/.github/workflows/test-executables.yml
index ed069a0..87e8a37 100644
--- a/.github/workflows/test-executables.yml
+++ b/.github/workflows/test-executables.yml
@@ -18,9 +18,9 @@ jobs:
fail-fast: false
matrix:
os: [ macos-26, macos-26-intel, ubuntu-24.04-arm, ubuntu-latest ]
- version: [ '2.10', '2.9', '2.8' ]
+ version: [ '2.11', '2.10' ]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Test on ${{ matrix.os }}
run: |
curl --fail -s https://get.simplelocalize.io/${{ matrix.version }}/install | bash
@@ -32,9 +32,9 @@ jobs:
fail-fast: false
matrix:
os: [ windows-latest ]
- version: [ '2.10', '2.9', '2.8' ]
+ version: [ '2.11', '2.10' ]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Test on ${{ matrix.os }}
run: |
. { iwr -useb https://get.simplelocalize.io/${{ matrix.version }}/install-windows } | iex;
diff --git a/README.md b/README.md
index 86c6b60..d7266a4 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ and it's a great tool for CI/CD pipelines and localization automation.
- [Synchronize files](https://simplelocalize.io/auto-translation/) - keep in sync all your translation files
- [Auto-translate](https://simplelocalize.io/auto-translation/) - auto-translate texts in seconds
- [Host translations](https://simplelocalize.io/translation-hosting/) - manage your hosted translations
-- [GitHub Actions support](https://github.com/simplelocalize/github-action-cli/) - seemles integration with GitHub Actions
+- [GitHub Actions support](https://github.com/simplelocalize/github-action-cli/) - seamless integration with GitHub Actions
## Installation
diff --git a/pom.xml b/pom.xml
index be043fd..92ccb0a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.simplelocalize
simplelocalize-cli
- 2.11.1
+ 2.12.0
jar
simplelocalize-cli
Official SimpleLocalize Command Line Interface
@@ -254,6 +254,7 @@
${graalvm.quickBuild}
--no-fallback
+ -march=compatibility
--enable-http
--enable-https
-H:IncludeResources=logback.xml
diff --git a/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java b/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java
index e310f3b..e4dee75 100644
--- a/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java
+++ b/src/main/java/io/simplelocalize/cli/SimplelocalizeCliCommand.java
@@ -462,7 +462,7 @@ public void publish(
@Command(
name = "purge",
- description = "Purge translations from Translation Hosting. Use 'simplelocalize purge --help' to learn more about the parameters.")
+ description = "Purge translations from the project. Use 'simplelocalize purge --help' to learn more about the parameters.")
public void purge(
@Option(names = {"--apiKey"}, description = "Project API Key") String apiKey,
@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 dcf3642..98e6eb0 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.11.1";
+ public static final String NUMBER = "2.12.0";
private Version()
{
diff --git a/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeClient.java b/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeClient.java
index 8187cda..7e84638 100644
--- a/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeClient.java
+++ b/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeClient.java
@@ -97,6 +97,19 @@ private String prettyPrintJson(DocumentContext json) throws JsonProcessingExcept
return mapper.writeValueAsString(data);
}
+ public List fetchMultiLanguageFileFormats() throws IOException, InterruptedException
+ {
+ URI uri = uriFactory.buildFileFormatsUri();
+ HttpRequest httpRequest = httpRequestFactory.createGetRequest(uri).build();
+ HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+ if (httpResponse.statusCode() != 200)
+ {
+ throw new ApiRequestException(readErrorMessage(httpResponse));
+ }
+ DocumentContext json = JsonPath.parse(httpResponse.body());
+ return json.read("$.data.fileFormats[?(@.multiLanguageSupport == true)].value");
+ }
+
public List exportFiles(ExportRequest exportRequest) throws IOException, InterruptedException
{
URI downloadUri = uriFactory.buildDownloadUri(exportRequest);
@@ -244,16 +257,22 @@ private void throwOnError(HttpResponse> httpResponse)
{
if (httpResponse.statusCode() != 200)
{
- String message = tryReadErrorMessage(httpResponse);
- if (StringUtils.isBlank(message))
- {
- message = "Unknown error, HTTP Status: " + httpResponse.statusCode();
- }
+ String message = readErrorMessage(httpResponse);
log.error("Request failed: {}", message);
throw new ApiRequestException(message);
}
}
+ private String readErrorMessage(HttpResponse> httpResponse)
+ {
+ String message = tryReadErrorMessage(httpResponse);
+ if (StringUtils.isBlank(message))
+ {
+ message = "Unknown error, HTTP Status: " + httpResponse.statusCode();
+ }
+ return message;
+ }
+
private String tryReadErrorMessage(HttpResponse> httpResponse)
{
String stringBody = safeCastHttpBodyToString(httpResponse.body());
diff --git a/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeUriFactory.java b/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeUriFactory.java
index 3c5e2c5..609359d 100644
--- a/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeUriFactory.java
+++ b/src/main/java/io/simplelocalize/cli/client/SimpleLocalizeUriFactory.java
@@ -147,6 +147,11 @@ public URI buildStacktraceUri()
return URI.create(baseUrl + "/cli/v1/stacktrace");
}
+ URI buildFileFormatsUri()
+ {
+ return URI.create(baseUrl + "/api/v1/file-formats");
+ }
+
URI buildGetRunningAutoTranslationJobsUri()
{
return URI.create(baseUrl + "/api/v2/jobs?status=RUNNING&type=AUTO_TRANSLATION");
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
index e63ecbc..264a0f8 100644
--- a/src/main/java/io/simplelocalize/cli/client/dto/proxy/LanguageTransform.java
+++ b/src/main/java/io/simplelocalize/cli/client/dto/proxy/LanguageTransform.java
@@ -4,6 +4,8 @@
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.StringJoiner;
+
/**
* 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
@@ -17,4 +19,13 @@ public class LanguageTransform
{
private String languageKey;
private String placeholder;
+
+ @Override
+ public String toString()
+ {
+ return new StringJoiner(", ", "(", ")")
+ .add("languageKey='" + languageKey + "'")
+ .add("placeholder='" + placeholder + "'")
+ .toString();
+ }
}
diff --git a/src/main/java/io/simplelocalize/cli/command/UploadCommand.java b/src/main/java/io/simplelocalize/cli/command/UploadCommand.java
index c01a2f4..b7af926 100644
--- a/src/main/java/io/simplelocalize/cli/command/UploadCommand.java
+++ b/src/main/java/io/simplelocalize/cli/command/UploadCommand.java
@@ -18,10 +18,12 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
+import java.util.Locale;
public class UploadCommand implements CliCommand
{
private static final Logger log = LoggerFactory.getLogger(UploadCommand.class);
+ private static final List FALLBACK_MULTI_LANGUAGE_FILE_FORMATS = List.of("multi-language-json", "project-json", "excel", "csv-translations", "tsv", "localizable-xcstrings");
private final FileListReader fileListReader = new FileListReader();
private final SimpleLocalizeClient client;
private final Configuration configuration;
@@ -215,8 +217,7 @@ private void validateConfiguration(Configuration configuration)
throw new ConfigurationException("You cannot use {ns} placeholder in uploadPath and namespace parameter at the same time");
}
- final boolean isMultiLanguage = isMultiLanguage(configuration);
- if (isMultiLanguage && hasLanguagePlaceholder)
+ if (hasLanguagePlaceholder && isMultiLanguage(configuration))
{
throw new ConfigurationException("You cannot use language key parameter with multi-language file formats");
}
@@ -224,23 +225,59 @@ private void validateConfiguration(Configuration configuration)
private boolean isMultiLanguage(Configuration configuration)
{
- final List multiLanguageFileFormats = List.of("multi-language-json", "excel", "csv-translations", "tsv", "localizable-xcstrings");
- for (String uploadFormat : multiLanguageFileFormats)
+ final List uploadOptions = configuration.getUploadOptions();
+ for (String uploadOption : uploadOptions)
{
- if (uploadFormat.equalsIgnoreCase(configuration.getUploadFormat()))
+ if (uploadOption.equalsIgnoreCase("MULTI_LANGUAGE"))
{
return true;
}
}
- final List uploadOptions = configuration.getUploadOptions();
- for (String uploadOption : uploadOptions)
+ final String uploadFormat = normalizeFileFormat(configuration.getUploadFormat());
+ for (String multiLanguageFileFormat : getMultiLanguageFileFormats())
{
- if (uploadOption.equalsIgnoreCase("MULTI_LANGUAGE"))
+ if (normalizeFileFormat(multiLanguageFileFormat).equals(uploadFormat))
{
return true;
}
}
return false;
}
+
+ private String normalizeFileFormat(String fileFormat)
+ {
+ if (fileFormat == null)
+ {
+ return "";
+ }
+ return fileFormat.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
+ }
+
+ private List getMultiLanguageFileFormats()
+ {
+ try
+ {
+ final List fileFormats = client.fetchMultiLanguageFileFormats();
+ if (fileFormats != null && !fileFormats.isEmpty())
+ {
+ return fileFormats;
+ }
+ log.warn("Could not load multi-language file formats from the API (empty response), using the list built into this CLI version");
+ } catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ logFileFormatsFallback(e);
+ } catch (Exception e)
+ {
+ logFileFormatsFallback(e);
+ }
+ return FALLBACK_MULTI_LANGUAGE_FILE_FORMATS;
+ }
+
+ private void logFileFormatsFallback(Exception e)
+ {
+ final String reason = StringUtils.isNotEmpty(e.getMessage()) ? e.getMessage() : e.getClass().getSimpleName();
+ log.warn("Could not load multi-language file formats from the API ({}), using the list built into this CLI version", reason);
+ }
}
diff --git a/src/test/java/io/simplelocalize/cli/client/SimpleLocalizeClientTest.java b/src/test/java/io/simplelocalize/cli/client/SimpleLocalizeClientTest.java
index e261c08..17a7fcb 100644
--- a/src/test/java/io/simplelocalize/cli/client/SimpleLocalizeClientTest.java
+++ b/src/test/java/io/simplelocalize/cli/client/SimpleLocalizeClientTest.java
@@ -388,4 +388,63 @@ void shouldGiveUpDownloadAfterMaxRetryAttempts()
assertThat(Path.of(downloadPath)).doesNotExist();
}
+ @Test
+ void shouldFetchMultiLanguageFileFormats() throws Exception
+ {
+ //given
+ SimpleLocalizeClient client = new SimpleLocalizeClient(MOCK_SERVER_BASE_URL, "81707741b64e68427e1a2c20e75095b1");
+ // Response shape of GET /api/v1/file-formats, wrapped by the API's global response wrapper
+ String responseBody = """
+ {
+ "msg": "OK",
+ "status": 200,
+ "data": {
+ "fileFormats": [
+ {"value": "java-properties", "multiLanguageSupport": false},
+ {"value": "csv", "multiLanguageSupport": false, "deprecated": true},
+ {"value": "excel", "multiLanguageSupport": true},
+ {"value": "multi-language-json", "multiLanguageSupport": true},
+ {"value": "ini", "multiLanguageSupport": false, "beta": true}
+ ]
+ }
+ }
+ """;
+ mockServer.when(request()
+ .withMethod("GET")
+ .withPath("/api/v1/file-formats"),
+ Times.exactly(1))
+ .respond(
+ response()
+ .withStatusCode(200)
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody(responseBody)
+ );
+
+ //when
+ List result = client.fetchMultiLanguageFileFormats();
+
+ //then
+ assertThat(result).containsExactly("excel", "multi-language-json");
+ }
+
+ @Test
+ void shouldThrowWhenFetchingFileFormatsFails()
+ {
+ //given
+ SimpleLocalizeClient client = new SimpleLocalizeClient(MOCK_SERVER_BASE_URL, "81707741b64e68427e1a2c20e75095b1");
+ mockServer.when(request()
+ .withMethod("GET")
+ .withPath("/api/v1/file-formats"),
+ Times.exactly(1))
+ .respond(
+ response()
+ .withStatusCode(500)
+ .withContentType(MediaType.APPLICATION_JSON)
+ .withBody("{\"msg\": \"Internal server error\"}")
+ );
+
+ //when & then
+ Assertions.assertThatThrownBy(client::fetchMultiLanguageFileFormats)
+ .isInstanceOf(ApiRequestException.class);
+ }
}
diff --git a/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java b/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java
index 1cd5a55..87b88fb 100644
--- a/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java
+++ b/src/test/java/io/simplelocalize/cli/command/UploadCommandTest.java
@@ -5,6 +5,7 @@
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 io.simplelocalize.cli.exception.ConfigurationException;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -15,6 +16,7 @@
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@@ -262,4 +264,129 @@ public void shouldUseLanguageMappingReverseValueForSingleMappedFile() throws Exc
.orElseThrow();
Assertions.assertThat(enUsRequest.path().toString()).contains("values-en");
}
+ @Test
+ public void shouldUseMultiLanguageFileFormatsReturnedByApi() throws Exception
+ {
+ //given
+ // 'json' is not multi-language in the CLI's built-in list, so rejecting it proves the API list was used
+ Mockito.when(client.fetchMultiLanguageFileFormats()).thenReturn(List.of("json"));
+
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("json");
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+
+ //then
+ Assertions.assertThatThrownBy(uploadCommand::invoke)
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("multi-language file formats");
+ Mockito.verify(client, times(0)).uploadFile(any());
+ }
+
+ @Test
+ public void shouldAcceptFormatThatApiDoesNotReportAsMultiLanguage() throws Exception
+ {
+ //given
+ // 'excel' is multi-language in the built-in list, but the API is the source of truth
+ Mockito.when(client.fetchMultiLanguageFileFormats()).thenReturn(List.of("tsv"));
+
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("excel");
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+ uploadCommand.invoke();
+
+ //then
+ Mockito.verify(client, times(12)).uploadFile(any());
+ }
+
+ @Test
+ public void shouldFallBackToBuiltInFormatsWhenApiRequestFails() throws Exception
+ {
+ //given
+ Mockito.when(client.fetchMultiLanguageFileFormats()).thenThrow(new IOException("connection refused"));
+
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("excel");
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+
+ //then
+ Assertions.assertThatThrownBy(uploadCommand::invoke)
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("multi-language file formats");
+ Mockito.verify(client, times(0)).uploadFile(any());
+ }
+
+ @Test
+ public void shouldFallBackToBuiltInFormatsWhenApiReturnsNothing() throws Exception
+ {
+ //given
+ Mockito.when(client.fetchMultiLanguageFileFormats()).thenReturn(List.of());
+
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("project-json");
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+
+ //then
+ Assertions.assertThatThrownBy(uploadCommand::invoke)
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("multi-language file formats");
+ Mockito.verify(client, times(0)).uploadFile(any());
+ }
+
+ @Test
+ public void shouldTreatMultiLanguageUploadOptionAsMultiLanguageWithoutCallingApi() throws Exception
+ {
+ //given
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("android");
+ configuration.setUploadOptions(List.of("MULTI_LANGUAGE"));
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+
+ //then
+ Assertions.assertThatThrownBy(uploadCommand::invoke)
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("multi-language file formats");
+ Mockito.verify(client, times(0)).fetchMultiLanguageFileFormats();
+ }
+
+ @Test
+ public void shouldUnderstandEnumNamesReturnedByOlderApiVersions() throws Exception
+ {
+ //given
+ // API versions before the file-formats fix report enum names instead of format values
+ Mockito.when(client.fetchMultiLanguageFileFormats()).thenReturn(List.of("MULTI_LANGUAGE_JSON", "LOCALIZABLE_XC_STRINGS", "EXCEL"));
+
+ Configuration configuration = Configuration.defaultConfiguration();
+ configuration.setApiKey("my-api-key");
+ configuration.setUploadPath("./junit/download-test/values-{lang}/strings.xml");
+ configuration.setUploadFormat("localizable-xcstrings");
+
+ //when
+ UploadCommand uploadCommand = new UploadCommand(client, configuration);
+
+ //then
+ Assertions.assertThatThrownBy(uploadCommand::invoke)
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("multi-language file formats");
+ Mockito.verify(client, times(0)).uploadFile(any());
+ }
}