Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand All @@ -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'
Expand Down Expand Up @@ -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 }}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions junit/download-retry/file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "key": "value" }
1 change: 1 addition & 0 deletions junit/download-test/file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 3 additions & 0 deletions junit/lang-region-codes/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "English"
}
3 changes: 3 additions & 0 deletions junit/lang-region-codes/es_419.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Spanish (Latin America)"
}
3 changes: 3 additions & 0 deletions junit/lang-region-codes/pt-BR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Portuguese (Brazil)"
}
3 changes: 3 additions & 0 deletions junit/lang-region-codes/zh-Hant-TW.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Chinese (Traditional, Taiwan)"
}
36 changes: 20 additions & 16 deletions junit/mock-server/test.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions junit/multi-lang-nested/en/app/settings/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Home"
}
3 changes: 3 additions & 0 deletions junit/multi-lang-nested/en/toast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Toast"
}
3 changes: 3 additions & 0 deletions junit/multi-lang-nested/fr/app/settings/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Home"
}
3 changes: 3 additions & 0 deletions junit/multi-lang-nested/fr/toast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Toast"
}
3 changes: 3 additions & 0 deletions junit/nested-namespaces/en/app/settings/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Home"
}
3 changes: 3 additions & 0 deletions junit/nested-namespaces/en/components/impact/ProjectItem.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Project item"
}
3 changes: 3 additions & 0 deletions junit/nested-namespaces/en/toast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Toast"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title=Settings
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title=Settings
1 change: 1 addition & 0 deletions junit/ns-dir-lang-suffix/home/messages_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title=Home
1 change: 1 addition & 0 deletions junit/ns-dir-lang-suffix/home/messages_pl.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title=Home
3 changes: 3 additions & 0 deletions junit/ns-nested-lang-filename/common/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Common"
}
3 changes: 3 additions & 0 deletions junit/ns-nested-lang-filename/common/pl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Common"
}
3 changes: 3 additions & 0 deletions junit/ns-nested-lang-filename/mobile/settings/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Settings"
}
3 changes: 3 additions & 0 deletions junit/ns-nested-lang-filename/mobile/settings/pl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "Settings"
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.simplelocalize</groupId>
<artifactId>simplelocalize-cli</artifactId>
<version>2.10.0</version>
<version>2.11.0</version>
<packaging>jar</packaging>
<name>simplelocalize-cli</name>
<description>Official SimpleLocalize Command Line Interface</description>
Expand Down
19 changes: 17 additions & 2 deletions simplelocalize-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
#########################################
Expand All @@ -67,5 +84,3 @@ projectType: yahoo/react-intl
ignoreKeys:
- 'WELCOME'
- 'ABOUT-US'


Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public void download(
@Option(names = {"--downloadOptions", "--options"}, split = ",", description = "(Optional) Download options, use comma separated values for multiple options") List<String> downloadOptions,
@Option(names = {"--downloadTags", "--tags"}, split = ",", description = "(Optional) Download translation keys with given tags") List<String> 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<String> 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<String> 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
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/simplelocalize/cli/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/main/java/io/simplelocalize/cli/client/dto/proxy/Mappings.java
Original file line number Diff line number Diff line change
@@ -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<LanguageTransform> lang = new ArrayList<>();
}
29 changes: 28 additions & 1 deletion src/main/java/io/simplelocalize/cli/command/DownloadCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,12 +101,37 @@ public void invoke() throws IOException, InterruptedException
.withSort(sort)
.withTags(tags)
.build();
final List<LanguageTransform> languageTransforms = configuration.getMappings().getLang();
if (!languageTransforms.isEmpty())
{
log.info("Language mapping: {}", languageTransforms);
}

final List<DownloadableFile> 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<LanguageTransform> 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();
}

}
11 changes: 10 additions & 1 deletion src/main/java/io/simplelocalize/cli/command/UploadCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,6 +90,12 @@ public void invoke() throws IOException, InterruptedException
log.info("Options: {}", uploadOptions);
}

final List<LanguageTransform> languageTransforms = configuration.getMappings().getLang();
if (!languageTransforms.isEmpty())
{
log.info("Language mapping: {}", languageTransforms);
}

final List<FileToUpload> filesToUpload = fileListReader.findFilesToUpload(uploadPath);
if (filesToUpload.size() > 1)
{
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/io/simplelocalize/cli/io/FileListReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ private Matcher getMatcher(String input, String pattern)
.replace("\\", "\\\\") // escape backslash
.replace(NAMESPACE_TEMPLATE_KEY, "(?<ns>.*)")
.replace(TRANSLATION_KEY_TEMPLATE_KEY, "(?<translationKey>.*)")
.replace(LANGUAGE_TEMPLATE_KEY, "(?<lang>.*)");
// 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, "(?<lang>[^/\\\\]*)");
Pattern regex = Pattern.compile(replace);
return regex.matcher(input);
}
Expand Down
Loading
Loading