├── .github ├── dependabot.yml └── workflows │ ├── build-docker.yaml │ ├── lint-docker.yaml │ ├── lint-markdown.yaml │ ├── lint-python.yaml │ ├── lint-yaml.yaml │ └── test-python.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yaml ├── poetry.lock ├── pyproject.toml ├── src └── translations.py └── tests └── test_translations.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build Docker image 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master, main] 7 | paths: 8 | - "**/Dockerfile" 9 | - "src/**" 10 | - ".dockerignore" 11 | - "Makefile" 12 | - ".github/workflows/build-docker.yaml" 13 | pull_request: 14 | branches: [master, main] 15 | paths: 16 | - "**/Dockerfile" 17 | - "src/**" 18 | - ".dockerignore" 19 | - "Makefile" 20 | - ".github/workflows/build-docker.yaml" 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | 28 | # Run this locally with act - https://github.com/nektos/act 29 | # act -j buildDocker 30 | buildDocker: 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 15 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Docker build 39 | run: make docker_build 40 | -------------------------------------------------------------------------------- /.github/workflows/lint-docker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Dockerfiles 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [main, master] 7 | paths: 8 | - "**/Dockerfile" 9 | - ".github/workflows/lint-docker.yaml" 10 | pull_request: 11 | branches: [main, master] 12 | paths: 13 | - "**/Dockerfile" 14 | - ".github/workflows/lint-docker.yaml" 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | # Run this locally with act - https://github.com/nektos/act 22 | # act -j lintDocker 23 | lintDocker: 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 15 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | # To lint multiple docker files 32 | # Ref: https://github.com/hadolint/hadolint-action/issues/54#issuecomment-1130157411 33 | - name: Install hadolint 34 | run: | 35 | docker pull hadolint/hadolint:latest 36 | container_id=$(docker create hadolint/hadolint) 37 | docker cp $container_id:/bin/hadolint . 38 | 39 | - name: Run hadolint 40 | run: | 41 | ./hadolint --ignore DL3007 --ignore DL3008 --ignore DL3013 --ignore DL3018 $(find . -iname Dockerfile) 42 | -------------------------------------------------------------------------------- /.github/workflows/lint-markdown.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Run this locally with act - https://github.com/nektos/act 3 | # act -j lintMarkdown 4 | name: Lint Markdown 5 | 6 | on: # yamllint disable-line rule:truthy 7 | push: 8 | branches: [master, main] 9 | paths: 10 | - '**.md' 11 | - '.github/workflows/lint-markdown.yaml' 12 | pull_request: 13 | branches: [master, main] 14 | paths: 15 | - '**.md' 16 | - '.github/workflows/lint-markdown.yaml' 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | lintMarkdown: 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 15 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Ruby 32 | # See https://github.com/ruby/setup-ruby#versioning 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: 3.0 36 | 37 | - name: Install dependencies 38 | run: gem install mdl 39 | 40 | - name: Run tests 41 | # Rule list: https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 42 | # Don't check for line length (MD013) 43 | # Don't care about list ordering (MD029) 44 | run: mdl --git-recurse --rules ~MD013,~MD029 . 45 | -------------------------------------------------------------------------------- /.github/workflows/lint-python.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Python Code 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master, main] 7 | paths: 8 | - "**.py" 9 | - ".github/workflows/lint-python.yaml" 10 | pull_request: 11 | branches: [master, main] 12 | paths: 13 | - "**.py" 14 | - ".github/workflows/lint-python.yaml" 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | 22 | # Run this locally with act - https://github.com/nektos/act 23 | # act -j lintPython 24 | lintPython: 25 | runs-on: ubuntu-latest 26 | timeout-minutes: 15 27 | 28 | steps: 29 | 30 | - name: Checkout respository 31 | uses: actions/checkout@v4 32 | 33 | - name: Check Python code for formatting 34 | uses: rickstaa/action-black@v1 35 | with: 36 | # Specify config with "--config " here 37 | black_args: > 38 | . 39 | -S 40 | --check 41 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Run this locally with act - https://github.com/nektos/act 3 | # act -j lintYaml 4 | name: Lint YAML 5 | 6 | on: # yamllint disable-line rule:truthy 7 | push: 8 | branches: [master, main] 9 | paths: 10 | - '**.yml' 11 | - '**.yaml' 12 | - '.github/workflows/**.yml' 13 | - '.github/workflows/**.yaml' 14 | pull_request: 15 | branches: [master, main] 16 | paths: 17 | - '**.yml' 18 | - '**.yaml' 19 | - '.github/workflows/**.yml' 20 | - '.github/workflows/**.yaml' 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | lintYaml: 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 15 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | - name: Check YAML files with linter 35 | uses: ibiqlik/action-yamllint@v3 36 | with: 37 | # All files under base dir 38 | file_or_dir: "." 39 | config_data: | 40 | extends: default 41 | yaml-files: 42 | - '*.yaml' 43 | - '*.yml' 44 | rules: 45 | document-start: 46 | level: warning 47 | line-length: 48 | level: warning 49 | new-line-at-end-of-file: 50 | level: warning 51 | trailing-spaces: 52 | level: warning 53 | -------------------------------------------------------------------------------- /.github/workflows/test-python.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Python Code 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master, main] 7 | paths: 8 | - "src/**" 9 | - "tests/**" 10 | - "requirements.txt" 11 | - "dev-requirements.txt" 12 | - ".github/workflows/test-python.yaml" 13 | pull_request: 14 | branches: [master, main] 15 | paths: 16 | - "src/**" 17 | - "tests/**" 18 | - "requirements.txt" 19 | - "dev-requirements.txt" 20 | - ".github/workflows/test-python.yaml" 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | 28 | # Run this locally with act - https://github.com/nektos/act 29 | # act -j testPython 30 | testPython: 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 15 33 | 34 | steps: 35 | 36 | - name: Checkout respository 37 | uses: actions/checkout@v4 38 | 39 | - name: Set up Python3 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: "3.11" 43 | 44 | - name: Install Poetry 45 | uses: snok/install-poetry@v1 46 | 47 | - name: Install dependencies 48 | run: make install 49 | 50 | - name: Lint 51 | run: make python_lint 52 | 53 | - name: Test 54 | run: make test 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | 109 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim as base 2 | 3 | FROM base as builder 4 | # Install Poetry 5 | RUN pip install --no-cache-dir poetry==1.8.3 6 | WORKDIR /app 7 | COPY pyproject.toml poetry.lock /app/ 8 | # virtual env is created in "/app/.venv" directory 9 | ENV POETRY_NO_INTERACTION=1 \ 10 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 11 | POETRY_VIRTUALENVS_CREATE=true \ 12 | POETRY_CACHE_DIR=/tmp/poetry_cache 13 | # Install dependencies 14 | RUN --mount=type=cache,target=/tmp/poetry_cache poetry install --only main --no-root 15 | RUN poetry install 16 | 17 | 18 | FROM base as runner 19 | COPY src /app/src 20 | COPY --from=builder /app/.venv /app/.venv 21 | ENV PATH="/app/.venv/bin:$PATH" 22 | # Code file to execute when the docker container starts up (`entrypoint.sh`) 23 | ENTRYPOINT ["/app/src/translations.py"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME := "test-action" 2 | DOCKER_TAG := "auto-translate-docker-action" 3 | 4 | docker_build: 5 | DOCKER_BUILDKIT=1 docker build -t ${DOCKER_TAG} -f Dockerfile . 6 | echo "Created docker image with tag ${DOCKER_TAG} and size `$(MAKE) --quiet docker_print_image_size`" 7 | 8 | # For local testing 9 | docker_run: docker_build 10 | docker rm ${BINARY_NAME}; docker run --name ${BINARY_NAME} -p 127.0.0.1:80:80 \ 11 | -it ${DOCKER_TAG} 12 | 13 | docker_print_image_size: 14 | docker image inspect ${DOCKER_TAG} --format='{{.Size}}' | numfmt --to=iec-i 15 | 16 | install: 17 | poetry install 18 | 19 | docker_lint: 20 | hadolint Dockerfile 21 | 22 | python_lint: 23 | # stop the build if there are Python syntax errors or undefined names 24 | poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 27 | poetry run black . -S 28 | 29 | python_test: 30 | poetry run pytest 31 | 32 | lint: docker_lint python_lint 33 | 34 | test: python_test 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-auto-translate 2 | 3 | [![Lint YAML](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-yaml.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-yaml.yaml) 4 | [![Lint Markdown](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-markdown.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-markdown.yaml) 5 | [![Lint Dockerfiles](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-docker.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-docker.yaml) 6 | [![Lint Python Code](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-python.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/lint-python.yaml) 7 | 8 | [![Test Python Code](https://github.com/ashishb/android-auto-translate/actions/workflows/test-python.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/test-python.yaml) 9 | 10 | [![Build Docker image](https://github.com/ashishb/android-auto-translate/actions/workflows/build-docker.yaml/badge.svg)](https://github.com/ashishb/android-auto-translate/actions/workflows/build-docker.yaml) 11 | 12 | This GitHub Action auto-translates Android's `strings.xml` and 13 | fills in the missing translations in all other languages. 14 | 15 | It also deletes any translations that is no longer defined in default `strings.xml`. 16 | 17 | To use this for a new language, say "es", first create "values-es/strings.xml" file with 18 | the following placeholder content 19 | 20 | ```xml 21 | 22 | 23 | ``` 24 | 25 | and save the following to `.github/workflows/translate-android.yaml` in the repository. 26 | 27 | ```yaml 28 | --- 29 | name: Automatically Translate Android App 30 | 31 | on: # yamllint disable-line rule:truthy 32 | push: 33 | branches: ["master", "main"] 34 | paths: 35 | - "**/strings.xml" 36 | 37 | concurrency: 38 | group: ${{ github.workflow }}-${{ github.ref }} 39 | cancel-in-progress: true 40 | 41 | jobs: 42 | 43 | # Run locally with "act -j translateAndroid" 44 | translateAndroid: 45 | 46 | runs-on: ubuntu-latest 47 | timeout-minutes: 15 48 | 49 | steps: 50 | - name: Checkout Repository 51 | uses: actions/checkout@v3 52 | 53 | - name: Translate strings.xml to supported languages 54 | uses: ashishb/android-auto-translate@master 55 | 56 | - name: Create Pull Request 57 | uses: peter-evans/create-pull-request@v4 58 | with: 59 | committer: "ashishb's Translation Bot " 60 | title: "[Bot]Auto-generated translations for non-English languages" 61 | body: "Auto-generated translations by [Android Auto Translate](https://github.com/ashishb/android-auto-translate) bot" 62 | ``` 63 | 64 | ## How to run this locally 65 | 66 | ```bash 67 | $ git clone https://github.com/ashishb/android-auto-translate 68 | ... 69 | $ cd android-auto-translate 70 | ... 71 | # Do note that this will modify strings.xml files in the specified 72 | # Android dir, so, don't forget to back them up first 73 | $ GITHUB_WORKSPACE= poetry run ./src/translations.py 74 | ``` 75 | 76 | ## Used by 77 | 78 | 1. [MusicSync](https://musicsync.ashishb.net/) 79 | 1. [ChatPaths](https://github.com/gyund/chatpaths/blob/main/.github/workflows/translate.yml) 80 | 1. [eFurry](https://github.com/eFurry/android-translation/blob/main/.github/workflows/translate-android.yml) 81 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # action.yml 3 | # Ref: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions 4 | name: "Android Auto Translate" 5 | description: "Auto Translate Android's strings.xml" 6 | runs: 7 | using: "docker" 8 | image: "Dockerfile" 9 | branding: 10 | icon: "globe" 11 | color: "blue" 12 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.4.2" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 11 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 12 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 13 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 14 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 15 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 16 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 17 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 18 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 19 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 20 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 21 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 22 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 23 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 24 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 25 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 26 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 27 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 28 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 29 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 30 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 31 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 32 | ] 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | packaging = ">=22.0" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 41 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 42 | 43 | [package.extras] 44 | colorama = ["colorama (>=0.4.3)"] 45 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 46 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 47 | uvloop = ["uvloop (>=0.15.2)"] 48 | 49 | [[package]] 50 | name = "certifi" 51 | version = "2024.7.4" 52 | description = "Python package for providing Mozilla's CA Bundle." 53 | optional = false 54 | python-versions = ">=3.6" 55 | files = [ 56 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 57 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 58 | ] 59 | 60 | [[package]] 61 | name = "chardet" 62 | version = "3.0.4" 63 | description = "Universal encoding detector for Python 2 and 3" 64 | optional = false 65 | python-versions = "*" 66 | files = [ 67 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 68 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 69 | ] 70 | 71 | [[package]] 72 | name = "click" 73 | version = "8.1.7" 74 | description = "Composable command line interface toolkit" 75 | optional = false 76 | python-versions = ">=3.7" 77 | files = [ 78 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 79 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 80 | ] 81 | 82 | [package.dependencies] 83 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 84 | 85 | [[package]] 86 | name = "colorama" 87 | version = "0.4.6" 88 | description = "Cross-platform colored terminal text." 89 | optional = false 90 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 91 | files = [ 92 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 93 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 94 | ] 95 | 96 | [[package]] 97 | name = "exceptiongroup" 98 | version = "1.2.1" 99 | description = "Backport of PEP 654 (exception groups)" 100 | optional = false 101 | python-versions = ">=3.7" 102 | files = [ 103 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 104 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 105 | ] 106 | 107 | [package.extras] 108 | test = ["pytest (>=6)"] 109 | 110 | [[package]] 111 | name = "flake8" 112 | version = "7.1.0" 113 | description = "the modular source code checker: pep8 pyflakes and co" 114 | optional = false 115 | python-versions = ">=3.8.1" 116 | files = [ 117 | {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, 118 | {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, 119 | ] 120 | 121 | [package.dependencies] 122 | mccabe = ">=0.7.0,<0.8.0" 123 | pycodestyle = ">=2.12.0,<2.13.0" 124 | pyflakes = ">=3.2.0,<3.3.0" 125 | 126 | [[package]] 127 | name = "googletrans" 128 | version = "4.0.0rc1" 129 | description = "Free Google Translate API for Python. Translates totally free of charge." 130 | optional = false 131 | python-versions = ">=3.6" 132 | files = [ 133 | {file = "googletrans-4.0.0rc1.tar.gz", hash = "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372"}, 134 | ] 135 | 136 | [package.dependencies] 137 | httpx = "0.13.3" 138 | 139 | [[package]] 140 | name = "h11" 141 | version = "0.9.0" 142 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 143 | optional = false 144 | python-versions = "*" 145 | files = [ 146 | {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, 147 | {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, 148 | ] 149 | 150 | [[package]] 151 | name = "h2" 152 | version = "3.2.0" 153 | description = "HTTP/2 State-Machine based protocol implementation" 154 | optional = false 155 | python-versions = "*" 156 | files = [ 157 | {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, 158 | {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, 159 | ] 160 | 161 | [package.dependencies] 162 | hpack = ">=3.0,<4" 163 | hyperframe = ">=5.2.0,<6" 164 | 165 | [[package]] 166 | name = "hpack" 167 | version = "3.0.0" 168 | description = "Pure-Python HPACK header compression" 169 | optional = false 170 | python-versions = "*" 171 | files = [ 172 | {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, 173 | {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, 174 | ] 175 | 176 | [[package]] 177 | name = "hstspreload" 178 | version = "2024.6.1" 179 | description = "Chromium HSTS Preload list as a Python package" 180 | optional = false 181 | python-versions = ">=3.6" 182 | files = [ 183 | {file = "hstspreload-2024.6.1-py3-none-any.whl", hash = "sha256:561e2382ca0a2faf789709c3e5a6b5f482bfece996fb1963a7cfe5d812e4bd04"}, 184 | {file = "hstspreload-2024.6.1.tar.gz", hash = "sha256:64be485ffe18b83680b2ddf82214937e87e76c458599dde99f2d81459f7be7de"}, 185 | ] 186 | 187 | [[package]] 188 | name = "httpcore" 189 | version = "0.9.1" 190 | description = "A minimal low-level HTTP client." 191 | optional = false 192 | python-versions = ">=3.6" 193 | files = [ 194 | {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, 195 | {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, 196 | ] 197 | 198 | [package.dependencies] 199 | h11 = ">=0.8,<0.10" 200 | h2 = "==3.*" 201 | sniffio = "==1.*" 202 | 203 | [[package]] 204 | name = "httpx" 205 | version = "0.13.3" 206 | description = "The next generation HTTP client." 207 | optional = false 208 | python-versions = ">=3.6" 209 | files = [ 210 | {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, 211 | {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, 212 | ] 213 | 214 | [package.dependencies] 215 | certifi = "*" 216 | chardet = "==3.*" 217 | hstspreload = "*" 218 | httpcore = "==0.9.*" 219 | idna = "==2.*" 220 | rfc3986 = ">=1.3,<2" 221 | sniffio = "*" 222 | 223 | [[package]] 224 | name = "hyperframe" 225 | version = "5.2.0" 226 | description = "HTTP/2 framing layer for Python" 227 | optional = false 228 | python-versions = "*" 229 | files = [ 230 | {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, 231 | {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, 232 | ] 233 | 234 | [[package]] 235 | name = "idna" 236 | version = "2.10" 237 | description = "Internationalized Domain Names in Applications (IDNA)" 238 | optional = false 239 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 240 | files = [ 241 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 242 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 243 | ] 244 | 245 | [[package]] 246 | name = "iniconfig" 247 | version = "2.0.0" 248 | description = "brain-dead simple config-ini parsing" 249 | optional = false 250 | python-versions = ">=3.7" 251 | files = [ 252 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 253 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 254 | ] 255 | 256 | [[package]] 257 | name = "mccabe" 258 | version = "0.7.0" 259 | description = "McCabe checker, plugin for flake8" 260 | optional = false 261 | python-versions = ">=3.6" 262 | files = [ 263 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 264 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 265 | ] 266 | 267 | [[package]] 268 | name = "mypy-extensions" 269 | version = "1.0.0" 270 | description = "Type system extensions for programs checked with the mypy type checker." 271 | optional = false 272 | python-versions = ">=3.5" 273 | files = [ 274 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 275 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 276 | ] 277 | 278 | [[package]] 279 | name = "packaging" 280 | version = "24.1" 281 | description = "Core utilities for Python packages" 282 | optional = false 283 | python-versions = ">=3.8" 284 | files = [ 285 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 286 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 287 | ] 288 | 289 | [[package]] 290 | name = "pathspec" 291 | version = "0.12.1" 292 | description = "Utility library for gitignore style pattern matching of file paths." 293 | optional = false 294 | python-versions = ">=3.8" 295 | files = [ 296 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 297 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 298 | ] 299 | 300 | [[package]] 301 | name = "platformdirs" 302 | version = "4.2.2" 303 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 304 | optional = false 305 | python-versions = ">=3.8" 306 | files = [ 307 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 308 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 309 | ] 310 | 311 | [package.extras] 312 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 313 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 314 | type = ["mypy (>=1.8)"] 315 | 316 | [[package]] 317 | name = "pluggy" 318 | version = "1.5.0" 319 | description = "plugin and hook calling mechanisms for python" 320 | optional = false 321 | python-versions = ">=3.8" 322 | files = [ 323 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 324 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 325 | ] 326 | 327 | [package.extras] 328 | dev = ["pre-commit", "tox"] 329 | testing = ["pytest", "pytest-benchmark"] 330 | 331 | [[package]] 332 | name = "pycodestyle" 333 | version = "2.12.0" 334 | description = "Python style guide checker" 335 | optional = false 336 | python-versions = ">=3.8" 337 | files = [ 338 | {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, 339 | {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, 340 | ] 341 | 342 | [[package]] 343 | name = "pyflakes" 344 | version = "3.2.0" 345 | description = "passive checker of Python programs" 346 | optional = false 347 | python-versions = ">=3.8" 348 | files = [ 349 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 350 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 351 | ] 352 | 353 | [[package]] 354 | name = "pytest" 355 | version = "8.2.2" 356 | description = "pytest: simple powerful testing with Python" 357 | optional = false 358 | python-versions = ">=3.8" 359 | files = [ 360 | {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, 361 | {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, 362 | ] 363 | 364 | [package.dependencies] 365 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 366 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 367 | iniconfig = "*" 368 | packaging = "*" 369 | pluggy = ">=1.5,<2.0" 370 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 371 | 372 | [package.extras] 373 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 374 | 375 | [[package]] 376 | name = "rfc3986" 377 | version = "1.5.0" 378 | description = "Validating URI References per RFC 3986" 379 | optional = false 380 | python-versions = "*" 381 | files = [ 382 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 383 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 384 | ] 385 | 386 | [package.extras] 387 | idna2008 = ["idna"] 388 | 389 | [[package]] 390 | name = "sniffio" 391 | version = "1.3.1" 392 | description = "Sniff out which async library your code is running under" 393 | optional = false 394 | python-versions = ">=3.7" 395 | files = [ 396 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 397 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 398 | ] 399 | 400 | [[package]] 401 | name = "tomli" 402 | version = "2.0.1" 403 | description = "A lil' TOML parser" 404 | optional = false 405 | python-versions = ">=3.7" 406 | files = [ 407 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 408 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 409 | ] 410 | 411 | [[package]] 412 | name = "typing-extensions" 413 | version = "4.12.2" 414 | description = "Backported and Experimental Type Hints for Python 3.8+" 415 | optional = false 416 | python-versions = ">=3.8" 417 | files = [ 418 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 419 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 420 | ] 421 | 422 | [metadata] 423 | lock-version = "2.0" 424 | python-versions = "^3.10" 425 | content-hash = "2e2cb24238fa36ede4d99751200f1316b0801d147544766aeafb53eccc7e519b" 426 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "src" 3 | version = "1.0" 4 | description = "Android Auto Translate" 5 | authors = ["Your Name "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | googletrans = "4.0.0rc1" 11 | 12 | [tool.poetry.group.dev.dependencies] 13 | pytest = "^8.2.2" 14 | black = "^24.4.2" 15 | flake8 = "^7.1.0" 16 | 17 | [build-system] 18 | requires = ["poetry-core"] 19 | build-backend = "poetry.core.masonry.api" 20 | -------------------------------------------------------------------------------- /src/translations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import copy 3 | import logging 4 | import os 5 | import pathlib 6 | import re 7 | import sys 8 | import typing 9 | import time 10 | 11 | import xml.etree.ElementTree as ET 12 | import googletrans 13 | 14 | _XML_ATTR_TRANSLATABLE = "translatable" 15 | _XML_ATTR_NAME = "name" 16 | 17 | 18 | def _get_english_string_files(base_dir: str) -> [str]: 19 | return list(pathlib.Path(base_dir).glob("**/src/*/res/values/strings.xml")) 20 | 21 | 22 | def _get_strings_to_translate(source_path: str) -> typing.Dict[str, ET.Element]: 23 | source_strings = dict() 24 | logging.debug("Input string file is %s", source_path) 25 | source_tree = ET.parse(source_path) 26 | for child in source_tree.getroot(): 27 | # Respect translatable attribute 28 | if child.attrib.get(_XML_ATTR_TRANSLATABLE) == "false": 29 | continue 30 | # Add this child to our dict where key is the name attribute 31 | source_strings[child.attrib.get(_XML_ATTR_NAME)] = child 32 | return source_strings 33 | 34 | 35 | def _get_target_languages(res_dir: str) -> typing.Dict[str, str]: 36 | # Map of language code -> strings.xml file 37 | result: typing.Dict[str, str] = {} 38 | for path in pathlib.Path(res_dir).glob("**/values-*"): 39 | strings_path = os.path.join(path, "strings.xml") 40 | if not os.path.exists(strings_path): 41 | continue 42 | lang_code = path.name.replace("values-", "") 43 | result[lang_code] = strings_path 44 | logging.debug('Target language is "%s" -> "%s"', lang_code, strings_path) 45 | return result 46 | 47 | 48 | def _normalize_response(text: str) -> str: 49 | text = text.replace("$、d", "$d") 50 | text = text.replace("$、s", "$s") 51 | # Replace Chinese % sign with standard English one or "%d" and "%s" won't work 52 | text = text.replace("%", "%") 53 | # Replace Arabic % sign with standard English one or "%d" and "%s" won't work 54 | text = text.replace("٪", "%") 55 | # Fix responses like \ "%1 $ S \" -> \"%1$s\" 56 | pattern = r'%\s*([\d*])\s*\$(,?)\s*([sdfSDF])' 57 | text = re.sub(pattern, r'%\1$\2\3', text) 58 | # Fix responses like %4 $ .1f -> %4$.1f 59 | pattern = r'%\s*([\d*])\s*\$(,?)\s*(\d*\.\d+)([fF])' 60 | text = re.sub(pattern, r'%\1$\2\3\4', text) 61 | # Remove extraneous spaces just before or after double-quotes 62 | # TODO(ashishb): If there are multiple pair of quotes then this 63 | # regex won't handle that properly and might remove more whitespace 64 | # than necessary 65 | text = re.sub(r'\"\s*(.*?)\s*\"', r'"\1"', text) 66 | text = re.sub(r'\(\s*(.*?)\s*\)', r'(\1)', text) 67 | # Replace unescaped quotes 68 | text = re.sub(r"([^\\])'", r"\1\'", text) 69 | 70 | text = text.replace('" ', '"') 71 | # text = text.replace(" \"", "\"") 72 | # text = text.replace(" \\\"", "\\\"") 73 | text = text.replace(r'\ "', r'\"') 74 | 75 | text = text.replace("%D", "%d") 76 | text = text.replace("%S", "%s") 77 | text = text.replace("$D", "$d") 78 | text = text.replace("$S", "$s") 79 | text = text.replace("d/ %", "d/%") 80 | text = text.replace("$,S", "$,s") 81 | text = text.replace("$,D", "$,d") 82 | text = text.replace("f/ %", "f/%") 83 | text = text.replace("...", "…") 84 | text = text.replace(" …", "…") 85 | text = text.replace("“", "\"") 86 | text = text.replace("”", "\"") 87 | # TODO: escape apostrophe as well 88 | return text 89 | 90 | 91 | def _translate( 92 | src_strings: typing.Dict[str, ET.Element], 93 | target_lang: str, 94 | translated_string_xml_file: str, 95 | ): 96 | # See the full list of language codes here 97 | # https://py-googletrans.readthedocs.io/en/latest/#googletrans-languages 98 | if target_lang == "zh-rTW": 99 | target_lang = "zh-TW" 100 | elif target_lang == "pt-rBR": # Use "Portuguese for "pt-rBR" 101 | target_lang = "pt" 102 | elif target_lang == "in": 103 | # Use "id" for "in" (in is old ISO-2 name for Indonesian that Android and Java use) 104 | target_lang = "id" 105 | elif target_lang == "iw": 106 | target_lang = "he" 107 | logging.info("Translating into '%s'...", translated_string_xml_file) 108 | translated_strings = _get_strings_to_translate(translated_string_xml_file) 109 | translations_to_add: typing.Dict[str, ET.Element] = dict() 110 | num_translated = 0 111 | translator = googletrans.Translator() 112 | 113 | # Strings that have been translated but are no longer part of the main 114 | # file 115 | translations_to_remove = list( 116 | filter(lambda x: x not in src_strings, translated_strings) 117 | ) 118 | logging.info( 119 | '%d strings will be removed from %s', 120 | len(translations_to_remove), 121 | translated_string_xml_file, 122 | ) 123 | 124 | for k in src_strings: 125 | if translated_strings.get(k, None) is not None: 126 | continue 127 | logging.debug("Requires translation in '%s' -> '%s'", target_lang, k) 128 | num_translated += 1 129 | try: 130 | translation = translator.translate(src_strings[k].text, dest=target_lang) 131 | except Exception as e: 132 | logging.error( 133 | "Failed to translate '%s' to '%s': %s" 134 | % (src_strings[k].text, target_lang, e) 135 | ) 136 | continue 137 | element = copy.deepcopy(src_strings[k]) 138 | element.text = _normalize_response(translation.text) 139 | translations_to_add[k] = element 140 | if num_translated % 10 == 0: 141 | logging.info("Num translated: %d/%d", num_translated, len(src_strings)) 142 | 143 | # sleep to avoid hitting Google Translation API's rate limits 144 | if num_translated > 1: 145 | time.sleep(0.7) 146 | 147 | logging.info( 148 | "Translated %d strings to (%s, %s)", 149 | num_translated, 150 | target_lang, 151 | translated_string_xml_file, 152 | ) 153 | if num_translated == 0 and len(translations_to_remove) == 0: 154 | return 155 | 156 | xml_tree = ET.parse(translated_string_xml_file) 157 | qualified_strings_root = xml_tree.getroot() 158 | for k in translations_to_remove: 159 | for qualified_string in qualified_strings_root: 160 | if qualified_string.attrib.get(_XML_ATTR_NAME) == k: 161 | qualified_strings_root.remove(qualified_string) 162 | for k in translations_to_add: 163 | qualified_strings_root.append(translations_to_add[k]) 164 | logging.info("Writing changes to '%s'", translated_string_xml_file) 165 | xml_tree.write( 166 | translated_string_xml_file, encoding="utf-8", xml_declaration=True, method="xml" 167 | ) 168 | 169 | 170 | # Note: some of the code in this file is inspired from a similar work referenced below that uses ChatGPT 171 | # Ref: https://proandroiddev.com/using-openais-text-completion-api-for-android-translations-80846e03b9cb 172 | 173 | 174 | def main(): 175 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 176 | base_dir = os.getenv("GITHUB_WORKSPACE", ".") 177 | 178 | files = _get_english_string_files(base_dir=base_dir) 179 | if len(files) == 0: 180 | logging.error("No strings.xml found") 181 | exit(1) 182 | for file in files: 183 | strings_to_translate = _get_strings_to_translate(file) 184 | # for k in strings_to_translate: 185 | # logging.debug("%s -> \"%s\"", k, strings_to_translate[k].text) 186 | 187 | target_lang_and_files = _get_target_languages(file.parent.parent) 188 | for target_lang in target_lang_and_files: 189 | _translate( 190 | strings_to_translate, target_lang, target_lang_and_files[target_lang] 191 | ) 192 | 193 | 194 | if __name__ == "__main__": 195 | main() 196 | -------------------------------------------------------------------------------- /tests/test_translations.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | myPath = os.path.dirname(os.path.abspath(__file__)) 5 | sys.path.insert(0, myPath + '/../') 6 | 7 | from src import translations 8 | 9 | 10 | def test_normalize_responses(): 11 | assert translations._normalize_response("") == "" 12 | assert translations._normalize_response("%D") == "%d" 13 | assert translations._normalize_response("%S") == "%s" 14 | assert translations._normalize_response("% 1 $ s") == "%1$s" 15 | assert translations._normalize_response("%1 $ s") == "%1$s" 16 | assert translations._normalize_response("% 1$ s") == "%1$s" 17 | assert translations._normalize_response("% 1 $s") == "%1$s" 18 | 19 | assert translations._normalize_response("% 1 $ d") == "%1$d" 20 | assert translations._normalize_response("%1 $ d") == "%1$d" 21 | assert translations._normalize_response("% 1$ d") == "%1$d" 22 | assert translations._normalize_response("% 1 $d") == "%1$d" 23 | 24 | assert translations._normalize_response("% 1 $ D") == "%1$d" 25 | assert translations._normalize_response("%1 $ D") == "%1$d" 26 | assert translations._normalize_response("% 1$ D") == "%1$d" 27 | assert translations._normalize_response("% 1 $D") == "%1$d" 28 | 29 | assert translations._normalize_response("%1 $, D") == "%1$,d" 30 | assert translations._normalize_response("%1 $, d") == "%1$,d" 31 | assert translations._normalize_response("%2 $, s") == "%2$,s" 32 | assert translations._normalize_response("de \"%1$s \"") == "de \"%1$s\"" 33 | assert ( 34 | translations._normalize_response("Dateien: %1$,d ( %2$s)") 35 | == "Dateien: %1$,d (%2$s)" 36 | ) 37 | 38 | assert translations._normalize_response("%4 $ .1f") == "%4$.1f" 39 | # E.g. 200/300 40 | assert translations._normalize_response("d/ %") == "d/%" 41 | assert translations._normalize_response("f/ %") == "f/%" 42 | 43 | # percentage sign that shows up in Chinese translations 44 | assert translations._normalize_response("%") == "%" 45 | # percentage sign from Arabic translations 46 | assert translations._normalize_response("٪") == "%" 47 | assert translations._normalize_response("...") == "…" 48 | assert translations._normalize_response("“") == "\"" 49 | assert translations._normalize_response("”") == "\"" 50 | 51 | # Happens with Japanese 52 | assert translations._normalize_response("$、d") == "$d" 53 | 54 | assert translations._normalize_response("l'artiste") == r"l\'artiste" 55 | --------------------------------------------------------------------------------