GitHub action to download translation files from Lokalise TMS to your GitHub repository in the form of a pull request.
- Step-by-step tutorial covering the usage of this action is available on Lokalise Developer Hub
- If you're looking for an in-depth tutorial, check out our blog post
To upload translation files from GitHub to Lokalise, use the lokalise-push-action.
To find documentation for the stable version 3, browse the v3 tag.
Use this action in the following way:
name: Demo pull with tags
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Pull from Lokalise
uses: lokalise/[email protected]
with:
api_token: ${{ secrets.LOKALISE_API_TOKEN }}
project_id: LOKALISE_PROJECT_ID
base_lang: en
translations_path: |
TRANSLATIONS_PATH1
TRANSLATIONS_PATH2
file_format: json
additional_params: >
{
"indentation": "2sp",
"export_empty_as": "skip",
"export_sort": "a_z",
"replace_breaks": false,
"language_mapping": [
{"original_language_iso": "en_US", "custom_language_iso": "en-US"}
]
}Before running this action, ensure that your translation keys in Lokalise are properly assigned with relevant filenames and tags.
By default, the action filters downloaded keys based on a tag that matches the Git branch name used to trigger the workflow. This is done automatically via the "include_tags": [BRANCH_NAME] API parameter.
For example, if the action is triggered from the hub branch on GitHub, it will download only the keys tagged with hub: "include_tags": ["hub"]. If no keys with the specified tag are found, the action will terminate.
To disable this automatic filtering or use custom tags, set the skip_include_tags parameter to true. You can also provide your own tags via the additional_params field:
skip_include_tags: true
additional_params: >
{
"include_tags": ["release-2025-08-19"]
}If you specify locales as the translations_path, your keys must include filenames that align with this structure, such as:
locales/%LANG_ISO%.jsonlocales/%LANG_ISO%/main.xml
Here:
%LANG_ISO%will be replaced with the language code (e.g.,en,fr, etc.).
If the filenames do not include the correct directory prefix (like locales/), the action will fail to compare the downloaded files with the existing files in your translations_path. In this case, the workflow logs will show the message: "No changes detected in translation files.".
To avoid this, double-check that your Lokalise filenames match the expected directory structure.
Note that by default the action adds the "original_filenames": true and "directory_prefix": "/" API parameters. To disable this behavior, set the skip_original_filenames to true.
You'll need to provide some parameters for the action. These can be set as environment variables, secrets, or passed directly. Refer to the General setup section for detailed instructions.
api_token— Lokalise API token with read/write permissions.project_id— Your Lokalise project ID.translations_path— One or more paths to your translation files. Do not provide the actual filenames here. For example, if your translations are stored in thelocalesfolder at the project root, uselocales. Defaults tolocales.file_format— Defines the format of your translation files, such asjsonfor JSON files. Defaults tojson. This format determines how translation files are processed and also influences the file extension used when searching for them. However, some specific formats, such asjson_structured, may still be downloaded with a generic.jsonextension. If you're using such a format, make sure to set thefile_extparameter explicitly to match the correct extension for your files.base_lang— Your project base language, such asenfor English. Defaults toen.file_ext(not strictly mandatory but recommended) — One or more custom file extensions to use when searching for translation files (without leading dot, e.g.jsonoryml). By default, the extension is inferred from thefile_formatvalue. However, for certain formats (e.g.json_structured) or mixed bundles (e.g. iOS, which uses both.stringsand.stringsdict), the downloaded files may still have a generic extension or require multiple extensions. In such cases, this parameter allows specifying the correct extension(s) manually to ensure proper file matching.
file_ext: json
# Or (useful when the bundle contains miltiple extensions)
file_ext: |
strings
stringsdictasync_mode— Download translations in asynchronous mode. Not recommended for small projects but required for larger ones (>= 10 000 key-language pairs). Defaults tofalse.flat_naming— Use flat naming convention. Set totrueif your translation files follow a flat naming pattern likelocales/en.jsoninstead oflocales/en/file.json. Defaults tofalse.skip_include_tags— Skip setting the"include_tags"param during download. This will download all translation keys for the specified format, regardless of tags.skip_original_filenames— Skips setting the"original_filenames": trueand"directory_prefix": "/"params during download. You can disable original filenames by setting"original_filenames": falseexplicitly viaadditional_params.additional_params— Extra parameters to pass when sending File download API request. Must be valid JSON. For example, you can use"indentation": "2sp"to manage indentation. Defaults to an empty string. Multiple params can be specified:
additional_params: >
{
"indentation": "2sp",
"export_empty_as": "skip",
"export_sort": "a_z",
"replace_breaks": false,
"include_tags": ["release-2025-08-19"],
"language_mapping": [
{"original_language_iso": "en_US", "custom_language_iso": "en-US"}
]
}post_process_command— A shell command that runs after pulling translation files from Lokalise but before committing them. This allows you to perform custom transformations, cleanup, replacements, or validations on the downloaded files. The command is executed in the root of your repository and has access to several environment variables (TRANSLATIONS_PATH,BASE_LANG,FILE_FORMAT,FILE_EXT,FLAT_NAMING,PLATFORM).- Please note that this is an experimental feature. You are fully responsible for the logic and behavior of any script executed through this option. These scripts run in your own repository context, under your control. If something breaks or behaves unexpectedly, we cannot guarantee support or ensure the security of the code being executed.
- This is executed inside a Bash shell (
shell: bash) therefore your command must be runnable from Bash. If you need a different interpreter or shell, call it explicitly, for examplepost_process_command: "zsh -c 'source ~/.zshrc && run_my_script'". - If your command requires a custom interpreter (e.g. running tools that are not available by default on GitHub-hosted runners), you are responsible for setting it up yourself before the command is executed.
# This will run replace_test.py file from the scripts folder in the root of your repo
post_process_command: "python scripts/replace_test.py"
# Or using a simple shell one-liner:
post_process_command: "sed -i 's/test/REPLACED/g' messages/fr.json"
# You can also run custom tools or binaries:
post_process_command: "./scripts/postprocess"Then, for example, code post-processing logic inside the ./scripts/replace_test.py file:
def replace_values(obj):
if isinstance(obj, dict):
return {k: replace_values(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_values(v) for v in obj]
elif isinstance(obj, str):
return obj.replace("test", "REPLACED")
else:
return obj
# TRANSLATIONS_PATH, FILE_EXT are set for you
translations_path = os.getenv("TRANSLATIONS_PATH", "locales")
file_ext = os.getenv("FILE_EXT", "json")
file_path = os.path.join(translations_path, f"fr.{file_ext}")
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(replace_values(data), f, ensure_ascii=False, indent=2)post_process_strict— Whether to fail the workflow if thepost_process_commandfails (non-zero exit code). If set totrue, the workflow will exit immediately on failure. Defaults tofalse.
always_pull_base— By default, changes in the base language translation files (defined by thebase_langoption) are ignored when checking for updates. Set this option totrueto include changes in the base language translations in the pull request. Defaults tofalse.
max_retries— Maximum number of retries on rate limit (HTTP 429) and other retryable errors. Defaults to3.sleep_on_retry— Number of seconds to sleep before retrying on retryable errors (exponential backoff applies). Defaults to1.http_timeout— Timeout in seconds for every HTTP operation (requesting bundle, downloading archive, etc.). Defaults to120.async_poll_initial_wait— Number of seconds to wait before polling the async download process for the first time. Has no effect if theasync_modeis disabled. Defaults to1.async_poll_max_wait— Timeout for polling the async download process. Has no effect if theasync_modeis disabled. Defaults to120.download_timeout— Timeout in seconds for the whole download and unzip operation. Defaults to600.
git_user_name— Optional Git username for commits. Defaults to the GitHub actor of the workflow run. Handy for using a specific identity (e.g., "Localization Bot").git_user_email— Optional Git email for commits. Defaults to a noreply address based on the username (e.g.,[email protected]). Useful for cleaner commit metadata or bot identities.
git_commit_message— Custom commit message. Defaults to "Translations update".override_branch_name— Static branch name instead of an auto-generated one. Helps update the same PR across runs (e.g., alwayslokalise-sync). If the branch exists, it’s updated rather than recreated.force_push— Force push to the remote branch. Use with caution, as it overwrites history. Defaults tofalse.temp_branch_prefix— Prefix for temporary branch names (e.g.,lok— branch starts withlok). Defaults tolok.
pr_title— Title for the PR. Defaults to "Translations update".pr_body— Body text for the PR. Defaults to "This pull request updates translations from Lokalise".pr_labels— Comma-separated labels to apply to the PR.pr_draft— Create the PR as a draft (true/false). Defaults tofalse.pr_assignees— Comma-separated GitHub usernames to assign to the PR. Defaults to none.
pr_reviewers— Comma-separated GitHub usernames to request as reviewers. Only individual users can be specified.pr_teams_reviewers— Comma-separated team slugs (e.g.,backend,qa) from the same org as the repo.- Requires a token with
repoandread:orgscopes if the defaultGITHUB_TOKENis restricted.
- Requires a token with
custom_github_token— Optional token for creating/updating pull requests. Defaults toGITHUB_TOKEN. Use when elevated permissions are needed (assigning reviewers, interacting with protected branches, cross-repo changes). Keep secret.
os_platform— Target platform for the precompiled binaries used by this action (linux_amd64,linux_arm64,mac_amd64,mac_arm64). These binaries handle tasks like downloading and processing translations. Typically, you don't need to change this, as the default (linux_amd64) works for most environments. Override if running on a macOS runner or a different architecture.
- Go to your repository's Settings.
- Navigate to Actions > General.
- Under Workflow permissions, set the permissions to Read and write permissions.
- Enable Allow GitHub Actions to create and approve pull requests on the same page (under "Choose whether GitHub Actions can create pull requests or submit approving pull request reviews").
This action exposes the following outputs:
created_branch— The branch used for the PR.- On manual runs: a new temp branch is created.
- On PR runs: the PR head branch is reused.
- Empty if no changes were committed.
pr_exists—trueif a pull request exists after the run (either created or updated). Empty/false if no PR was touched.pr_created—trueif a brand-new pull request was created by this run. Empty/false otherwise.pr_updated—trueif an existing pull request was updated by this run. Empty/false otherwise.pr_action— String value:"created","updated", or"none". Convenience output to know what happened.pr_number— Number of the pull request (created or existing). Empty if no PR exists.pr_id— Node ID of the pull request (useful for GraphQL API calls).pr_url— URL of the pull request. Empty if no PR exists.
For example:
- name: Debug outputs
run: |
echo "Branch used: ${{ steps.lokalise-pull.outputs.created_branch }}"
echo "PR exists: ${{ steps.lokalise-pull.outputs.pr_exists }}"
echo "PR created: ${{ steps.lokalise-pull.outputs.pr_created }}"
echo "PR updated: ${{ steps.lokalise-pull.outputs.pr_updated }}"
echo "PR action: ${{ steps.lokalise-pull.outputs.pr_action }}"
echo "PR number: ${{ steps.lokalise-pull.outputs.pr_number }}"
echo "PR id: ${{ steps.lokalise-pull.outputs.pr_id }}"
echo "PR url: ${{ steps.lokalise-pull.outputs.pr_url }}"By default, this action requires the following permissions:
permissions:
contents: write
pull-requests: writeAlso, issues: write might be needed if you're providing the pr_labels parameter.
When triggered, this action follows these steps:
-
Download translation files:
- Retrieves translation files for all languages from the specified Lokalise project.
- The downloaded keys are filtered by the tag corresponding to the triggering branch. For example, if the branch is named
lokalise-hub, only keys tagged withlokalise-hubin Lokalise will be included in the download bundle.
-
Detect changes:
- Compares the downloaded translation files against the repository’s existing files to detect any updates or modifications.
-
Create a pull request:
- If changes are detected, the action creates a pull request from a temporary branch to the triggering branch.
- The temporary branch name is constructed using the prefix specified in the
temp_branch_prefixparameter.
For more information on assumptions, refer to the Assumptions and defaults section.
By default, the following headers and parameters are set when downloading files from Lokalise:
X-Api-Tokenheader — Derived from theapi_tokenparameter.project_idGET param — Derived from theproject_idparameter.format— Derived from thefile_formatparameter.original_filenames— Set totrue.directory_prefix— Set to/.include_tags— Set to the branch name that triggered the workflow.
- If you are using Gettext (PO files) and the action opens pull requests when no translations have been changed and the only difference is the "revision date", refer to the following comment for clarifications
- If you are using iOS strings files, please check the following document on our Developer Hub containing setup recommendations
Apache license version 2