├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── lint-pr-title.yml │ ├── test-action.yml │ └── update-main-version.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── docs ├── AI.md └── EXAMPLES.md └── entrypoint.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Crowdin GitHub Action configuration 16 | 2. `crowdin.yml` file content 17 | 3. Information about workflow (OS, steps, etc.) 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: lint-pr-title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: amannn/action-semantic-pull-request@v5 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test-action.yml: -------------------------------------------------------------------------------- 1 | name: 'test' 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - '*' 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Crowdin Action 17 | uses: ./ 18 | with: 19 | command: 'push' 20 | command_args: '-h' 21 | 22 | - name: Get CLI version 23 | uses: ./ 24 | with: 25 | command: 'init' 26 | command_args: '-V' 27 | -------------------------------------------------------------------------------- /.github/workflows/update-main-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Main Version 2 | 3 | run-name: Move ${{ github.event.inputs.main_version }} to ${{ github.event.inputs.target }} 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | target: 9 | description: The tag or reference to use 10 | required: true 11 | main_version: 12 | type: choice 13 | description: The main version to update 14 | options: 15 | - v2 16 | - v1 17 | 18 | jobs: 19 | tag: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Git config 27 | run: | 28 | git config user.name github-actions 29 | git config user.email github-actions@github.com 30 | 31 | - name: Tag new target 32 | run: git tag -f ${{ github.event.inputs.main_version }} ${{ github.event.inputs.target }} 33 | 34 | - name: Push new tag 35 | run: git push origin ${{ github.event.inputs.main_version }} --force 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@crowdin.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :tada: First off, thanks for taking the time to contribute! :tada: 4 | 5 | The following is a set of guidelines for contributing to Crowdin GitHub Action. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | This project and everyone participating in it are governed by the [Code of Conduct](/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 8 | 9 | ## How can I contribute? 10 | 11 | ### Star this repo 12 | 13 | It's quick and goes a long way! :stars: 14 | 15 | ### Reporting Bugs 16 | 17 | This section guides you through submitting a bug report for Crowdin Action. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer:, and find related reports :mag_right:. 18 | 19 | When you are creating a bug report, please include as many details as possible. Fill out the required issue template, the information it asks for helps us resolve issues faster. 20 | 21 | #### How Do I Submit a Bug Report? 22 | 23 | Bugs are tracked as [GitHub issues](https://github.com/crowdin/github-action/issues/). 24 | 25 | Explain the problem and include additional details to help maintainers reproduce the problem: 26 | 27 | * **Use a clear and descriptive title** for the issue to identify the problem. 28 | * **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. 29 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 30 | * **Explain which behavior you expected to see instead and why.** 31 | 32 | Include details about your configuration and environment: 33 | 34 | * Crowdin Action configuration 35 | * `crowdin.yml` file if used and its content 36 | * Workflow configuration 37 | 38 | ### Suggesting Enhancements 39 | 40 | This section guides you through submitting an enhancement suggestion for Crowdin Action, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. 41 | 42 | When you are creating an enhancement suggestion, please include as many details as possible. Fill in feature request, including the steps that you imagine you would take if the feature you're requesting existed. 43 | 44 | #### How Do I Submit an Enhancement Suggestion? 45 | 46 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/crowdin/github-action/issues/). 47 | 48 | Create an issue on that repository and provide the following information: 49 | 50 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 51 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 52 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 53 | * **Explain why this enhancement would be useful** to most Crowdin Action users. 54 | 55 | ### Your First Code Contribution 56 | 57 | Unsure where to begin contributing to Crowdin Action? You can start by looking through these `good-first-issue` and `help-wanted` issues: 58 | 59 | * [Good first issue](https://github.com/crowdin/github-action/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - issues which should only require a small amount of code, and a test or two. 60 | * [Help wanted](https://github.com/crowdin/github-action/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) - issues which should be a bit more involved than `Good first issue` issues. 61 | 62 | #### Pull Request Checklist 63 | 64 | Before sending your pull requests, make sure you followed the list below: 65 | 66 | - Read these guidelines. 67 | - Read [Code of Conduct](/CODE_OF_CONDUCT.md). 68 | - Ensure that your code adheres to standard conventions, as used in the rest of the project. 69 | - Ensure that your changes are well tested. 70 | 71 | > **Note** 72 | > This project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for commit messages and PR titles. 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM crowdin/cli:4.7.1 2 | 3 | RUN apk --no-cache add curl git git-lfs jq gnupg; 4 | 5 | COPY . . 6 | COPY entrypoint.sh /entrypoint.sh 7 | 8 | ENTRYPOINT ["/entrypoint.sh"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Crowdin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
6 |
278 | The Crowdin GitHub Action is licensed under the MIT License. 279 | See the LICENSE file distributed with this work for additional 280 | information regarding copyright ownership. 281 | 282 | Except as contained in the LICENSE file, the name(s) of the above copyright 283 | holders shall not be used in advertising or otherwise to promote the sale, 284 | use or other dealings in this Software without prior written authorization. 285 |286 | 287 | [upload-sources]: https://crowdin.github.io/crowdin-cli/commands/crowdin-upload-sources 288 | [upload-translations]: https://crowdin.github.io/crowdin-cli/commands/crowdin-upload-translations 289 | [download-sources]: https://crowdin.github.io/crowdin-cli/commands/crowdin-download-sources 290 | [download-translations]: https://crowdin.github.io/crowdin-cli/commands/crowdin-download-translations 291 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'crowdin-action' 2 | 3 | description: 'This action allows you to manage and synchronize localization resources with your Crowdin project' 4 | 5 | branding: 6 | icon: 'refresh-cw' 7 | color: 'green' 8 | 9 | inputs: 10 | # upload sources options 11 | upload_sources: 12 | description: 'Upload sources to Crowdin' 13 | default: 'true' 14 | required: false 15 | upload_sources_args: 16 | description: 'Additional arguments which will be passed to the `upload sources` cli command' 17 | default: '' 18 | required: false 19 | 20 | # upload translations options 21 | upload_translations: 22 | description: 'Upload translations to Crowdin' 23 | default: 'false' 24 | required: false 25 | upload_language: 26 | description: 'Use this option to upload translations for a single specified language - Case-Sensitive' 27 | required: false 28 | auto_approve_imported: 29 | description: 'Automatically approves uploaded translations' 30 | default: 'false' 31 | required: false 32 | import_eq_suggestions: 33 | description: 'Defines whether to add translation if it is equal to source string in Crowdin project' 34 | default: 'false' 35 | required: false 36 | upload_translations_args: 37 | description: 'Additional arguments which will be passed to the `upload translations` cli command' 38 | default: '' 39 | required: false 40 | 41 | # download sources options 42 | download_sources: 43 | description: 'Defines whether to download source files from the Crowdin project' 44 | default: 'false' 45 | required: false 46 | push_sources: 47 | description: 'Push downloaded sources to the branch' 48 | default: 'true' 49 | required: false 50 | download_sources_args: 51 | description: 'Additional arguments which will be passed to the `download sources` cli command' 52 | default: '' 53 | required: false 54 | 55 | # download translations options 56 | download_translations: 57 | description: 'Defines whether to download translation files from the Crowdin project' 58 | default: 'false' 59 | required: false 60 | download_language: 61 | description: 'Use this option to download translations for a single specified language' 62 | required: false 63 | download_bundle: 64 | description: 'Download bundle from Crowdin project (by ID)' 65 | default: '' 66 | required: false 67 | skip_untranslated_strings: 68 | description: 'Skip untranslated strings in exported files (does not work with .docx, .html, .md and other document files)' 69 | default: 'false' 70 | required: false 71 | skip_untranslated_files: 72 | description: 'Omit downloading not fully translated files' 73 | default: 'false' 74 | required: false 75 | export_only_approved: 76 | description: 'Include approved translations only in exported files. If not combined with --skip-untranslated-strings option, strings without approval are fulfilled with the source language' 77 | default: 'false' 78 | required: false 79 | push_translations: 80 | description: 'Push downloaded translations to the branch' 81 | default: 'true' 82 | required: false 83 | commit_message: 84 | description: 'Commit message for download translations' 85 | default: 'New Crowdin translations by GitHub Action' 86 | required: false 87 | localization_branch_name: 88 | description: 'To download translations to the specified version branch' 89 | default: 'l10n_crowdin_action' 90 | required: false 91 | create_pull_request: 92 | description: 'Create pull request after pushing to branch' 93 | default: 'true' 94 | required: false 95 | pull_request_title: 96 | description: 'The title of the new pull request' 97 | default: 'New Crowdin translations by GitHub Action' 98 | required: false 99 | pull_request_body: 100 | description: 'The contents of the pull request' 101 | required: false 102 | pull_request_assignees: 103 | description: 'Add up to 10 assignees to the created pull request (separated by comma)' 104 | required: false 105 | pull_request_reviewers: 106 | description: 'Usernames of people from whom a review is requested for this pull request (separated by comma)' 107 | required: false 108 | pull_request_team_reviewers: 109 | description: 'Team slugs from which a review is requested for this pull request (separated by comma)' 110 | required: false 111 | pull_request_labels: 112 | description: 'To add labels for created pull request' 113 | required: false 114 | pull_request_base_branch_name: 115 | description: 'Create pull request to specified branch instead of default one' 116 | required: false 117 | download_translations_args: 118 | description: 'Additional arguments which will be passed to the `download translations` cli command' 119 | default: '' 120 | required: false 121 | skip_ref_checkout: 122 | description: 'Skip default git checkout on GITHUB_REF' 123 | default: 'false' 124 | required: false 125 | 126 | # global options 127 | crowdin_branch_name: 128 | description: 'Option to upload or download files to the specified version branch in your Crowdin project' 129 | required: false 130 | config: 131 | description: 'Option to specify a path to the configuration file, without / at the beginning' 132 | required: false 133 | dryrun_action: 134 | description: 'Option to preview the list of managed files' 135 | default: 'false' 136 | required: false 137 | 138 | # GitHub (Enterprise) configuration 139 | github_base_url: 140 | description: 'Option to configure the base URL of GitHub server, if using GHE.' 141 | default: 'github.com' 142 | required: false 143 | github_api_base_url: 144 | description: 'Options to configure the base URL of GitHub server for API requests, if using GHE and different from api.github_base_url.' 145 | required: false 146 | github_user_name: 147 | description: 'Option to configure GitHub user name on commits.' 148 | default: 'Crowdin Bot' 149 | required: false 150 | github_user_email: 151 | description: 'Option to configure GitHub user email on commits.' 152 | default: 'support+bot@crowdin.com' 153 | required: false 154 | gpg_private_key: 155 | description: 'GPG private key in ASCII-armored format' 156 | required: false 157 | gpg_passphrase: 158 | description: 'GPG Passphrase' 159 | default: '' 160 | required: false 161 | 162 | # config options 163 | project_id: 164 | description: 'Numerical ID of the project' 165 | required: false 166 | token: 167 | description: 'Personal access token required for authentication' 168 | required: false 169 | base_url: 170 | description: 'Base URL of Crowdin server for API requests execution' 171 | required: false 172 | base_path: 173 | description: 'Path to your project directory on a local machine, without / at the beginning' 174 | required: false 175 | source: 176 | description: 'Path to the source files, without / at the beginning' 177 | required: false 178 | translation: 179 | description: 'Path to the translation files' 180 | required: false 181 | 182 | # command options 183 | command: 184 | description: 'Crowdin CLI command to execute' 185 | required: false 186 | command_args: 187 | description: 'Additional arguments which will be passed to the Crowdin CLI command' 188 | required: false 189 | outputs: 190 | pull_request_url: 191 | description: 'The URL of the pull request created by the workflow' 192 | pull_request_number: 193 | description: 'The number of the pull request created by the workflow' 194 | 195 | runs: 196 | using: docker 197 | image: 'Dockerfile' 198 | -------------------------------------------------------------------------------- /docs/AI.md: -------------------------------------------------------------------------------- 1 | # AI Localization 2 | 3 | Crowdin offers a set of tools to help you localize your project with AI. These tools are designed to help you save time and effort on localization tasks, making the process more efficient and cost-effective. 4 | 5 | While Crowdin GitHub Action is a powerful tool for automating localization workflows, you will need to make sure that you provide the necessary context for the AI to work effectively. This means that you will need to provide the AI with the necessary information about your project, such as the source files. 6 | 7 | ## Preparing Your Project 8 | 9 | Crowdin integrates with top AI providers, including OpenAI, Google Gemini, Microsoft Azure OpenAI, DeepSeek, xAI, and more, allowing you to leverage advanced AI-powered translations that consider additional context at different levels. 10 | 11 | To get started, you will need to add the AI Provider and Prompt to your Crowdin profile or organization settings. 12 | 13 | > [!TIP] 14 | > Visit the [Crowdin AI](https://support.crowdin.com/crowdin-ai/) page to learn more about the AI providers and how to set up AI in your Crowdin account. 15 | 16 | After setting up the AI provider and Prompt, store their IDs in the Actions secrets: create the `PROVIDER_ID`, `PROMPT_ID` secrets in _Repository settings_ -> _Secrets and variables_ -> _Actions_ > _Repository secrets_. Also, create a new GitHub Actions secret to store the Personal Access Token and the Crowdin Project ID: `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID`. Read more about [Personal Access Tokens](https://support.crowdin.com/account-settings/#personal-access-tokens/). 17 | 18 | As a result, you must have the following secrets configured for your repository: 19 | 20 | - `PROVIDER_ID` - AI Provider ID 21 | - `PROMPT_ID` - Prompt ID 22 | - `CROWDIN_PERSONAL_TOKEN` - Crowdin Personal Access Token 23 | - `CROWDIN_PROJECT_ID` - Crowdin Project ID 24 | 25 | ## Automatic AI Pre-Translation 26 | 27 | The basic workflow for AI localization includes the following steps: 28 | 29 | - Source files are uploaded to Crowdin. 30 | - Automatic pre-translation is performed using the AI provider. 31 | - Translations are downloaded. 32 | - Push the translations to the repository and create a pull request. 33 | 34 | Here is an example of how to set up the automatic AI pre-translation workflow using the Crowdin GitHub Action: 35 | 36 | ```yaml 37 | name: Crowdin Pre-translate with AI 38 | 39 | on: 40 | push: 41 | branches: [ main ] 42 | workflow_dispatch: 43 | 44 | jobs: 45 | crowdin-process: 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Upload Sources to Crowdin 52 | uses: crowdin/github-action@v2 53 | with: 54 | upload_sources: true 55 | upload_translations: false 56 | download_translations: false 57 | create_pull_request: false 58 | push_translations: false 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 62 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 63 | 64 | - name: Pre-translate with AI 65 | uses: crowdin/github-action@v2 66 | with: 67 | command: 'pre-translate' 68 | command_args: '--method ai --ai-prompt=${{ secrets.PROMPT_ID }}' 69 | env: 70 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 71 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 72 | 73 | - name: Download Translations from Crowdin 74 | uses: crowdin/github-action@v2 75 | with: 76 | upload_sources: false 77 | upload_translations: false 78 | download_translations: true 79 | localization_branch_name: l10n_crowdin_ai_translations 80 | create_pull_request: true 81 | pull_request_title: 'New Crowdin AI Translations' 82 | pull_request_body: 'New translations generated with Crowdin AI pre-translation' 83 | pull_request_base_branch_name: 'main' 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 87 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 88 | ``` 89 | 90 | ## Providing Context for AI 91 | 92 | Context is crucial for accurate and high-quality translations. With the enhanced context, AI produces production-quality translations that were previously only possible with human input. 93 | 94 | > [!IMPORTANT] 95 | > Experiments have shown that LLM + AI Extracted Context can improve the translation quality by up to 75% and LLM + AI Extracted Context + Screenshots + Crowdin AI Tools can improve the translation quality by up to 95%. 96 | 97 | ### Harvesting Context with Crowdin Context Harvester CLI 98 | 99 | To ensure that AI translations are accurate and contextually relevant, you need to provide the AI with the necessary context. 100 | 101 | Crowdin allows you to provide various levels of context to the AI, including the options available during prompt configuration (glossary terms, TM suggestions, previous and next strings, file context, screenshots, and more). It's highly recommended that you provide the AI with as much context as possible to improve the quality of the translations. 102 | 103 | You can use the [Crowdin Context Harvester CLI](https://store.crowdin.com/crowdin-context-harvester-cli) in your CI/CD pipeline to automate the context extraction process. The Context Harvester CLI is designed to simplify the process of extracting context for Crowdin strings from your code. Using Large Language Models (LLMs), it automatically analyzes your project code to find out how each key is used. This information is extremely useful for the human linguists or AI that will be translating your project keys, and is likely to improve the quality of the translation. 104 | 105 | First, install the Crowdin Context Harvester CLI and configure it with your Crowdin project: 106 | 107 | ```bash 108 | npm i -g crowdin-context-harvester 109 | crowdin-context-harvester configure 110 | ``` 111 | 112 | You'll be asked to enter all the necessary information, such as your Crowdin Personal Access Token, Project ID, and other details. 113 | 114 | For example: 115 | 116 | ```bash 117 | crowdin-context-harvester configure 118 | ? What Crowdin product do you use? Crowdin.com 119 | ? Crowdin Personal API token (with Project, AI scopes): __your_personal_token_ 120 | ? Crowdin project: Test Project 121 | ? AI provider: Crowdin AI Provider 122 | ? Crowdin AI provider (you should have the OpenAI provider configured in Crowdin): Open AI 123 | ? AI model (newest models with largest context window are preferred): gpt-4 124 | ? Model context window size in tokens: 128000 125 | ? Model maximum output tokens count: 16384 126 | ? Check if the code contains the key or the text of the string before sending it to the AI model 127 | (recommended if you have thousands of keys to avoid chunking and improve speed).: I use keys in the code 128 | ? Custom prompt file. "-" to read from STDIN (optional): 129 | ? Local files (glob pattern): **/*.* 130 | ? Ignore local files (glob pattern). Make sure to exclude unnecessary files to avoid unnecessary AI API calls: /**/node_modules/** 131 | ? Crowdin files (glob pattern e.g. **/*.*).: **/*.* 132 | ? CroQL query (optional): 133 | ? Output: Terminal (dry run) 134 | 135 | You can now execute the harvest command by running: 136 | 137 | crowdin-context-harvester harvest --token="__your_personal_token_" --project=11111 --ai="crowdin" --crowdinAiId=2222 --model="gpt-4" --localFiles="**/*.*" --localIgnore="/**/node_modules/**" --crowdinFiles="**/*.*" --contextWindowSize="128000" --maxOutputTokens="16384" --screen="keys" --output="terminal" 138 | ``` 139 | 140 | Once you have configured the Crowdin Context Harvester CLI, you can use the received `harvest` command to extract the context from your project files and provide it to the AI for better translations in your CI/CD pipeline. 141 | 142 | Then, add the following steps to your GitHub Actions workflow to extract the context and provide it to the AI: 143 | 144 | ```yaml 145 | # Upload sources step 146 | 147 | - uses: actions/setup-node@vv 148 | with: 149 | node-version: '20' 150 | 151 | - name: Extract Context for AI 152 | run: | 153 | npm i -g crowdin-context-harvester 154 | crowdin-context-harvester harvest \ 155 | --ai="crowdin" \ 156 | --crowdinAiId="${{ secrets.PROVIDER_ID }}" \ 157 | --model="gpt-4" \ 158 | --localFiles="**/*.*" \ 159 | --localIgnore="/**/node_modules/**" \ 160 | --crowdinFiles="**/*.*" \ 161 | --contextWindowSize="128000" \ 162 | --maxOutputTokens="16384" \ 163 | --screen="keys" \ 164 | --output="terminal" 165 | 166 | - name: Upload Context 167 | run: | 168 | crowdin-context-harvester upload \ 169 | --token="${{ secrets.CROWDIN_PERSONAL_TOKEN }}" \ 170 | --project="${{ secrets.CROWDIN_PROJECT_ID }}" \ 171 | --csvFile="crowdin-context.csv" 172 | 173 | # Pre-translate with AI step 174 | # Download translations step 175 | ``` 176 | 177 | > [!CAUTION] 178 | > Make sure to omit the personal access token and project ID from the command line and store them in the GitHub Actions secrets. The CLI will automatically use the secrets if they are set. 179 | 180 | ### Automated Screenshots 181 | 182 | Crowdin also allows you to provide screenshots to the AI to help it better understand the context. Depending on the type of project, Crowdin offers a few different ways to automate the screenshot generation process: 183 | 184 | - [For Web Projects](https://support.crowdin.com/developer/automating-screenshot-management/) 185 | - [For Android Projects](https://crowdin.github.io/mobile-sdk-android/guides/screenshots-automation) 186 | - [For iOS Projects](https://crowdin.github.io/mobile-sdk-ios/guides/screenshots-automation) 187 | 188 | Integrate automated screenshot generation into your CI/CD pipeline to give AI the context it needs for better translations. 189 | -------------------------------------------------------------------------------- /docs/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Crowdin Action usage examples 2 | 3 | - [Create PR with the new translations](#create-pr-with-the-new-translations) 4 | - [Translations export options configuration](#translations-export-options-configuration) 5 | - [No-crowdin.yml configuration](#no-crowdinyml-configuration) 6 | - [Upload sources only](#upload-sources-only) 7 | - [Upload sources to the branch in Crowdin](#upload-sources-to-the-branch-in-crowdin) 8 | - [Download only translations without pushing to a branch](#download-only-translations-without-pushing-to-a-branch) 9 | - [Download Bundle](#download-bundle) 10 | - [Advanced Pull Request configuration](#advanced-pull-request-configuration) 11 | - [Custom `crowdin.yml` file location](#custom-crowdinyml-file-location) 12 | - [Separate PRs for each target language](#separate-prs-for-each-target-language) 13 | - [Checking out multiple branches in a single workflow](#checking-out-multiple-branches-in-a-single-workflow) 14 | - [Outputs](#outputs) 15 | - [`pull_request_url`, `pull_request_number`](#pull_request_url-pull_request_number) 16 | - [Triggers](#triggers) 17 | - [Cron schedule](#cron-schedule) 18 | - [Manually](#manually) 19 | - [When a localization file is updated in the specified branch](#when-a-localization-file-is-updated-in-the-specified-branch) 20 | - [When a file or project is fully translated (Webhooks)](https://store.crowdin.com/github-actions-webhook) 21 | - [When a new GitHub Release is published](#when-a-new-github-release-is-published) 22 | - [Dealing with concurrency](#dealing-with-concurrency) 23 | - [Handling parallel runs](#handling-parallel-runs) 24 | - [Tips and tricks](#tips-and-tricks) 25 | - [Checking the translation progress](#checking-the-translation-progress) 26 | - [Pre-Translation](#pre-translation) 27 | - [Run test workflows on all commits of a PR](#run-test-workflows-on-all-commits-of-a-pr) 28 | - [Automatic AI Pre-Translation](AI.md) 29 | --- 30 | 31 | ### Create PR with the new translations 32 | 33 | ```yaml 34 | name: Crowdin Action 35 | 36 | on: 37 | push: 38 | branches: [ main ] 39 | 40 | permissions: 41 | contents: write 42 | pull-requests: write 43 | 44 | jobs: 45 | crowdin: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | - name: Synchronize with Crowdin 52 | uses: crowdin/github-action@v2 53 | with: 54 | upload_sources: true 55 | upload_translations: true 56 | download_translations: true 57 | localization_branch_name: l10n_crowdin_translations 58 | 59 | create_pull_request: true 60 | pull_request_title: 'New Crowdin translations' 61 | pull_request_body: 'New Crowdin pull request with translations' 62 | pull_request_base_branch_name: 'main' 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 66 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 67 | ``` 68 | 69 | ### Translations export options configuration 70 | 71 | ```yaml 72 | name: Crowdin Action 73 | 74 | on: 75 | push: 76 | branches: [ main ] 77 | 78 | jobs: 79 | crowdin: 80 | runs-on: ubuntu-latest 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@v4 84 | 85 | - name: Synchronize with Crowdin 86 | uses: crowdin/github-action@v2 87 | with: 88 | upload_sources: true 89 | upload_translations: false 90 | download_translations: true 91 | 92 | # Export options 93 | skip_untranslated_strings: true 94 | export_only_approved: true 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 98 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 99 | ``` 100 | 101 | ### No-crowdin.yml configuration 102 | 103 | ```yaml 104 | name: Crowdin Action 105 | 106 | on: 107 | push: 108 | branches: [ main ] 109 | 110 | jobs: 111 | crowdin: 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: Checkout 115 | uses: actions/checkout@v4 116 | 117 | - name: Crowdin sync 118 | uses: crowdin/github-action@v2 119 | with: 120 | upload_sources: true 121 | upload_translations: false 122 | source: src/locale/en.json # Sources pattern 123 | translation: src/locale/%android_code%.json # Translations pattern 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 127 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 128 | ``` 129 | 130 | The Action/CLI will automatically detect the [environment variables](https://crowdin.github.io/crowdin-cli/configuration#environment-variables) and use them for the configuration. 131 | 132 | > **Note** 133 | > To avoid any conflicts, do not use the `crowdin.yml` file in the repository when using the above configuration approach. 134 | 135 | ### Upload sources only 136 | 137 | ```yaml 138 | name: Crowdin Action 139 | 140 | on: 141 | push: 142 | branches: [ main ] 143 | 144 | jobs: 145 | crowdin: 146 | runs-on: ubuntu-latest 147 | steps: 148 | - name: Checkout 149 | uses: actions/checkout@v4 150 | 151 | - name: Crowdin push 152 | uses: crowdin/github-action@v2 153 | with: 154 | upload_sources: true 155 | upload_translations: false 156 | download_translations: false 157 | env: 158 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 159 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 160 | ``` 161 | 162 | ### Upload sources to the branch in Crowdin 163 | 164 | ```yaml 165 | name: Crowdin Action 166 | 167 | on: 168 | push: 169 | branches: [ main ] 170 | 171 | jobs: 172 | crowdin: 173 | runs-on: ubuntu-latest 174 | steps: 175 | - name: Checkout 176 | uses: actions/checkout@v4 177 | 178 | - name: Crowdin push 179 | uses: crowdin/github-action@v2 180 | with: 181 | upload_sources: true 182 | upload_translations: false 183 | download_translations: false 184 | crowdin_branch_name: ${{ env.BRANCH_NAME }} 185 | env: 186 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 187 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 188 | ``` 189 | 190 | Note that the value of the `crowdin_branch_name` is `env.BRANCH_NAME` - this is the name of the current branch on which the action is running. 191 | 192 | ### Download only translations without pushing to a branch 193 | 194 | It's possible to just download the translations without creating a PR immediately. It allows you to post-process the downloaded translations and create a PR later. 195 | 196 | ```yaml 197 | name: Crowdin Action 198 | 199 | on: 200 | push: 201 | branches: [ main ] 202 | 203 | jobs: 204 | crowdin: 205 | runs-on: ubuntu-latest 206 | steps: 207 | - name: Checkout 208 | uses: actions/checkout@v4 209 | 210 | - name: Crowdin pull 211 | uses: crowdin/github-action@v2 212 | with: 213 | upload_sources: false 214 | upload_translations: false 215 | download_translations: true 216 | create_pull_request: false 217 | push_translations: false 218 | env: 219 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 220 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 221 | ``` 222 | 223 | You can use the [Create Pull Request](https://github.com/marketplace/actions/create-pull-request) GitHub Action to create a PR with the downloaded translations. 224 | 225 | ### Download Bundle 226 | 227 | Target file bundles or simply Bundles is the feature that allows you to export sets of strings or files in the formats you select, regardless of the original file format 228 | 229 | You can use the `download_bundle` option to download the bundle from Crowdin: 230 | 231 | ```yaml 232 | name: Crowdin Action 233 | 234 | on: 235 | push: 236 | branches: [ main ] 237 | 238 | jobs: 239 | crowdin: 240 | runs-on: ubuntu-latest 241 | steps: 242 | - name: Checkout 243 | uses: actions/checkout@v4 244 | 245 | - name: Crowdin pull 246 | uses: crowdin/github-action@v2 247 | with: 248 | download_translations: false 249 | download_bundle: 1 250 | create_pull_request: true 251 | env: 252 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 253 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 254 | ``` 255 | 256 | > **Note** 257 | > If you are using a **String-based** project, you need to use this option to download translations. The default `download_translations` option does not work for this type of projects. 258 | 259 | The `download_bundle` option accepts the bundle numeric ID. 260 | 261 | Visit the [official documentation](https://support.crowdin.com/bundles/) to learn more about Bundles. 262 | 263 | ### Advanced Pull Request configuration 264 | 265 | There is a possibility to specify labels, assignees, reviewers for PR created by the Action. 266 | 267 | ```yaml 268 | name: Crowdin Action 269 | 270 | on: 271 | push: 272 | branches: [ main ] 273 | 274 | jobs: 275 | crowdin: 276 | runs-on: ubuntu-latest 277 | steps: 278 | - name: Checkout 279 | uses: actions/checkout@v4 280 | 281 | - name: Synchronize with Crowdin 282 | uses: crowdin/github-action@v2 283 | with: 284 | upload_sources: true 285 | upload_translations: true 286 | download_translations: true 287 | localization_branch_name: l10n_crowdin_translations 288 | 289 | create_pull_request: true 290 | pull_request_title: 'New Crowdin translations' 291 | pull_request_body: 'New Crowdin pull request with translations' 292 | pull_request_base_branch_name: 'main' 293 | 294 | pull_request_labels: 'enhancement, good first issue' 295 | pull_request_assignees: 'crowdin-bot' 296 | pull_request_reviewers: 'crowdin-user-reviewer' 297 | pull_request_team_reviewers: 'crowdin-team-reviewer' 298 | env: 299 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 300 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 301 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 302 | ``` 303 | 304 | ### Custom `crowdin.yml` file location 305 | 306 | By default, the Action looks for the `crowdin.yml` file in the repository root. You can specify a custom location of the configuration file: 307 | 308 | ```yaml 309 | # ... 310 | 311 | - name: Crowdin 312 | uses: crowdin/github-action@v2 313 | with: 314 | config: '.github/crowdin.yml' 315 | #... 316 | ``` 317 | 318 | ### Separate PRs for each target language 319 | 320 | You can use the [`matrix`](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) feature of GitHub Actions to create separate PRs for each target language: 321 | 322 | ```yaml 323 | name: Crowdin Action 324 | 325 | on: 326 | push: 327 | branches: [ main ] 328 | 329 | jobs: 330 | crowdin: 331 | name: Synchronize with Crowdin 332 | runs-on: ubuntu-latest 333 | 334 | strategy: 335 | fail-fast: false 336 | max-parallel: 1 # Should be 1 to avoid parallel builds 337 | matrix: 338 | lc: [uk, it, es, fr, de, pt-BR] # Target languages https://developer.crowdin.com/language-codes/ 339 | steps: 340 | - name: Checkout 341 | uses: actions/checkout@v4 342 | 343 | - name: Matrix 344 | uses: crowdin/github-action@v2 345 | with: 346 | upload_sources: false 347 | upload_translations: false 348 | download_translations: true 349 | commit_message: New Crowdin translations - ${{ matrix.lc }} 350 | localization_branch_name: l10n_main_${{ matrix.lc }} 351 | pull_request_base_branch_name: 'main' 352 | pull_request_title: New translations - ${{ matrix.lc }} 353 | download_language: ${{ matrix.lc }} 354 | env: 355 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 356 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 357 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 358 | ``` 359 | 360 | ### Checking out multiple branches in a single workflow 361 | 362 | By default, the action checks out and pushes to the branch specified in the `GITHUB_REF` environment variable. This is the fully-formed ref of the branch or tag that triggered the workflow run. 363 | 364 | If you need to checkout multiple branches in a single workflow (e.g. a matrix of feature branches), or need to push your translations to a branch other than the one that triggered the workflow, you'll need to disable automatic checkout to the `GITHUB_REF` using the `skip_ref_checkout` option: 365 | 366 | ```yml 367 | name: Crowdin Action 368 | 369 | on: 370 | workflow_dispatch: 371 | 372 | jobs: 373 | crowdin: 374 | name: Synchronize with Crowdin 375 | runs-on: ubuntu-latest 376 | 377 | strategy: 378 | fail-fast: false 379 | max-parallel: 1 # Should be 1 to avoid parallel builds 380 | matrix: 381 | branch: ["feat/1", "feat/2", "feat/3"] 382 | 383 | steps: 384 | - uses: actions/checkout@v4 385 | with: 386 | ref: ${{ matrix.branch }} 387 | fetch-depth: 0 388 | 389 | - name: Synchronize with Crowdin 390 | uses: crowdin/github-action@v2 391 | with: 392 | upload_sources: true 393 | upload_translations: true 394 | download_translations: true 395 | localization_branch_name: l10n_crowdin_translations 396 | create_pull_request: true 397 | skip_ref_checkout: true # Disable 'git checkout "${GITHUB_REF#refs/heads/}"' 398 | env: 399 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 400 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 401 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 402 | ``` 403 | 404 | ## Triggers 405 | 406 | ### Cron schedule 407 | 408 | ```yaml 409 | on: 410 | schedule: 411 | - cron: '0 */12 * * *' # Every 12 hours - https://crontab.guru/#0_*/12_*_*_* 412 | ``` 413 | 414 | ### Manually 415 | 416 | ```yaml 417 | on: 418 | workflow_dispatch: 419 | ``` 420 | 421 | ### When a localization file is updated in the specified branch 422 | 423 | ```yaml 424 | on: 425 | push: 426 | paths: 427 | - 'src/locales/en.json' 428 | branches: [ main ] 429 | ``` 430 | 431 | ### When a new GitHub Release is published 432 | 433 | ```yaml 434 | on: 435 | release: 436 | types: [published] 437 | ``` 438 | 439 | ### Dealing with concurrency 440 | 441 | ```yaml 442 | on: 443 | push: 444 | branches: 445 | - main 446 | concurrency: 447 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 448 | cancel-in-progress: true 449 | ``` 450 | 451 | ### Handling parallel runs 452 | 453 | In case your action fails when a build is in progress (409 error code), you need to configure the workflow in a way to avoid parallel runs. 454 | 455 | ```yaml 456 | strategy: 457 | max-parallel: 1 458 | ``` 459 | 460 | [Read more](https://github.com/crowdin/github-action/wiki/Handling-parallel-runs) 461 | 462 | ## Outputs 463 | 464 | ### `pull_request_url`, `pull_request_number` 465 | 466 | There is a possibility to get the URL or number of the created Pull Request. You can use it in the next steps of your workflow. 467 | 468 | ```yaml 469 | # ... 470 | - name: Crowdin 471 | uses: crowdin/github-action@v2 472 | id: crowdin-download 473 | with: 474 | download_translations: true 475 | create_pull_request: true 476 | env: 477 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 478 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 479 | 480 | - name: Enable auto-merge for the PR 481 | if: steps.crowdin-download.outputs.pull_request_url 482 | run: gh pr --repo $GITHUB_REPOSITORY merge ${{ steps.crowdin-download.outputs.pull_request_url }} --auto --merge 483 | env: 484 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 485 | 486 | - name: Approve the PR 487 | if: steps.crowdin-download.outputs.pull_request_url 488 | run: gh pr --repo $GITHUB_REPOSITORY review ${{ steps.crowdin-download.outputs.pull_request_url }} --approve 489 | env: 490 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 491 | ``` 492 | 493 | ## Tips and Tricks 494 | 495 | ### Checking the translation progress 496 | 497 | ```yaml 498 | name: Crowdin Action 499 | 500 | on: 501 | push: 502 | branches: [ main ] 503 | 504 | jobs: 505 | crowdin: 506 | runs-on: ubuntu-latest 507 | steps: 508 | - name: Checkout 509 | uses: actions/checkout@v4 510 | 511 | - name: Check translation progress 512 | uses: crowdin/github-action@v2 513 | with: 514 | command: 'status translation' 515 | command_args: '--fail-if-incomplete' 516 | ``` 517 | 518 | In the example above, the workflow will fail if the translation progress is less than 100%. 519 | 520 | Visit the [official documentation](https://crowdin.github.io/crowdin-cli/commands/crowdin-status) to learn more about the available translation status options. 521 | 522 | ### Pre-Translation 523 | 524 | ```yaml 525 | name: Crowdin Action 526 | 527 | on: 528 | push: 529 | branches: [ main ] 530 | 531 | jobs: 532 | crowdin: 533 | runs-on: ubuntu-latest 534 | steps: 535 | - name: Checkout 536 | uses: actions/checkout@v4 537 | 538 | - name: Pre-translate 539 | uses: crowdin/github-action@v2 540 | with: 541 | command: 'pre-translate' 542 | command_args: '--language uk --method tm' 543 | env: 544 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 545 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 546 | ``` 547 | 548 | Visit the [official documentation](https://crowdin.github.io/crowdin-cli/commands/crowdin-pre-translate) to learn more about the available pre-translation options. 549 | 550 | ### Run test workflows on all commits of a PR 551 | 552 | Every time the job runs and there is already a PR, all checks will be invalidated and not run again (because the action will force pushes even if the translation has not changed). 553 | 554 | (Related issues: [#34](https://github.com/crowdin/github-action/issues/34), [#142](https://github.com/crowdin/github-action/issues/142), [#221](https://github.com/crowdin/github-action/issues/221)) 555 | 556 | To avoid this, add a custom PAT to the checkout action: 557 | 558 | ```yaml 559 | - name: Checkout 560 | uses: actions/checkout@v4 561 | with: 562 | token: ${{ secrets.GHA_CUSTOM_PAT }} 563 | ``` 564 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Default values for action outputs 4 | echo "pull_request_url=" >> $GITHUB_OUTPUT 5 | echo "pull_request_number=" >> $GITHUB_OUTPUT 6 | 7 | if [ "$INPUT_DEBUG_MODE" = true ] || [ -n "$RUNNER_DEBUG" ]; then 8 | echo '---------------------------' 9 | printenv 10 | echo '---------------------------' 11 | fi 12 | 13 | upload_sources() { 14 | if [ -n "$INPUT_UPLOAD_SOURCES_ARGS" ]; then 15 | UPLOAD_SOURCES_OPTIONS="${UPLOAD_SOURCES_OPTIONS} ${INPUT_UPLOAD_SOURCES_ARGS}" 16 | fi 17 | 18 | echo "UPLOAD SOURCES" 19 | crowdin upload sources "$@" $UPLOAD_SOURCES_OPTIONS 20 | } 21 | 22 | upload_translations() { 23 | if [ -n "$INPUT_UPLOAD_LANGUAGE" ]; then 24 | UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_UPLOAD_LANGUAGE}" 25 | fi 26 | 27 | if [ "$INPUT_AUTO_APPROVE_IMPORTED" = true ]; then 28 | UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --auto-approve-imported" 29 | fi 30 | 31 | if [ "$INPUT_IMPORT_EQ_SUGGESTIONS" = true ]; then 32 | UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --import-eq-suggestions" 33 | fi 34 | 35 | if [ -n "$INPUT_UPLOAD_TRANSLATIONS_ARGS" ]; then 36 | UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} ${INPUT_UPLOAD_TRANSLATIONS_ARGS}" 37 | fi 38 | 39 | echo "UPLOAD TRANSLATIONS" 40 | crowdin upload translations "$@" $UPLOAD_TRANSLATIONS_OPTIONS 41 | } 42 | 43 | download_sources() { 44 | if [ -n "$INPUT_DOWNLOAD_SOURCES_ARGS" ]; then 45 | DOWNLOAD_SOURCES_OPTIONS="${DOWNLOAD_SOURCES_OPTIONS} ${INPUT_DOWNLOAD_SOURCES_ARGS}" 46 | fi 47 | 48 | echo "DOWNLOAD SOURCES" 49 | crowdin download sources "$@" $DOWNLOAD_SOURCES_OPTIONS 50 | } 51 | 52 | download_translations() { 53 | if [ -n "$INPUT_DOWNLOAD_LANGUAGE" ]; then 54 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_DOWNLOAD_LANGUAGE}" 55 | elif [ -n "$INPUT_LANGUAGE" ]; then #back compatibility for older versions 56 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_LANGUAGE}" 57 | fi 58 | 59 | if [ "$INPUT_SKIP_UNTRANSLATED_STRINGS" = true ]; then 60 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --skip-untranslated-strings" 61 | fi 62 | 63 | if [ "$INPUT_SKIP_UNTRANSLATED_FILES" = true ]; then 64 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --skip-untranslated-files" 65 | fi 66 | 67 | if [ "$INPUT_EXPORT_ONLY_APPROVED" = true ]; then 68 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --export-only-approved" 69 | fi 70 | 71 | if [ -n "$INPUT_DOWNLOAD_TRANSLATIONS_ARGS" ]; then 72 | DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} ${INPUT_DOWNLOAD_TRANSLATIONS_ARGS}" 73 | fi 74 | 75 | echo "DOWNLOAD TRANSLATIONS" 76 | crowdin download "$@" $DOWNLOAD_TRANSLATIONS_OPTIONS 77 | } 78 | 79 | create_pull_request() { 80 | BRANCH="${1}" 81 | 82 | AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" 83 | HEADER="Accept: application/vnd.github.v3+json; application/vnd.github.antiope-preview+json; application/vnd.github.shadow-cat-preview+json" 84 | 85 | if [ -n "$INPUT_GITHUB_API_BASE_URL" ]; then 86 | REPO_URL="https://${INPUT_GITHUB_API_BASE_URL}/repos/${GITHUB_REPOSITORY}" 87 | else 88 | REPO_URL="https://api.${INPUT_GITHUB_BASE_URL}/repos/${GITHUB_REPOSITORY}" 89 | fi 90 | 91 | ORG_NAME=$(echo "${GITHUB_REPOSITORY}" | cut -d'/' -f1) 92 | PULLS_URL="${REPO_URL}/pulls" 93 | 94 | auth_status=$(curl -sL --write-out '%{http_code}' --output /dev/null -H "${AUTH_HEADER}" -H "${HEADER}" "${PULLS_URL}") 95 | if [[ $auth_status -eq 403 || "$auth_status" -eq 401 ]] ; then 96 | echo "FAILED TO AUTHENTICATE USING 'GITHUB_TOKEN' CHECK TOKEN IS VALID" 97 | echo "pull_request_url=" >> $GITHUB_OUTPUT 98 | echo "pull_request_number=" >> $GITHUB_OUTPUT 99 | exit 1 100 | fi 101 | 102 | echo "CHECK IF PULL REQUEST ALREADY EXIST" 103 | 104 | if [ -n "$INPUT_PULL_REQUEST_BASE_BRANCH_NAME" ]; then 105 | BASE_BRANCH="$INPUT_PULL_REQUEST_BASE_BRANCH_NAME" 106 | else 107 | if [ -n "$GITHUB_HEAD_REF" ]; then 108 | BASE_BRANCH=${GITHUB_HEAD_REF} 109 | else 110 | BASE_BRANCH=${GITHUB_REF#refs/heads/} 111 | fi 112 | fi 113 | 114 | pull_requests_response=$(curl -sSL -w "%{http_code}" -H "${AUTH_HEADER}" -H "${HEADER}" -X GET "${PULLS_URL}?base=${BASE_BRANCH}&head=${ORG_NAME}:${BRANCH}") 115 | http_code="${pull_requests_response: -3}" 116 | response_body="${pull_requests_response%???}" 117 | 118 | if [ "$http_code" -ne 200 ]; then 119 | echo "Error: Failed to fetch pull requests. HTTP status: $http_code" 120 | echo "Response: $response_body" 121 | exit 1 122 | fi 123 | 124 | PULL_REQUESTS=$(echo "$response_body" | jq --raw-output '.[] | .head.ref') 125 | 126 | if echo "$PULL_REQUESTS" | grep -xq "$BRANCH"; then 127 | echo "PULL REQUEST ALREADY EXIST" 128 | else 129 | echo "CREATE PULL REQUEST" 130 | 131 | if [ -n "$INPUT_PULL_REQUEST_BODY" ]; then 132 | BODY=",\"body\":\"${INPUT_PULL_REQUEST_BODY//$'\n'/\\n}\"" 133 | fi 134 | 135 | PULL_RESPONSE_DATA=$(jq -n --arg pr_title "${INPUT_PULL_REQUEST_TITLE}" \ 136 | --arg base_branch "${BASE_BRANCH}" \ 137 | --arg branch "${BRANCH}" \ 138 | --arg body "${BODY}" \ 139 | "{title: \$pr_title, base: \$base_branch, head: \$branch ${BODY}}") 140 | # create pull request 141 | PULL_RESPONSE=$(curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X POST --data "${PULL_RESPONSE_DATA}" "${PULLS_URL}") 142 | 143 | set +x 144 | PULL_REQUESTS_URL=$(echo "${PULL_RESPONSE}" | jq '.html_url') 145 | PULL_REQUESTS_NUMBER=$(echo "${PULL_RESPONSE}" | jq '.number') 146 | view_debug_output 147 | 148 | if [ -n "$PULL_REQUESTS_URL" ]; then 149 | echo "pull_request_url=$PULL_REQUESTS_URL" >> $GITHUB_OUTPUT 150 | fi 151 | 152 | if [ -n "$PULL_REQUESTS_NUMBER" ]; then 153 | echo "pull_request_number=$PULL_REQUESTS_NUMBER" >> $GITHUB_OUTPUT 154 | fi 155 | 156 | if [ "$PULL_REQUESTS_URL" = null ]; then 157 | echo "FAILED TO CREATE PULL REQUEST" 158 | echo "RESPONSE: ${PULL_RESPONSE}" 159 | 160 | exit 1 161 | fi 162 | 163 | if [ -n "$INPUT_PULL_REQUEST_LABELS" ]; then 164 | PULL_REQUEST_LABELS=$(echo "[\"${INPUT_PULL_REQUEST_LABELS}\"]" | sed 's/, \|,/","/g') 165 | 166 | if [ "$(echo "$PULL_REQUEST_LABELS" | jq -e . > /dev/null 2>&1; echo $?)" -eq 0 ]; then 167 | echo "ADD LABELS TO PULL REQUEST" 168 | 169 | ISSUE_URL="${REPO_URL}/issues/${PULL_REQUESTS_NUMBER}" 170 | 171 | LABELS_DATA="{\"labels\":${PULL_REQUEST_LABELS}}" 172 | 173 | # add labels to created pull request 174 | curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X PATCH --data "${LABELS_DATA}" "${ISSUE_URL}" 175 | else 176 | echo "JSON OF pull_request_labels IS INVALID: ${PULL_REQUEST_LABELS}" 177 | fi 178 | fi 179 | 180 | if [ -n "$INPUT_PULL_REQUEST_ASSIGNEES" ]; then 181 | PULL_REQUEST_ASSIGNEES=$(echo "[\"${INPUT_PULL_REQUEST_ASSIGNEES}\"]" | sed 's/, \|,/","/g') 182 | 183 | if [ "$(echo "$PULL_REQUEST_ASSIGNEES" | jq -e . > /dev/null 2>&1; echo $?)" -eq 0 ]; then 184 | echo "ADD ASSIGNEES TO PULL REQUEST" 185 | 186 | ASSIGNEES_URL="${REPO_URL}/issues/${PULL_REQUESTS_NUMBER}/assignees" 187 | 188 | ASSIGNEES_DATA="{\"assignees\":${PULL_REQUEST_ASSIGNEES}}" 189 | 190 | # add assignees to created pull request 191 | curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X POST --data "${ASSIGNEES_DATA}" "${ASSIGNEES_URL}" 192 | else 193 | echo "JSON OF pull_request_assignees IS INVALID: ${PULL_REQUEST_ASSIGNEES}" 194 | fi 195 | fi 196 | 197 | if [ -n "$INPUT_PULL_REQUEST_REVIEWERS" ] || [ -n "$INPUT_PULL_REQUEST_TEAM_REVIEWERS" ]; then 198 | if [ -n "$INPUT_PULL_REQUEST_REVIEWERS" ]; then 199 | PULL_REQUEST_REVIEWERS=$(echo "\"${INPUT_PULL_REQUEST_REVIEWERS}\"" | sed 's/, \|,/","/g') 200 | 201 | if [ "$(echo "$PULL_REQUEST_REVIEWERS" | jq -e . > /dev/null 2>&1; echo $?)" -eq 0 ]; then 202 | echo "ADD REVIEWERS TO PULL REQUEST" 203 | else 204 | echo "JSON OF pull_request_reviewers IS INVALID: ${PULL_REQUEST_REVIEWERS}" 205 | fi 206 | fi 207 | 208 | if [ -n "$INPUT_PULL_REQUEST_TEAM_REVIEWERS" ]; then 209 | PULL_REQUEST_TEAM_REVIEWERS=$(echo "\"${INPUT_PULL_REQUEST_TEAM_REVIEWERS}\"" | sed 's/, \|,/","/g') 210 | 211 | if [ "$(echo "$PULL_REQUEST_TEAM_REVIEWERS" | jq -e . > /dev/null 2>&1; echo $?)" -eq 0 ]; then 212 | echo "ADD TEAM REVIEWERS TO PULL REQUEST" 213 | else 214 | echo "JSON OF pull_request_team_reviewers IS INVALID: ${PULL_REQUEST_TEAM_REVIEWERS}" 215 | fi 216 | fi 217 | 218 | { 219 | REVIEWERS_URL="${REPO_URL}/pulls/${PULL_REQUESTS_NUMBER}/requested_reviewers" 220 | REVIEWERS_DATA="{\"reviewers\":[${PULL_REQUEST_REVIEWERS}],\"team_reviewers\":[${PULL_REQUEST_TEAM_REVIEWERS}]}" 221 | curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X POST --data "${REVIEWERS_DATA}" "${REVIEWERS_URL}" 222 | } || { 223 | echo "Failed to add reviewers." 224 | } 225 | fi 226 | 227 | echo "PULL REQUEST CREATED: ${PULL_REQUESTS_URL}" 228 | fi 229 | } 230 | 231 | push_to_branch() { 232 | BRANCH=${INPUT_LOCALIZATION_BRANCH_NAME} 233 | 234 | REPO_URL="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@${INPUT_GITHUB_BASE_URL}/${GITHUB_REPOSITORY}.git" 235 | 236 | echo "CONFIGURING GIT USER" 237 | git config --global user.email "${INPUT_GITHUB_USER_EMAIL}" 238 | git config --global user.name "${INPUT_GITHUB_USER_NAME}" 239 | 240 | if [ "$INPUT_SKIP_REF_CHECKOUT" != true ]; then 241 | CHECKOUT=${GITHUB_HEAD_REF:-${GITHUB_REF}} 242 | CHECKOUT=${CHECKOUT#refs/heads/} 243 | CHECKOUT=${CHECKOUT#refs/tags/} 244 | git checkout "${CHECKOUT}" 245 | fi 246 | 247 | if [ -n "$(git show-ref refs/heads/${BRANCH})" ]; then 248 | git checkout "${BRANCH}" 249 | else 250 | git checkout -b "${BRANCH}" 251 | fi 252 | 253 | git add . 254 | 255 | if [ ! -n "$(git status -s)" ]; then 256 | echo "NOTHING TO COMMIT" 257 | return 258 | fi 259 | 260 | echo "PUSH TO BRANCH ${BRANCH}" 261 | git commit --no-verify -m "${INPUT_COMMIT_MESSAGE}" 262 | git push --no-verify --force "${REPO_URL}" 263 | 264 | if [ "$INPUT_CREATE_PULL_REQUEST" = true ]; then 265 | create_pull_request "${BRANCH}" 266 | fi 267 | } 268 | 269 | view_debug_output() { 270 | if [ "$INPUT_DEBUG_MODE" = true ] || [ -n "$RUNNER_DEBUG" ]; then 271 | set -x 272 | fi 273 | } 274 | 275 | setup_commit_signing() { 276 | echo "FOUND PRIVATE KEY, WILL SETUP GPG KEYSTORE" 277 | 278 | echo "${INPUT_GPG_PRIVATE_KEY}" > private.key 279 | 280 | gpg --import --batch private.key 281 | 282 | GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep -o "rsa\d\+\/\(\w\+\)" | head -n1 | sed "s/rsa\d\+\/\(\w\+\)/\1/") 283 | GPG_KEY_OWNER_NAME=$(gpg --list-secret-keys --keyid-format=long | grep "uid" | sed "s/.\+] \(.\+\) <\(.\+\)>/\1/") 284 | GPG_KEY_OWNER_EMAIL=$(gpg --list-secret-keys --keyid-format=long | grep "uid" | sed "s/.\+] \(.\+\) <\(.\+\)>/\2/") 285 | echo "Imported key information:" 286 | echo " Key id: ${GPG_KEY_ID}" 287 | echo " Owner name: ${GPG_KEY_OWNER_NAME}" 288 | echo " Owner email: ${GPG_KEY_OWNER_EMAIL}" 289 | 290 | git config --global user.signingkey "$GPG_KEY_ID" 291 | git config --global commit.gpgsign true 292 | 293 | export GPG_TTY=$(tty) 294 | # generate sign to store passphrase in cache for "git commit" 295 | echo "test" | gpg --clearsign --pinentry-mode=loopback --passphrase "${INPUT_GPG_PASSPHRASE}" > /dev/null 2>&1 296 | 297 | rm private.key 298 | } 299 | 300 | echo "STARTING CROWDIN ACTION" 301 | 302 | cd "${GITHUB_WORKSPACE}" || exit 1 303 | 304 | git config --global --add safe.directory $GITHUB_WORKSPACE 305 | 306 | view_debug_output 307 | 308 | set -e 309 | 310 | #SET OPTIONS 311 | set -- --no-progress --no-colors 312 | 313 | if [ "$INPUT_DEBUG_MODE" = true ] || [ -n "$RUNNER_DEBUG" ]; then 314 | set -- "$@" --verbose --debug 315 | fi 316 | 317 | if [ -n "$INPUT_CROWDIN_BRANCH_NAME" ]; then 318 | set -- "$@" --branch="${INPUT_CROWDIN_BRANCH_NAME}" 319 | fi 320 | 321 | if [ -n "$INPUT_CONFIG" ]; then 322 | set -- "$@" --config="${INPUT_CONFIG}" 323 | fi 324 | 325 | if [ "$INPUT_DRYRUN_ACTION" = true ]; then 326 | set -- "$@" --dryrun 327 | fi 328 | 329 | #SET CONFIG OPTIONS 330 | if [ -n "$INPUT_PROJECT_ID" ]; then 331 | set -- "$@" --project-id=${INPUT_PROJECT_ID} 332 | fi 333 | 334 | if [ -n "$INPUT_TOKEN" ]; then 335 | set -- "$@" --token="${INPUT_TOKEN}" 336 | fi 337 | 338 | if [ -n "$INPUT_BASE_URL" ]; then 339 | set -- "$@" --base-url="${INPUT_BASE_URL}" 340 | fi 341 | 342 | if [ -n "$INPUT_BASE_PATH" ]; then 343 | set -- "$@" --base-path="${INPUT_BASE_PATH}" 344 | fi 345 | 346 | if [ -n "$INPUT_SOURCE" ]; then 347 | set -- "$@" --source="${INPUT_SOURCE}" 348 | fi 349 | 350 | if [ -n "$INPUT_TRANSLATION" ]; then 351 | set -- "$@" --translation="${INPUT_TRANSLATION}" 352 | fi 353 | 354 | if [ -n "$INPUT_COMMAND_ARGS" ]; then 355 | set -- "$@" ${INPUT_COMMAND_ARGS} 356 | fi 357 | 358 | #EXECUTE COMMANDS 359 | 360 | if [ -n "$INPUT_COMMAND" ]; then 361 | echo "RUNNING COMMAND crowdin $INPUT_COMMAND $INPUT_COMMAND_ARGS" 362 | crowdin $INPUT_COMMAND $INPUT_COMMAND_ARGS 363 | 364 | # in this case, we don't need to continue executing any further default behavior 365 | exit 0 366 | fi 367 | 368 | if [ "$INPUT_UPLOAD_SOURCES" = true ]; then 369 | upload_sources "$@" 370 | fi 371 | 372 | if [ "$INPUT_UPLOAD_TRANSLATIONS" = true ]; then 373 | upload_translations "$@" 374 | fi 375 | 376 | if [ "$INPUT_DOWNLOAD_SOURCES" = true ]; then 377 | download_sources "$@" 378 | 379 | if [ "$INPUT_PUSH_SOURCES" = true ]; then 380 | [ -z "${GITHUB_TOKEN}" ] && { 381 | echo "CAN NOT FIND 'GITHUB_TOKEN' IN ENVIRONMENT VARIABLES" 382 | exit 1 383 | } 384 | 385 | [ -n "${INPUT_GPG_PRIVATE_KEY}" ] && { 386 | setup_commit_signing 387 | } 388 | 389 | push_to_branch 390 | fi 391 | fi 392 | 393 | if [ "$INPUT_DOWNLOAD_TRANSLATIONS" = true ]; then 394 | download_translations "$@" 395 | 396 | if [ "$INPUT_PUSH_TRANSLATIONS" = true ]; then 397 | [ -z "${GITHUB_TOKEN}" ] && { 398 | echo "CAN NOT FIND 'GITHUB_TOKEN' IN ENVIRONMENT VARIABLES" 399 | exit 1 400 | } 401 | 402 | [ -n "${INPUT_GPG_PRIVATE_KEY}" ] && { 403 | setup_commit_signing 404 | } 405 | 406 | push_to_branch 407 | fi 408 | fi 409 | 410 | if [ "$INPUT_DOWNLOAD_BUNDLE" ]; then 411 | echo "DOWNLOADING BUNDLE $INPUT_DOWNLOAD_BUNDLE" 412 | 413 | crowdin bundle download $INPUT_DOWNLOAD_BUNDLE $@ 414 | 415 | if [ "$INPUT_PUSH_TRANSLATIONS" = true ]; then 416 | [ -z "${GITHUB_TOKEN}" ] && { 417 | echo "CAN NOT FIND 'GITHUB_TOKEN' IN ENVIRONMENT VARIABLES" 418 | exit 1 419 | } 420 | 421 | [ -n "${INPUT_GPG_PRIVATE_KEY}" ] && { 422 | setup_commit_signing 423 | } 424 | 425 | push_to_branch 426 | fi 427 | fi 428 | --------------------------------------------------------------------------------