diff --git a/crowdin/Dockerfile b/crowdin/Dockerfile new file mode 100644 index 00000000..7683a565 --- /dev/null +++ b/crowdin/Dockerfile @@ -0,0 +1,8 @@ +FROM crowdin/cli:3.7.8 + +RUN apk --no-cache add curl git jq gnupg; + +COPY . . +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/crowdin/LICENSE b/crowdin/LICENSE new file mode 100644 index 00000000..cdbebac7 --- /dev/null +++ b/crowdin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Crowdin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crowdin/README.md b/crowdin/README.md new file mode 100644 index 00000000..be39282d --- /dev/null +++ b/crowdin/README.md @@ -0,0 +1,177 @@ +[

+The Crowdin GitHub Action is licensed under the MIT License. +See the LICENSE file distributed with this work for additional +information regarding copyright ownership. + +Except as contained in the LICENSE file, the name(s) of the above copyright +holders shall not be used in advertising or otherwise to promote the sale, +use or other dealings in this Software without prior written authorization. +diff --git a/crowdin/action.yml b/crowdin/action.yml new file mode 100644 index 00000000..82244469 --- /dev/null +++ b/crowdin/action.yml @@ -0,0 +1,175 @@ +name: 'crowdin-action' + +description: 'This action allows you to manage and synchronize localization resources with your Crowdin project' + +branding: + icon: 'refresh-cw' + color: 'green' + +inputs: + # upload sources options + upload_sources: + description: 'Upload sources to Crowdin' + default: 'true' + required: false + upload_sources_args: + description: 'Additional arguments which will be passed to the `upload sources` cli command' + default: '' + required: false + + # upload translations options + upload_translations: + description: 'Upload translations to Crowdin' + default: 'false' + required: false + upload_language: + description: 'Use this option to upload translations for a single specified language - Case-Sensitive' + required: false + auto_approve_imported: + description: 'Automatically approves uploaded translations' + default: 'false' + required: false + import_eq_suggestions: + description: 'Defines whether to add translation if it is equal to source string in Crowdin project' + default: 'false' + required: false + upload_translations_args: + description: 'Additional arguments which will be passed to the `upload translations` cli command' + default: '' + required: false + + # download translations options + download_translations: + description: 'Make pull request of Crowdin translations' + default: 'false' + required: false + download_language: + description: 'Use this option to download translations for a single specified language' + required: false + skip_untranslated_strings: + description: 'Skip untranslated strings in exported files (does not work with .docx, .html, .md and other document files)' + default: 'false' + required: false + skip_untranslated_files: + description: 'Omit downloading not fully translated files' + default: 'false' + required: false + export_only_approved: + 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' + default: 'false' + required: false + push_translations: + description: 'Download translations with pushing to branch' + default: 'true' + required: false + commit_message: + description: 'Commit message for download translations' + default: 'New Crowdin translations by Github Action' + required: false + localization_branch_name: + description: 'To download translations to the specified version branch' + default: 'l10n_crowdin_action' + required: false + create_pull_request: + description: 'Create pull request after pushing to branch' + default: 'true' + required: false + pull_request_title: + description: 'The title of the new pull request' + default: 'New Crowdin translations by Github Action' + required: false + pull_request_body: + description: 'The contents of the pull request' + required: false + pull_request_labels: + description: 'To add labels for created pull request' + required: false + pull_request_base_branch_name: + description: 'Create pull request to specified branch instead of default one' + required: false + download_translations_args: + description: 'Additional arguments which will be passed to the `download translations` cli command' + default: '' + required: false + + # branch options + add_crowdin_branch: + description: 'Option to create specified version branch in your Crowdin project' + required: false + new_branch_title: + description: 'Use to provide more details for translators. Title is available in UI only' + required: false + new_branch_export_pattern: + description: 'Branch export pattern. Defines branch name and path in resulting translations bundle' + required: false + new_branch_priority: + description: 'Defines priority level for each branch [LOW, NORMAL, HIGH]' + required: false + + delete_crowdin_branch: + description: 'Option to remove specified version branch in your Crowdin project' + required: false + + # global options + crowdin_branch_name: + description: 'Option to upload or download files to the specified version branch in your Crowdin project' + required: false + identity: + description: 'Option to specify a path to user-specific credentials, without / at the beginning' + required: false + config: + description: 'Option to specify a path to the configuration file, without / at the beginning' + required: false + dryrun_action: + description: 'Option to preview the list of managed files' + default: 'false' + required: false + + # GitHub (Enterprise) configuration + github_base_url: + description: 'Option to configure the base URL of GitHub server, if using GHE.' + default: 'github.com' + required: false + github_api_base_url: + description: 'Options to configure the base URL of GitHub server for API requests, if using GHE and different from api.github_base_url.' + required: false + github_user_name: + description: 'Option to configure GitHub user name on commits.' + default: 'Crowdin Bot' + required: false + github_user_email: + description: 'Option to configure GitHub user email on commits.' + default: 'support+bot@crowdin.com' + required: false + gpg_private_key: + description: 'GPG private key in ASCII-armored format' + required: false + + # config options + project_id: + description: 'Numerical ID of the project' + required: false + token: + description: 'Personal access token required for authentication' + required: false + base_url: + description: 'Base URL of Crowdin server for API requests execution' + required: false + base_path: + description: 'Path to your project directory on a local machine, without / at the beginning' + required: false + source: + description: 'Path to the source files, without / at the beginning' + required: false + translation: + description: 'Path to the translation files' + required: false + + # Misc configuration + working_directory: + description: "Path to project directory (to support monorepo projects)" + required: false + +runs: + using: docker + image: 'Dockerfile' diff --git a/crowdin/entrypoint.sh b/crowdin/entrypoint.sh new file mode 100755 index 00000000..1e58ad31 --- /dev/null +++ b/crowdin/entrypoint.sh @@ -0,0 +1,319 @@ +#!/bin/sh + +if [ "$INPUT_DEBUG_MODE" = true ]; then + echo '---------------------------' + printenv + echo '---------------------------' +fi + +upload_sources() { + if [ -n "$INPUT_UPLOAD_SOURCES_ARGS" ]; then + UPLOAD_SOURCES_OPTIONS="${UPLOAD_SOURCES_OPTIONS} ${INPUT_UPLOAD_SOURCES_ARGS}" + fi + + echo "UPLOAD SOURCES" + crowdin upload sources "$@" $UPLOAD_SOURCES_OPTIONS +} + +upload_translations() { + if [ -n "$INPUT_UPLOAD_LANGUAGE" ]; then + UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_UPLOAD_LANGUAGE}" + fi + + if [ "$INPUT_AUTO_APPROVE_IMPORTED" = true ]; then + UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --auto-approve-imported" + fi + + if [ "$INPUT_IMPORT_EQ_SUGGESTIONS" = true ]; then + UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} --import-eq-suggestions" + fi + + if [ -n "$INPUT_UPLOAD_TRANSLATIONS_ARGS" ]; then + UPLOAD_TRANSLATIONS_OPTIONS="${UPLOAD_TRANSLATIONS_OPTIONS} ${INPUT_UPLOAD_TRANSLATIONS_ARGS}" + fi + + echo "UPLOAD TRANSLATIONS" + crowdin upload translations "$@" $UPLOAD_TRANSLATIONS_OPTIONS +} + +download_translations() { + if [ -n "$INPUT_DOWNLOAD_LANGUAGE" ]; then + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_DOWNLOAD_LANGUAGE}" + elif [ -n "$INPUT_LANGUAGE" ]; then #back compatibility for older versions + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --language=${INPUT_LANGUAGE}" + fi + + if [ "$INPUT_SKIP_UNTRANSLATED_STRINGS" = true ]; then + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --skip-untranslated-strings" + fi + + if [ "$INPUT_SKIP_UNTRANSLATED_FILES" = true ]; then + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --skip-untranslated-files" + fi + + if [ "$INPUT_EXPORT_ONLY_APPROVED" = true ]; then + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} --export-only-approved" + fi + + if [ -n "$INPUT_DOWNLOAD_TRANSLATIONS_ARGS" ]; then + DOWNLOAD_TRANSLATIONS_OPTIONS="${DOWNLOAD_TRANSLATIONS_OPTIONS} ${INPUT_DOWNLOAD_TRANSLATIONS_ARGS}" + fi + + echo "DOWNLOAD TRANSLATIONS" + crowdin download "$@" $DOWNLOAD_TRANSLATIONS_OPTIONS +} + +create_pull_request() { + LOCALIZATION_BRANCH="${1}" + + AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" + HEADER="Accept: application/vnd.github.v3+json; application/vnd.github.antiope-preview+json; application/vnd.github.shadow-cat-preview+json" + + if [ -n "$INPUT_GITHUB_API_BASE_URL" ]; then + REPO_URL="https://${INPUT_GITHUB_API_BASE_URL}/repos/${GITHUB_REPOSITORY}" + else + REPO_URL="https://api.${INPUT_GITHUB_BASE_URL}/repos/${GITHUB_REPOSITORY}" + fi + + PULLS_URL="${REPO_URL}/pulls" + + echo "CHECK IF ISSET SAME PULL REQUEST" + + if [ -n "$INPUT_PULL_REQUEST_BASE_BRANCH_NAME" ]; then + BASE_BRANCH="$INPUT_PULL_REQUEST_BASE_BRANCH_NAME" + else + if [ -n "$GITHUB_HEAD_REF" ]; then + BASE_BRANCH=${GITHUB_HEAD_REF} + else + BASE_BRANCH=${GITHUB_REF#refs/heads/} + fi + fi + + PULL_REQUESTS_QUERY_PARAMS="?base=${BASE_BRANCH}&head=${LOCALIZATION_BRANCH}" + + PULL_REQUESTS=$(echo "$(curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X GET "${PULLS_URL}${PULL_REQUESTS_QUERY_PARAMS}")" | jq --raw-output '.[] | .head.ref ') + + # check if pull request exist + if echo "$PULL_REQUESTS " | grep -q "$LOCALIZATION_BRANCH "; then + echo "PULL REQUEST ALREADY EXIST" + else + echo "CREATE PULL REQUEST" + + if [ -n "$INPUT_PULL_REQUEST_BODY" ]; then + BODY=",\"body\":\"${INPUT_PULL_REQUEST_BODY//$'\n'/\\n}\"" + fi + + PULL_RESPONSE_DATA="{\"title\":\"${INPUT_PULL_REQUEST_TITLE}\", \"base\":\"${BASE_BRANCH}\", \"head\":\"${LOCALIZATION_BRANCH}\" ${BODY}}" + # create pull request + PULL_RESPONSE=$(curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X POST --data "${PULL_RESPONSE_DATA}" "${PULLS_URL}") + + set +x + PULL_REQUESTS_URL=$(echo "${PULL_RESPONSE}" | jq '.html_url') + PULL_REQUESTS_NUMBER=$(echo "${PULL_RESPONSE}" | jq '.number') + view_debug_output + + if [ -n "$INPUT_PULL_REQUEST_LABELS" ]; then + PULL_REQUEST_LABELS=$(echo "[\"${INPUT_PULL_REQUEST_LABELS}\"]" | sed 's/, \|,/","/g') + + if [ "$(echo "$PULL_REQUEST_LABELS" | jq -e . > /dev/null 2>&1; echo $?)" -eq 0 ]; then + echo "ADD LABELS TO PULL REQUEST" + + ISSUE_URL="${REPO_URL}/issues/${PULL_REQUESTS_NUMBER}" + + LABELS_DATA="{\"labels\":${PULL_REQUEST_LABELS}}" + + # add labels to created pull request + curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" -X PATCH --data "${LABELS_DATA}" "${ISSUE_URL}" + else + echo "JSON OF pull_request_labels IS INVALID: ${PULL_REQUEST_LABELS}" + fi + fi + + echo "PULL REQUEST CREATED: ${PULL_REQUESTS_URL}" + fi +} + +push_to_branch() { + LOCALIZATION_BRANCH=${INPUT_LOCALIZATION_BRANCH_NAME} + + REPO_URL="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@${INPUT_GITHUB_BASE_URL}/${GITHUB_REPOSITORY}.git" + + echo "CONFIGURATION GIT USER" + git config --global user.email "${INPUT_GITHUB_USER_EMAIL}" + git config --global user.name "${INPUT_GITHUB_USER_NAME}" + + if [ ${GITHUB_REF#refs/heads/} != $GITHUB_REF ]; then + git checkout "${GITHUB_REF#refs/heads/}" + fi + + if [ -n "$(git show-ref refs/heads/${LOCALIZATION_BRANCH})" ]; then + git checkout "${LOCALIZATION_BRANCH}" + else + git checkout -b "${LOCALIZATION_BRANCH}" + fi + + git add . + + if [ ! -n "$(git status -s)" ]; then + echo "NOTHING TO COMMIT" + return + fi + + echo "PUSH TO BRANCH ${LOCALIZATION_BRANCH}" + git commit --no-verify -m "${INPUT_COMMIT_MESSAGE}" + git push --no-verify --force "${REPO_URL}" + + if [ "$INPUT_CREATE_PULL_REQUEST" = true ]; then + create_pull_request "${LOCALIZATION_BRANCH}" + fi +} + +view_debug_output() { + if [ "$INPUT_DEBUG_MODE" = true ]; then + set -x + fi +} + +setup_commit_signing() { + echo "FOUND PRIVATE KEY, WILL SETUP GPG KEYSTORE" + + echo "${INPUT_GPG_PRIVATE_KEY}" > private.key + + gpg --import --batch private.key + + GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep -o "rsa\d\+\/\(\w\+\)" | head -n1 | sed "s/rsa\d\+\/\(\w\+\)/\1/") + GPG_KEY_OWNER_NAME=$(gpg --list-secret-keys --keyid-format=long | grep "uid" | sed "s/.\+] \(.\+\) <\(.\+\)>/\1/") + GPG_KEY_OWNER_EMAIL=$(gpg --list-secret-keys --keyid-format=long | grep "uid" | sed "s/.\+] \(.\+\) <\(.\+\)>/\2/") + echo "Imported key information:" + echo " Key id: ${GPG_KEY_ID}" + echo " Owner name: ${GPG_KEY_OWNER_NAME}" + echo " Owner email: ${GPG_KEY_OWNER_EMAIL}" + + git config --global user.signingkey "$GPG_KEY_ID" + git config --global commit.gpgsign true + + export GPG_TTY=$(tty) + # generate sign to store passphrase in cache for "git commit" + echo "test" | gpg --clearsign --pinentry-mode=loopback --passphrase "${INPUT_GPG_PASSPHRASE}" > /dev/null 2>&1 + + rm private.key +} + +get_branch_available_options() { + for OPTION in "$@" ; do + if echo "$OPTION" | egrep -vq "^(--dryrun|--branch|--source|--translation)"; then + AVAILABLE_OPTIONS="${AVAILABLE_OPTIONS} ${OPTION}" + fi + done + + echo "$AVAILABLE_OPTIONS" +} + +echo "STARTING CROWDIN ACTION" + +if [ -n "INPUT_WORKING_DIRECTORY" ]; then + WORKING_DIRECTORY="${GITHUB_WORKSPACE}/${INPUT_WORKING_DIRECTORY}" +else + WORKING_DIRECTORY="${GITHUB_WORKSPACE}" +fi + +cd "${WORKING_DIRECTORY}" || exit 1 + +git config --global --add safe.directory $GITHUB_WORKSPACE + +view_debug_output + +set -e + +#SET OPTIONS +set -- --no-progress --no-colors + +if [ "$INPUT_DEBUG_MODE" = true ]; then + set -- "$@" --verbose --debug +fi + +if [ -n "$INPUT_CROWDIN_BRANCH_NAME" ]; then + set -- "$@" --branch="${INPUT_CROWDIN_BRANCH_NAME}" +fi + +if [ -n "$INPUT_IDENTITY" ]; then + set -- "$@" --identity="${INPUT_IDENTITY}" +fi + +if [ -n "$INPUT_CONFIG" ]; then + set -- "$@" --config="${INPUT_CONFIG}" +fi + +if [ "$INPUT_DRYRUN_ACTION" = true ]; then + set -- "$@" --dryrun +fi + +#SET CONFIG OPTIONS +if [ -n "$INPUT_PROJECT_ID" ]; then + set -- "$@" --project-id=${INPUT_PROJECT_ID} +fi + +if [ -n "$INPUT_TOKEN" ]; then + set -- "$@" --token="${INPUT_TOKEN}" +fi + +if [ -n "$INPUT_BASE_URL" ]; then + set -- "$@" --base-url="${INPUT_BASE_URL}" +fi + +if [ -n "$INPUT_BASE_PATH" ]; then + set -- "$@" --base-path="${INPUT_BASE_PATH}" +fi + +if [ -n "$INPUT_SOURCE" ]; then + set -- "$@" --source="${INPUT_SOURCE}" +fi + +if [ -n "$INPUT_TRANSLATION" ]; then + set -- "$@" --translation="${INPUT_TRANSLATION}" +fi + +#EXECUTE COMMANDS + +if [ -n "$INPUT_ADD_CROWDIN_BRANCH" ]; then + NEW_BRANCH_OPTIONS=$( get_branch_available_options "$@" ) + + if [ -n "$INPUT_NEW_BRANCH_PRIORITY" ]; then + NEW_BRANCH_OPTIONS="${NEW_BRANCH_OPTIONS} --priority=${INPUT_NEW_BRANCH_PRIORITY}" + fi + + echo "CREATING BRANCH $INPUT_ADD_CROWDIN_BRANCH" + + crowdin branch add $INPUT_ADD_CROWDIN_BRANCH $NEW_BRANCH_OPTIONS --title="${INPUT_NEW_BRANCH_TITLE}" --export-pattern="${INPUT_NEW_BRANCH_EXPORT_PATTERN}" +fi + +if [ "$INPUT_UPLOAD_SOURCES" = true ]; then + upload_sources "$@" +fi + +if [ "$INPUT_UPLOAD_TRANSLATIONS" = true ]; then + upload_translations "$@" +fi + +if [ "$INPUT_DOWNLOAD_TRANSLATIONS" = true ]; then + download_translations "$@" + + if [ "$INPUT_PUSH_TRANSLATIONS" = true ]; then + [ -z "${GITHUB_TOKEN}" ] && { + echo "CAN NOT FIND 'GITHUB_TOKEN' IN ENVIRONMENT VARIABLES" + exit 1 + } + + [ -n "${INPUT_GPG_PRIVATE_KEY}" ] && [ -n "${INPUT_GPG_PASSPHRASE}" ] && { + setup_commit_signing + } + + push_to_branch + fi +fi + +if [ -n "$INPUT_DELETE_CROWDIN_BRANCH" ]; then + echo "REMOVING BRANCH $INPUT_DELETE_CROWDIN_BRANCH" + + crowdin branch delete $INPUT_DELETE_CROWDIN_BRANCH $( get_branch_available_options "$@" ) +fi