From 928b939c6d03cc18b3ea780b5490f6503382dac7 Mon Sep 17 00:00:00 2001 From: Niall Byrne <9848926+niall-byrne@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:18:08 -0400 Subject: [PATCH] feat(PRE-COMMIT): support gettext translations --- .cicd-tools/bin/manifest.sh | 4 + .cicd-tools/bin/toolbox.sh | 4 + .cicd-tools/bin/verify.sh | 4 + .../pre-commit/gettext-translations.sh | 1 + .cicd-tools/containers/gettext/Dockerfile | 19 + .../utilities}/Dockerfile | 20 +- .../utilities}/amd64/Dockerfile.sha256 | 0 .../utilities}/arm64/Dockerfile.sha256 | 0 .../0.toml_linting-0.workflow_linting.json | 2 +- .../0.toml_linting-1.workflow_linting.json | 2 +- .../1.toml_linting-0.workflow_linting.json | 2 +- .../1.toml_linting-1.workflow_linting.json | 2 +- .../workflow-container-gettext-multiarch.yml | 143 +++++ ...orkflow-container-utilities-multiarch.yml} | 27 +- .vale/Vocab/cicd-tools/accept.txt | 1 + README.md | 46 +- cicd-tools/boxes/0.1.0.tar.gz | Bin 17311 -> 20351 bytes cicd-tools/boxes/0.1.0/libraries/logging.sh | 1 + .../0.1.0/pre-commit/gettext-translations.sh | 524 ++++++++++++++++++ cookiecutter.json | 2 +- markdown/OVERVIEW.md | 4 +- scripts/containers.sh | 21 +- .../pre-commit/gettext-translations.sh | 1 + .../.github/workflows/workflow-push.yml | 29 + .../.pre-commit-config.yaml | 25 +- .../python/__init__.py | 0 .../python/locales/base.pot | 26 + .../python/locales/en/LC_MESSAGES/base.po | 26 + {{cookiecutter.project_slug}}/python/main.py | 22 + 29 files changed, 901 insertions(+), 57 deletions(-) create mode 120000 .cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh create mode 100644 .cicd-tools/containers/gettext/Dockerfile rename .cicd-tools/{container => containers/utilities}/Dockerfile (79%) rename .cicd-tools/{container => containers/utilities}/amd64/Dockerfile.sha256 (100%) rename .cicd-tools/{container => containers/utilities}/arm64/Dockerfile.sha256 (100%) create mode 100644 .github/workflows/workflow-container-gettext-multiarch.yml rename .github/workflows/{workflow-container-multiarch.yml => workflow-container-utilities-multiarch.yml} (88%) create mode 100755 cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh create mode 120000 {{cookiecutter.project_slug}}/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh create mode 100644 {{cookiecutter.project_slug}}/python/__init__.py create mode 100644 {{cookiecutter.project_slug}}/python/locales/base.pot create mode 100644 {{cookiecutter.project_slug}}/python/locales/en/LC_MESSAGES/base.po create mode 100644 {{cookiecutter.project_slug}}/python/main.py diff --git a/.cicd-tools/bin/manifest.sh b/.cicd-tools/bin/manifest.sh index 140069de..e8fcabbf 100755 --- a/.cicd-tools/bin/manifest.sh +++ b/.cicd-tools/bin/manifest.sh @@ -16,6 +16,10 @@ manifest() { } _manifest_args() { + local OPTARG + local OPTIND + local OPTION + while getopts "m:" OPTION; do case "$OPTION" in m) diff --git a/.cicd-tools/bin/toolbox.sh b/.cicd-tools/bin/toolbox.sh index c3a7177e..388d2424 100755 --- a/.cicd-tools/bin/toolbox.sh +++ b/.cicd-tools/bin/toolbox.sh @@ -39,6 +39,10 @@ main() { } _toolbox_args() { + local OPTARG + local OPTIND + local OPTION + while getopts "b:m:r:t:" OPTION; do case "$OPTION" in b) diff --git a/.cicd-tools/bin/verify.sh b/.cicd-tools/bin/verify.sh index 7441fff6..8ce00a3e 100755 --- a/.cicd-tools/bin/verify.sh +++ b/.cicd-tools/bin/verify.sh @@ -19,6 +19,10 @@ main() { } _verify_args() { + local OPTARG + local OPTIND + local OPTION + while getopts "k:" OPTION; do case "$OPTION" in k) diff --git a/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh b/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh new file mode 120000 index 00000000..07f3d7fa --- /dev/null +++ b/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh @@ -0,0 +1 @@ +../../../../cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh \ No newline at end of file diff --git a/.cicd-tools/containers/gettext/Dockerfile b/.cicd-tools/containers/gettext/Dockerfile new file mode 100644 index 00000000..880510ee --- /dev/null +++ b/.cicd-tools/containers/gettext/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:stable + +LABEL org.opencontainers.image.source=https://github.com/cicd-tools-org/cicd-tools +LABEL org.opencontainers.image.description="FOSS gettext binaries for CICD-Tools." + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install \ + -y \ + --no-install-recommends \ + gettext=0.* && \ + apt-get clean && \ + rm -rf /var/cache/apt/* && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /tmp/* + +RUN mkdir -p /mnt +WORKDIR /mnt diff --git a/.cicd-tools/container/Dockerfile b/.cicd-tools/containers/utilities/Dockerfile similarity index 79% rename from .cicd-tools/container/Dockerfile rename to .cicd-tools/containers/utilities/Dockerfile index 2a11a4f3..dcde8599 100644 --- a/.cicd-tools/container/Dockerfile +++ b/.cicd-tools/containers/utilities/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 as sha +FROM ubuntu:22.04 AS sha ARG BUILD_ARG_ARCH_FORMAT_1 ARG BUILD_ARG_ARCH_FORMAT_2 @@ -14,14 +14,14 @@ RUN apt-get update \ xz-utils \ && rm -rf /var/lib/apt/lists/* -ENV ACTIONLINT_VERSION "https://github.com/rhysd/actionlint/releases/download/v1.6.26/actionlint_1.6.26_linux_${BUILD_ARG_ARCH_FORMAT_1}.tar.gz" -ENV HADOLINT_VERSION "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-${BUILD_ARG_ARCH_FORMAT_2}" -ENV JQ_VERSION "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${BUILD_ARG_ARCH_FORMAT_1}" -ENV SHFMT_VERSION "https://github.com/mvdan/sh/releases/download/v3.7.0/shfmt_v3.7.0_linux_${BUILD_ARG_ARCH_FORMAT_1}" -ENV SHELLCHECK_VERSION "https://github.com/koalaman/shellcheck/releases/download/v0.9.0/shellcheck-v0.9.0.linux.${BUILD_ARG_ARCH_FORMAT_3}.tar.xz" -ENV TOMLL_VERSION "https://github.com/pelletier/go-toml/releases/download/v2.1.1/tomll_2.1.1_linux_${BUILD_ARG_ARCH_FORMAT_1}.tar.xz" -ENV VALE_VERSION "https://github.com/errata-ai/vale/releases/download/v2.30.0/vale_2.30.0_Linux_${BUILD_ARG_ARCH_FORMAT_4}.tar.gz" -ENV VALE3_VERSION "https://github.com/errata-ai/vale/releases/download/v3.6.0/vale_3.6.0_Linux_${BUILD_ARG_ARCH_FORMAT_4}.tar.gz" +ENV ACTIONLINT_VERSION="https://github.com/rhysd/actionlint/releases/download/v1.6.26/actionlint_1.6.26_linux_${BUILD_ARG_ARCH_FORMAT_1}.tar.gz" +ENV HADOLINT_VERSION="https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-${BUILD_ARG_ARCH_FORMAT_2}" +ENV JQ_VERSION="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${BUILD_ARG_ARCH_FORMAT_1}" +ENV SHFMT_VERSION="https://github.com/mvdan/sh/releases/download/v3.7.0/shfmt_v3.7.0_linux_${BUILD_ARG_ARCH_FORMAT_1}" +ENV SHELLCHECK_VERSION="https://github.com/koalaman/shellcheck/releases/download/v0.9.0/shellcheck-v0.9.0.linux.${BUILD_ARG_ARCH_FORMAT_3}.tar.xz" +ENV TOMLL_VERSION="https://github.com/pelletier/go-toml/releases/download/v2.1.1/tomll_2.1.1_linux_${BUILD_ARG_ARCH_FORMAT_1}.tar.xz" +ENV VALE_VERSION="https://github.com/errata-ai/vale/releases/download/v2.30.0/vale_2.30.0_Linux_${BUILD_ARG_ARCH_FORMAT_4}.tar.gz" +ENV VALE3_VERSION="https://github.com/errata-ai/vale/releases/download/v3.6.0/vale_3.6.0_Linux_${BUILD_ARG_ARCH_FORMAT_4}.tar.gz" RUN mkdir -p /dist @@ -60,7 +60,7 @@ WORKDIR /dist RUN sha256sum -c Dockerfile.sha256 && exit 0 || sha256sum /dist/* && exit 127 -FROM scratch as ship +FROM scratch AS ship LABEL org.opencontainers.image.source=https://github.com/cicd-tools-org/cicd-tools LABEL org.opencontainers.image.description="FOSS binaries for CICD-Tools." diff --git a/.cicd-tools/container/amd64/Dockerfile.sha256 b/.cicd-tools/containers/utilities/amd64/Dockerfile.sha256 similarity index 100% rename from .cicd-tools/container/amd64/Dockerfile.sha256 rename to .cicd-tools/containers/utilities/amd64/Dockerfile.sha256 diff --git a/.cicd-tools/container/arm64/Dockerfile.sha256 b/.cicd-tools/containers/utilities/arm64/Dockerfile.sha256 similarity index 100% rename from .cicd-tools/container/arm64/Dockerfile.sha256 rename to .cicd-tools/containers/utilities/arm64/Dockerfile.sha256 diff --git a/.github/scenarios/0.toml_linting-0.workflow_linting.json b/.github/scenarios/0.toml_linting-0.workflow_linting.json index a0c67e29..a6e59e4c 100644 --- a/.github/scenarios/0.toml_linting-0.workflow_linting.json +++ b/.github/scenarios/0.toml_linting-0.workflow_linting.json @@ -14,7 +14,7 @@ "_GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS": true, "_*DO_NOT_MODIFY_THIS_FILE*": "This file is created to assist with upgrading to future versions of this template.", "_copy_without_render": [ - ".cicd-tools/boxes/bootstrap/libraries/environment.sh", + ".cicd-tools/**/*", ".github/actions", "ansible_role/*", "scripts/*" diff --git a/.github/scenarios/0.toml_linting-1.workflow_linting.json b/.github/scenarios/0.toml_linting-1.workflow_linting.json index 53c0fe2c..0cdc083d 100644 --- a/.github/scenarios/0.toml_linting-1.workflow_linting.json +++ b/.github/scenarios/0.toml_linting-1.workflow_linting.json @@ -14,7 +14,7 @@ "_GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS": true, "_*DO_NOT_MODIFY_THIS_FILE*": "This file is created to assist with upgrading to future versions of this template.", "_copy_without_render": [ - ".cicd-tools/boxes/bootstrap/libraries/environment.sh", + ".cicd-tools/**/*", ".github/actions", "ansible_role/*", "scripts/*" diff --git a/.github/scenarios/1.toml_linting-0.workflow_linting.json b/.github/scenarios/1.toml_linting-0.workflow_linting.json index 4a7f9ef7..5038a31b 100644 --- a/.github/scenarios/1.toml_linting-0.workflow_linting.json +++ b/.github/scenarios/1.toml_linting-0.workflow_linting.json @@ -14,7 +14,7 @@ "_GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS": true, "_*DO_NOT_MODIFY_THIS_FILE*": "This file is created to assist with upgrading to future versions of this template.", "_copy_without_render": [ - ".cicd-tools/boxes/bootstrap/libraries/environment.sh", + ".cicd-tools/**/*", ".github/actions", "ansible_role/*", "scripts/*" diff --git a/.github/scenarios/1.toml_linting-1.workflow_linting.json b/.github/scenarios/1.toml_linting-1.workflow_linting.json index eac9c752..b875cb4a 100644 --- a/.github/scenarios/1.toml_linting-1.workflow_linting.json +++ b/.github/scenarios/1.toml_linting-1.workflow_linting.json @@ -14,7 +14,7 @@ "_GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS": true, "_*DO_NOT_MODIFY_THIS_FILE*": "This file is created to assist with upgrading to future versions of this template.", "_copy_without_render": [ - ".cicd-tools/boxes/bootstrap/libraries/environment.sh", + ".cicd-tools/**/*", ".github/actions", "ansible_role/*", "scripts/*" diff --git a/.github/workflows/workflow-container-gettext-multiarch.yml b/.github/workflows/workflow-container-gettext-multiarch.yml new file mode 100644 index 00000000..11a4b250 --- /dev/null +++ b/.github/workflows/workflow-container-gettext-multiarch.yml @@ -0,0 +1,143 @@ +--- +name: cicd-tooling-github-workflow-container-gettext-multiarch + +on: + push: + paths: + - ".cicd-tools/containers/gettext" + - ".github/workflows/workflow-container-gettext-multiarch.yml" + - ".github/workflows/job-*-container-*.yml" + - "scripts/container.sh" + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +# secrets: +# SLACK_WEBHOOK: +# description: "Optional, enables Slack notifications." +# required: false + +jobs: + + configuration: + uses: ./.github/workflows/job-00-cookiecutter-read_configuration.yml + + start: + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: ./.github/workflows/job-00-generic-notification.yml + with: + NOTIFICATION_EMOJI: ":vertical_traffic_light:" + NOTIFICATION_MESSAGE: "Multi-arch container build has started!" + WORKFLOW_NAME: "gettext-container" + + security: + needs: + - configuration + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: ./.github/workflows/job-10-generic-security_scan_credentials.yml + with: + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} + WORKFLOW_NAME: "gettext-container" + + scan: + permissions: + security-events: write + needs: + - configuration + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + strategy: + fail-fast: true + matrix: + include: + - build-platform: linux/amd64 + build-tag: linux-amd64 + - build-platform: linux/arm64 + build-tag: linux-arm64 + max-parallel: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_CONCURRENCY }} + uses: ./.github/workflows/job-10-container-security_scan_container.yml + with: + CONTEXT: .cicd-tools/containers/gettext + FAIL_BUILD: true + FAIL_THRESHOLD: "critical" + FIXED_ONLY: true + IMAGE_NAME: cicd-tools-org/cicd-tools-gettext + IMAGE_TAG: ${{ matrix.build-tag }} + PLATFORM: ${{ matrix.build-platform }} + REQUIRES_QEMU: true + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} + WORKFLOW_NAME: "gettext-container" + + lint: + needs: + - configuration + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: ./.github/workflows/job-80-container-dockerfile_linter.yml + with: + DOCKERFILE: .cicd-tools/containers/gettext/Dockerfile + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} + WORKFLOW_NAME: "gettext-container" + + push: + needs: + - configuration + - lint + - scan + - security + - start + permissions: + packages: write + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + strategy: + fail-fast: true + matrix: + include: + - build-platform: linux/amd64 + build-tag: linux-amd64 + - build-platform: linux/arm64 + build-tag: linux-arm64 + max-parallel: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_CONCURRENCY }} + uses: ./.github/workflows/job-95-container-push.yml + with: + CONTEXT: .cicd-tools/containers/gettext + IMAGE_NAME: cicd-tools-org/cicd-tools-gettext + IMAGE_TAG: ${{ matrix.build-tag }} + PLATFORM: ${{ matrix.build-platform }} + REQUIRES_QEMU: true + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} + WORKFLOW_NAME: "gettext-container" + + multiarch: + needs: + - configuration + - push + permissions: + packages: write + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: ./.github/workflows/job-95-container-multiarch.yml + with: + IMAGE_GIT: true + IMAGE_LATEST: true + IMAGE_NAME: cicd-tools-org/cicd-tools-gettext + MULTIARCH_TAG: "multiarch" + SOURCE_TAGS: | + linux-amd64 + linux-arm64 + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} + WORKFLOW_NAME: "gettext-container" + + success: + needs: + - multiarch + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: ./.github/workflows/job-00-generic-notification.yml + with: + NOTIFICATION_EMOJI: ":checkered_flag:" + NOTIFICATION_MESSAGE: "Multi-arch container build has completed successfully!" + WORKFLOW_NAME: "gettext-container" diff --git a/.github/workflows/workflow-container-multiarch.yml b/.github/workflows/workflow-container-utilities-multiarch.yml similarity index 88% rename from .github/workflows/workflow-container-multiarch.yml rename to .github/workflows/workflow-container-utilities-multiarch.yml index 537a5b34..fa9d4c43 100644 --- a/.github/workflows/workflow-container-multiarch.yml +++ b/.github/workflows/workflow-container-utilities-multiarch.yml @@ -1,13 +1,12 @@ --- -name: cicd-tooling-github-workflow-container-multiarch +name: cicd-tooling-github-workflow-container-utilities-multiarch on: push: paths: - - ".github/workflows/workflow-container-multiarch.yml" + - ".cicd-tools/containers/utilities" + - ".github/workflows/workflow-container-utilities-multiarch.yml" - ".github/workflows/job-*-container-*.yml" - - ".cicd-tools/container/Dockerfile" - - ".cicd-tools/container/Dockerfile.sha256" - "scripts/container.sh" schedule: - cron: "0 6 * * 1" @@ -30,7 +29,7 @@ jobs: with: NOTIFICATION_EMOJI: ":vertical_traffic_light:" NOTIFICATION_MESSAGE: "Multi-arch container build has started!" - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" security: needs: @@ -40,7 +39,7 @@ jobs: uses: ./.github/workflows/job-10-generic-security_scan_credentials.yml with: VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" scan: permissions: @@ -71,7 +70,7 @@ jobs: uses: ./.github/workflows/job-10-container-security_scan_container.yml with: BUILD_ARGS: ${{ matrix.build-args }} - CONTEXT: .cicd-tools/container + CONTEXT: .cicd-tools/containers/utilities FAIL_BUILD: true FAIL_THRESHOLD: "critical" FIXED_ONLY: true @@ -79,7 +78,7 @@ jobs: PLATFORM: ${{ matrix.build-platform }} REQUIRES_QEMU: true VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" lint: needs: @@ -88,9 +87,9 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} uses: ./.github/workflows/job-80-container-dockerfile_linter.yml with: - DOCKERFILE: .cicd-tools/container/Dockerfile + DOCKERFILE: .cicd-tools/containers/utilities/Dockerfile VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" push: needs: @@ -125,12 +124,12 @@ jobs: uses: ./.github/workflows/job-95-container-push.yml with: BUILD_ARGS: ${{ matrix.build-args }} - CONTEXT: .cicd-tools/container + CONTEXT: .cicd-tools/containers/utilities IMAGE_TAG: ${{ matrix.build-tag }} PLATFORM: ${{ matrix.build-platform }} REQUIRES_QEMU: true VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" multiarch: needs: @@ -149,7 +148,7 @@ jobs: linux-amd64 linux-arm64 VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.COOKIECUTTER_CONFIGURATION)._GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS }} - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" success: needs: @@ -160,4 +159,4 @@ jobs: with: NOTIFICATION_EMOJI: ":checkered_flag:" NOTIFICATION_MESSAGE: "Multi-arch container build has completed successfully!" - WORKFLOW_NAME: "container" + WORKFLOW_NAME: "utilities-container" diff --git a/.vale/Vocab/cicd-tools/accept.txt b/.vale/Vocab/cicd-tools/accept.txt index ab686c38..f06c1b9e 100644 --- a/.vale/Vocab/cicd-tools/accept.txt +++ b/.vale/Vocab/cicd-tools/accept.txt @@ -1,4 +1,5 @@ anchore codebase's +gettext mac_maker tmate diff --git a/README.md b/README.md index 1656a70e..25113cae 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,29 @@ Managed, Centralized CI/CD Components. A platform in a repository. -| Builds: [main](https://github.com/cicd-tools-org/cicd-tools/tree/main) | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-multiarch.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-multiarch.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml) | - -| Builds: [dev](https://github.com/cicd-tools-org/cicd-tools/tree/dev) | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-multiarch.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-multiarch.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml) | -| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml) | +| Builds: [main](https://github.com/cicd-tools-org/cicd-tools/tree/main) | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-gettext-multiarch.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-gettext-multiarch.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-utilities-multiarch.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-utilities-multiarch.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml/badge.svg?branch=main)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml) | + +| Builds: [dev](https://github.com/cicd-tools-org/cicd-tools/tree/dev) | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-ansible-role-molecule.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-compose-command.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-gettext-multiarch.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-gettext-multiarch.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-utilities-multiarch.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-container-utilities-multiarch.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-cookiecutter-template.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-mac_maker.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-meta_tests.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-npm-node_application.yml) | +| [![workflow-link](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml/badge.svg?branch=dev)](https://github.com/cicd-tools-org/cicd-tools/actions/workflows/workflow-poetry-command.yml) | ## About @@ -37,7 +39,7 @@ At the current time only [GitHub Actions](https://docs.github.com/en/actions) ar CICD-Tools provides four consumables to end-user projects that together form the basis of a managed CI solution: 1. Customized [pre-commit hooks](https://github.com/cicd-tools-org/pre-commit) for end-user projects. -2. A custom [Docker container](.cicd-tools/container/Dockerfile) which supplies the required binary tools for the pre-commit hooks. +2. A custom [Docker container](.cicd-tools/containers/utilities/Dockerfile) which supplies the required binary tools for the pre-commit hooks. 3. Remotely consumable [GitHub "Jobs"](.github/workflows) that are actively maintained. 4. A custom [packaging system](https://github.com/cicd-tools-org/manifest/blob/main/manifest.json.asc) that securely delivers upgradable [Toolboxes](cicd-tools/boxes) full of scripts for the workflows. diff --git a/cicd-tools/boxes/0.1.0.tar.gz b/cicd-tools/boxes/0.1.0.tar.gz index 286dcec1e2087618d932068988ca806efdbc57a7..8f32052fd0526dbf6f044140f7f9881c48cfc9c9 100644 GIT binary patch literal 20351 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
  • yLq8Zd65cNZ5F(hCuA-}_+ZK62H;mHa zvm2&6G(41)tLx$@`_yvxb1B95|Iv!PQ(l1i_5aQ7<@vA0lzHcW?%?kS`TBn~9{;nw zv%A#)7g0V>|37mRu^WL>!ZR%6dISl+U8)AK>jg$8)rP1o;Kz5nluAwOt$!7R>Ajqu zrG$BN9h(jOL+htB?dYw6iO=zp2cFFiB%4Xl+5zeHU9A@G#Mr`&$B{F7g6wcU+ z`2J&vW*cZmOa@?@F+Ya!*1 z^FNmvkfZ-U-i+P%+0i-}egt>=W_Me;KhfmwUrXz#-n?NQj@eO|yG zw0gb9i`31ZvEjnq1Jc=47c$W5ZUmwYaXF=Ng&At;Z@<`Q?Dl43=%$%xNuZ=*j`m6I z!D}5;w;GOPJGi0auw2s;Wl>{?auRRqK1{;&J$x1H0mMHk5xuWl&t7&qKfqu~;O8|y*+s7lIRCZ#cWq( zZ{^B#ys18--MR@Mnh9^}QJvy&-ut1RTUtWo%GZeIj;0qgS{8BaCmYL|IAvUj^f~>J zHL-Zf_hRL)$Ms~t5-7*KS_&zolRpgnX$U{-qot1hr?XiW4j$eo4&n#-;=n{I@tB+j z5_RMqqjc#?@6X`4)bFn3>3`KA8@O>cy;xZM=ab!~{=bMa@BDv+_)q%maV-Da<}&~9 zLP}i!kIZ;P^KUR!yVrXO$*?STC#V|!I-sygmbsGd!K^8nAd?-tB|@%Rs_kvX1bPh9%Ioa-RK{QQ4w zdvodkSxC9_{Ldi>a^wH$?~(kE+e`kxn35I$6Q1$J2q3J2?7uT{UqH2Z7!cN(JN~D! z-|F_Y#(ukzcmM!`6R7#GyW;fkz%g`xw6tgg*69&7MY`>NEZ@yhbuKrP;c>^3X*;HO zQw$K{XJU|iK!6K&e;<)8jiYwffd~2;4?W-=>GW@Zp5MLo9)XM0R{h zaS{z&+Ele^BGCuLfiL3vO(4xHD+=8Z zbWP@{1A|K*$4~l?U(+VGHS$mEuB?E~rW!Vxm=gn3VW@;E5B5P^d+H2~dbtu)6WKBu znP(1OFTdPc2kZ$?560fPWT>=w6!V zOyQRs&_A9+r^bli?Ei|J(Mx-z1+vDGpSBx5fHcFArYM*w- zaM`HgLZbTWtW*v@l1|w~WY1+)Rd-zhyF@{3guf60z_?Kv05FkbBV(M$&^GXd6dT_k zLV_X`fJw2y$7mlu@5nW}gVkwTp5dJ9bk%}a1C3qeBqEmuyN3)1 z4|Q~m&KeT*b;2qAG*(r*T_oXMm}Q+%o3KaC*i$g9;Wk{I=a#sjrl}QYhTQRyNOp0c zNrdwf$tv_if}}}LYs{x7p>ZlOj7Lj-`}96z+>Y^wPb~xgkxQ!nH?~K{U^+5X5xwT# zSr5$h|38W5|KHrLF7y8^q(t>U^y9_vbYgBYB8Y+nL=o4szBo1OptIj<9`A$CkHsnV zwAPQ!{o}pXp4M%>X#JQ~6P#Y4X=wsCnvWYzcP?C^0wui{M2prc0S9Mr~44`0H<}VuHHIKz9M1?5RTeyl}&ccrlWX7*HS? zOwtLPZ@=ZgPBMD}#AUaC^DjXMN$5d=A8%H>Q>#aJXl1zifc`SHEqH}0Fx8yb*tAdq zSzTZEl}+_Gpw?WBqSi^?GuL%iGeBQdMa=@7YuOo87@{dj%p4hWMtwMtc^2(k1*YqLh3`zFWiZIybE$Df

    b*#S| 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 K&*cXI-~j;8^Hyd6 diff --git a/cicd-tools/boxes/0.1.0/libraries/logging.sh b/cicd-tools/boxes/0.1.0/libraries/logging.sh index 12972271..dd3a3169 100644 --- a/cicd-tools/boxes/0.1.0/libraries/logging.sh +++ b/cicd-tools/boxes/0.1.0/libraries/logging.sh @@ -45,6 +45,7 @@ function log() { } function _log_args() { + local OPTARG local OPTIND local OPTION diff --git a/cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh b/cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh new file mode 100755 index 00000000..0d54db3e --- /dev/null +++ b/cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh @@ -0,0 +1,524 @@ +#!/bin/bash + +# Runs gettext utilities to manage translation related project tasks. +# Requires the gettext binary: https://www.gnu.org/software/gettext/ + +# pre-commit script. + +set -eo pipefail + +# shellcheck source=/dev/null +source "$(dirname -- "${BASH_SOURCE[0]}")/../libraries/logging.sh" + +GETTEXT_TRANSLATIONS_SED_PATTERN='s,^\"Content-Type: text/plain; charset=CHARSET\\n\"$,\"Content-Type: text/plain; charset=UTF-8\\n\",g' +GETTEXT_TRANSLATIONS_EXAMPLE_LANGUAGES_CODES_URL="https://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html" + +_gettext_translations_args() { + local GETTEXT_TRANSLATIONS_COMMAND + local OPTARG + local OPTIND + local OPTION + + if [[ -z "${1}" ]]; then + _gettext_translations_usage + fi + + OPTIND=1 + GETTEXT_TRANSLATIONS_COMMAND="${1}" + shift + + while getopts "b:c:e:i:m:p:r:s:u" OPTION; do + case "${OPTION}" in + b) + GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME="${OPTARG}" + ;; + c) + GETTEXT_TRANSLATIONS_CODE_BASE_PATH="${OPTARG}" + ;; + e) + GETTEXT_TRANSLATIONS_EMAIL_ADDRESS="${OPTARG}" + ;; + i) + GETTEXT_TRANSLATIONS_DOCKER_IMAGE="${OPTARG}" + ;; + m) + GETTEXT_TRANSLATIONS_EMPTY_MESSAGE_MATCH="${OPTARG}" + ;; + p) + GETTEXT_TRANSLATIONS_BASE_PATH="${OPTARG}" + ;; + r) + GETTEXT_TRANSLATIONS_CODE_BASE_REGEX="${OPTARG}" + ;; + s) + GETTEXT_TRANSLATIONS_LANGUAGES_BEING_SKIPPED+=("${OPTARG}") + ;; + u) + GETTEXT_TRANSLATIONS_UTF8_OVERRIDE="1" + ;; + \?) + _gettext_translations_usage + ;; + :) + _gettext_translations_usage + ;; + *) + _gettext_translations_usage + ;; + esac + done + shift $((OPTIND - 1)) + + case "${GETTEXT_TRANSLATIONS_COMMAND}" in + add) + if [[ -z "${GETTEXT_TRANSLATIONS_BASE_PATH}" ]]; then + _gettext_translations_usage_title + _gettext_translations_usage_add + _gettext_translations_usage_terminate + fi + gettext_translations_add + ;; + compile) + if [[ -z "${GETTEXT_TRANSLATIONS_BASE_PATH}" ]]; then + _gettext_translations_usage_title + _gettext_translations_usage_compile + _gettext_translations_usage_terminate + fi + gettext_translations_compile + ;; + missing) + if [[ -z "${GETTEXT_TRANSLATIONS_BASE_PATH}" ]] || + [[ -z "${GETTEXT_TRANSLATIONS_EMPTY_MESSAGE_MATCH}" ]]; then + _gettext_translations_usage_title + _gettext_translations_usage_missing + _gettext_translations_usage_terminate + fi + gettext_translations_missing + ;; + update) + if [[ -z "${GETTEXT_TRANSLATIONS_BASE_PATH}" ]] || + [[ -z "${GETTEXT_TRANSLATIONS_CODE_BASE_PATH}" ]] || + [[ -z "${GETTEXT_TRANSLATIONS_CODE_BASE_REGEX}" ]] || + [[ -z "${GETTEXT_TRANSLATIONS_EMAIL_ADDRESS}" ]] || + [[ -z "${GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME}" ]]; then + _gettext_translations_usage_title + _gettext_translations_usage_update + _gettext_translations_usage_terminate + fi + gettext_translations_update + ;; + :) + _gettext_translations_usage + + ;; + *) + _gettext_translations_usage + ;; + esac +} + +_gettext_translations_check_existing_base_path() { + if [[ ! -d "${GETTEXT_TRANSLATIONS_BASE_PATH}" ]]; then + log "ERROR" "The specified path '${GETTEXT_TRANSLATIONS_BASE_PATH}' does not exist." + return 127 + fi +} + +_gettext_translations_check_existing_po_files() { + # $1: LANGUAGE NAME + if ! ls -la "${GETTEXT_TRANSLATIONS_BASE_PATH}/${1}/LC_MESSAGES/"*.po 1> /dev/null 2>&1; then + log "ERROR" "There are no po files in '${GETTEXT_TRANSLATIONS_BASE_PATH}/${1}/LC_MESSAGES'." + return 127 + fi + +} + +_gettext_translations_check_existing_pot_files() { + if ! ls -la "${GETTEXT_TRANSLATIONS_BASE_PATH}/"*.pot 1> /dev/null 2>&1; then + log "ERROR" "There are no pot files in '${GETTEXT_TRANSLATIONS_BASE_PATH}'." + return 127 + fi +} + +_gettext_translations_generate_or_update_pot_file() { + local GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE + local GETTEXT_TRANSLATIONS_SOURCE_FILE + local GETTEXT_TRANSLATIONS_SOURCE_FILES + local GETTEXT_TRANSLATIONS_TMP_DIR + local GETTEXT_TRANSLATIONS_UPDATED_TEMP_FILE + + GETTEXT_TRANSLATIONS_SOURCE_FILES=() + + if [[ ! -d "${GETTEXT_TRANSLATIONS_CODE_BASE_PATH}" ]]; then + log "ERROR" "The specified code base path '${GETTEXT_TRANSLATIONS_CODE_BASE_PATH}' does not exist." + return 127 + fi + + GETTEXT_TRANSLATIONS_TMP_DIR="$(mktemp -d "./tmp.XXXXXXXXX")" + GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE="${GETTEXT_TRANSLATIONS_TMP_DIR}/extracted.pot" + GETTEXT_TRANSLATIONS_UPDATED_TEMP_FILE="${GETTEXT_TRANSLATIONS_TMP_DIR}/updated.pot" + + # shellcheck disable=SC2064 + trap "rm -rf \"${GETTEXT_TRANSLATIONS_TMP_DIR}\"" EXIT + + log "INFO" "Extracting strings from all '${GETTEXT_TRANSLATIONS_CODE_BASE_REGEX}' files in '${GETTEXT_TRANSLATIONS_CODE_BASE_PATH}' ..." + + while IFS= read -r -d $'\0' GETTEXT_TRANSLATIONS_SOURCE_FILE; do + GETTEXT_TRANSLATIONS_SOURCE_FILES+=("${GETTEXT_TRANSLATIONS_SOURCE_FILE}") + done < <( + find \ + "${GETTEXT_TRANSLATIONS_CODE_BASE_PATH}" \ + -iname "${GETTEXT_TRANSLATIONS_CODE_BASE_REGEX}" \ + -print0 + ) + + _gettext_translations_run_binary xgettext \ + --force-po \ + --from-code=UTF-8 \ + --msgid-bugs-address="${GETTEXT_TRANSLATIONS_EMAIL_ADDRESS}" \ + -d "${GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME}" \ + -o "${GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE}" \ + "${GETTEXT_TRANSLATIONS_SOURCE_FILES[@]}" + + if [[ ! -f "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" ]]; then + log "INFO" "Writing extract strings to '${GETTEXT_TRANSLATIONS_BASE_POT_FILE}' ..." + _gettext_translations_write_base_pot_file "${GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE}" + return + fi + + log "INFO" "Merging changes to '${GETTEXT_TRANSLATIONS_BASE_POT_FILE}' from '${GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE}' ..." + + _gettext_translations_run_binary msgmerge \ + -q \ + -N \ + "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" \ + "${GETTEXT_TRANSLATIONS_EXTRACTED_TEMP_FILE}" \ + -o "${GETTEXT_TRANSLATIONS_UPDATED_TEMP_FILE}" + + _gettext_translations_write_base_pot_file "${GETTEXT_TRANSLATIONS_UPDATED_TEMP_FILE}" +} + +_gettext_translations_identify_existing_languages() { + local GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH + + while IFS= read -r -d '' GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH; do + if ! ls -la "${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH}/LC_MESSAGES/"*.po 1> /dev/null 2>&1; then + log "WARNING" "Skipping '${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH}' as there are no .po files. " + else + GETTEXT_TRANSLATIONS_LANGUAGES+=("$(basename "${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH#*/}")") + fi + done < <(find "${GETTEXT_TRANSLATIONS_BASE_PATH}" -maxdepth 1 -mindepth 1 -type d -print0) + + if ! ((${#GETTEXT_TRANSLATIONS_LANGUAGES[@]})); then + log "ERROR" "No existing translations were found." + return 127 + fi +} + +_gettext_translations_run_binary() { + # $1 The binary to run + # $@ The arguments to pass to that binary + local GETTEXT_TRANSLATIONS_BINARY + + GETTEXT_TRANSLATIONS_BINARY="${1}" + shift + + log "DEBUG" " Container Image: '${GETTEXT_TRANSLATIONS_DOCKER_IMAGE}'" + log "DEBUG" " Container Binary: '${GETTEXT_TRANSLATIONS_BINARY}'" + log "DEBUG" " Container Arguments: '$*'" + + docker run \ + --rm \ + -t \ + -v "$(git rev-parse --show-toplevel):/mnt" \ + "${GETTEXT_TRANSLATIONS_DOCKER_IMAGE}" \ + "${GETTEXT_TRANSLATIONS_BINARY}" \ + "$@" +} + +_gettext_translations_write_base_pot_file() { + # $1: The source file + + if [[ -n "${GETTEXT_TRANSLATIONS_UTF8_OVERRIDE}" ]]; then + sed \ + "${GETTEXT_TRANSLATIONS_SED_PATTERN}" \ + "${1}" \ + > "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" + else + cat "${1}" \ + > "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" + fi + + # Revert changes that only modify the POT-Creation-Date field only. + if git diff --quiet --exit-code -I '(^"POT-Creation-Date:)' "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}"; then + log "WARNING" 'Reverting an empty update with changes to the "POT-Creation-Date" field ...' + git checkout "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" + fi +} + +_gettext_translations_usage() { + _gettext_translations_usage_title + _gettext_translations_usage_add + _gettext_translations_usage_compile + _gettext_translations_usage_missing + _gettext_translations_usage_update + _gettext_translations_usage_terminate +} + +_gettext_translations_usage_add() { + log "ERROR" "--------------------------------------------------------------------------------" + log "ERROR" "add < add a new language to the project." + log "ERROR" "translations.sh add" + log "ERROR" " -i [CONTAINER IMAGE WITH GETTEXT BINARIES]" + log "ERROR" " -p [BASE FILE PATH ('locales' folder or similar)]" +} + +_gettext_translations_usage_compile() { + log "ERROR" "--------------------------------------------------------------------------------" + log "ERROR" "compile < compile or recompile .mo files" + log "ERROR" "translations.sh compile" + log "ERROR" " -i [CONTAINER IMAGE WITH GETTEXT BINARIES]" + log "ERROR" " -p [BASE FILE PATH ('locales' folder or similar)]" +} + +_gettext_translations_usage_missing() { + log "ERROR" "--------------------------------------------------------------------------------" + log "ERROR" "missing < search for untranslated strings" + log "ERROR" "translations.sh missing" + log "ERROR" " -i [CONTAINER IMAGE WITH GETTEXT BINARIES]" + log "ERROR" " -p [BASE FILE PATH ('locales' folder or similar)]" + log "ERROR" " -m [EMPTY MESSAGE MATCH STRING (defaults to 'msgstr ""')]" + log "ERROR" " -s [LANGUAGES TO SKIP (use multiple times as needed)]" +} + +_gettext_translations_usage_terminate() { + exit 127 +} + +_gettext_translations_usage_title() { + log "ERROR" "translations.sh -- manage translation tasks with gnu gettext." +} + +_gettext_translations_usage_update() { + log "ERROR" "--------------------------------------------------------------------------------" + log "ERROR" "update < extract strings from the code base and update all files." + log "ERROR" "translations.sh update" + log "ERROR" " -i [CONTAINER IMAGE WITH GETTEXT BINARIES]" + log "ERROR" " -p [BASE FILE PATH ('locales' folder or similar)]" + log "ERROR" " -b [BASE FILE NAME (defaults to 'base')]" + log "ERROR" " -c [CODE BASE PATH (root folder of source code)]" + log "ERROR" " -e [CONTACT EMAIL (written to .po and .pot files)]" + log "ERROR" " -r [CODE BASE REGEX (defaults to '*.py')]" + log "ERROR" " -u (optionally set the CHARSET to UTF-8)" +} + +gettext_translations_add() { + local GETTEXT_TRANSLATIONS_LANGUAGE + local GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH + local GETTEXT_TRANSLATIONS_NEW_PO_FILE_NAME + local GETTEXT_TRANSLATIONS_POT_FILE + + # shellcheck source=/dev/null + source "$(dirname -- "${BASH_SOURCE[0]}")/../libraries/environment.sh" \ + -m "GETTEXT_TRANSLATIONS_DOCKER_IMAGE" + + # shellcheck disable=SC2128 + if [[ -z "${GETTEXT_TRANSLATIONS_LANGUAGES}" ]]; then + log "ERROR" "You must set the environment variable 'GETTEXT_TRANSLATIONS_LANGUAGES' to add new languages." + log "ERROR" "Please assign a space separated list of new language codes from:" + log "ERROR" " ${GETTEXT_TRANSLATIONS_EXAMPLE_LANGUAGES_CODES_URL}" + log "ERROR" "For example:" + log "ERROR" " export GETTEXT_TRANSLATIONS_LANGUAGES='en de fr ko'" + return 127 + fi + + if ! _gettext_translations_check_existing_base_path || + ! _gettext_translations_check_existing_pot_files; then + return 127 + fi + + while read -r -d ' ' GETTEXT_TRANSLATIONS_LANGUAGE; do + if [[ -z "${GETTEXT_TRANSLATIONS_LANGUAGE}" ]]; then + break + fi + + echo "" + + log "INFO" "Adding '${GETTEXT_TRANSLATIONS_LANGUAGE}' ..." + GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH="${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_LANGUAGE}/LC_MESSAGES" + + log "INFO" " Creating '${GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH}' ..." + if [[ -d "${GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH}" ]]; then + log "WARNING" "Found an existing path for this language!" + log "WARNING" "Skipping to avoid overwriting content." + continue + fi + mkdir -p "${GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH}" + + for GETTEXT_TRANSLATIONS_POT_FILE in "${GETTEXT_TRANSLATIONS_BASE_PATH}"/*.pot; do + GETTEXT_TRANSLATIONS_NEW_PO_FILE_NAME="${GETTEXT_TRANSLATIONS_NEW_LANGUAGE_PATH}/$(basename "${GETTEXT_TRANSLATIONS_POT_FILE}" ".pot").po" + + log "INFO" " Using '${GETTEXT_TRANSLATIONS_POT_FILE}' to create '${GETTEXT_TRANSLATIONS_NEW_PO_FILE_NAME}' ..." + cp -rp \ + "${GETTEXT_TRANSLATIONS_POT_FILE}" \ + "${GETTEXT_TRANSLATIONS_NEW_PO_FILE_NAME}" + _gettext_translations_run_binary msgmerge \ + -q \ + --force-po \ + -U "${GETTEXT_TRANSLATIONS_NEW_PO_FILE_NAME}" \ + "${GETTEXT_TRANSLATIONS_POT_FILE}" --lang="${GETTEXT_TRANSLATIONS_LANGUAGE}" + done + + done <<< "${GETTEXT_TRANSLATIONS_LANGUAGES}" + + log "INFO" "Done." +} + +gettext_translations_compile() { + local GETTEXT_TRANSLATIONS_LANGUAGE + local GETTEXT_TRANSLATIONS_LANGUAGES + local GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE + local GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH + local GETTEXT_TRANSLATIONS_NEW_MO_FILE_NAME + + GETTEXT_TRANSLATIONS_LANGUAGES=() + + # shellcheck source=/dev/null + source "$(dirname -- "${BASH_SOURCE[0]}")/../libraries/environment.sh" \ + -m "GETTEXT_TRANSLATIONS_DOCKER_IMAGE" + + if ! _gettext_translations_check_existing_base_path || + ! _gettext_translations_identify_existing_languages; then + return 127 + fi + + for GETTEXT_TRANSLATIONS_LANGUAGE in "${GETTEXT_TRANSLATIONS_LANGUAGES[@]}"; do + + log "INFO" "Processing '${GETTEXT_TRANSLATIONS_LANGUAGE}' ..." + + _gettext_translations_check_existing_po_files "${GETTEXT_TRANSLATIONS_LANGUAGE}" + + GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH="${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_LANGUAGE}/LC_MESSAGES" + + for GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE in "${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH}"/*.po; do + GETTEXT_TRANSLATIONS_NEW_MO_FILE_NAME="${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH}/$(basename "${GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE}" ".po").mo" + log "INFO" " Compiling '${GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE}' -> '${GETTEXT_TRANSLATIONS_NEW_MO_FILE_NAME}' ..." + _gettext_translations_run_binary msgfmt \ + -o "${GETTEXT_TRANSLATIONS_NEW_MO_FILE_NAME}" \ + "${GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE}" + done + + done + + log "INFO" "Done." +} + +gettext_translations_missing() { + local GETTEXT_TRANSLATIONS_LANGUAGE + local GETTEXT_TRANSLATIONS_LANGUAGES + local GETTEXT_TRANSLATIONS_LANGUAGE_TO_SKIP + local GETTEXT_TRANSLATIONS_MISSING=0 + + GETTEXT_TRANSLATIONS_LANGUAGES=() + + if ! _gettext_translations_check_existing_base_path || + ! _gettext_translations_identify_existing_languages; then + return 127 + fi + + for GETTEXT_TRANSLATIONS_LANGUAGE in "${GETTEXT_TRANSLATIONS_LANGUAGES[@]}"; do + + for GETTEXT_TRANSLATIONS_LANGUAGE_TO_SKIP in "${GETTEXT_TRANSLATIONS_LANGUAGES_BEING_SKIPPED[@]}"; do + if [[ "${GETTEXT_TRANSLATIONS_LANGUAGE_TO_SKIP}" == "${GETTEXT_TRANSLATIONS_LANGUAGE}" ]]; then + log "WARNING" "Skipping checks on '${GETTEXT_TRANSLATIONS_LANGUAGE}' ..." + break + fi + GETTEXT_TRANSLATIONS_LANGUAGE_TO_SKIP="" + done + + if [[ -n "${GETTEXT_TRANSLATIONS_LANGUAGE_TO_SKIP}" ]]; then + continue + fi + + log "INFO" "Checking '${GETTEXT_TRANSLATIONS_LANGUAGE}' for missing translations ..." + if [[ "$( + grep \ + -c \ + "${GETTEXT_TRANSLATIONS_EMPTY_MESSAGE_MATCH}" \ + "${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_LANGUAGE}/LC_MESSAGES/"*.po || + true + )" -gt "1" ]] \ + ; then + log "ERROR" "Found untranslated strings!" + GETTEXT_TRANSLATIONS_MISSING=127 + continue + fi + log "INFO" "No missing translations found." + done + return "${GETTEXT_TRANSLATIONS_MISSING}" +} + +gettext_translations_update() { + local GETTEXT_TRANSLATIONS_BASE_POT_FILE + local GETTEXT_TRANSLATIONS_LANGUAGE + local GETTEXT_TRANSLATIONS_LANGUAGES + local GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE + local GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH + + GETTEXT_TRANSLATIONS_LANGUAGES=() + + # shellcheck source=/dev/null + source "$(dirname -- "${BASH_SOURCE[0]}")/../libraries/environment.sh" \ + -m "GETTEXT_TRANSLATIONS_DOCKER_IMAGE" + + mkdir -p "${GETTEXT_TRANSLATIONS_BASE_PATH}" + + if ! _gettext_translations_identify_existing_languages; then + log "WARNING" "There are no target languages defined for this project yet." + fi + + GETTEXT_TRANSLATIONS_BASE_POT_FILE="${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME}.pot" + _gettext_translations_generate_or_update_pot_file + + for GETTEXT_TRANSLATIONS_LANGUAGE in "${GETTEXT_TRANSLATIONS_LANGUAGES[@]}"; do + + log "INFO" " Updating '${GETTEXT_TRANSLATIONS_LANGUAGE}' with changes to '${GETTEXT_TRANSLATIONS_BASE_POT_FILE}' ..." + + GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH="${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_LANGUAGE}/LC_MESSAGES" + + for GETTEXT_TRANSLATIONS_LANGUAGE_PO_FILE in "${GETTEXT_TRANSLATIONS_LANGUAGE_SUB_PATH}"/*.po; do + + _gettext_translations_run_binary msgmerge \ + -q \ + --force-po \ + --lang="${GETTEXT_TRANSLATIONS_LANGUAGE}" \ + -U "${GETTEXT_TRANSLATIONS_BASE_PATH}/${GETTEXT_TRANSLATIONS_LANGUAGE}/LC_MESSAGES/${GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME}.po" \ + "${GETTEXT_TRANSLATIONS_BASE_POT_FILE}" + + done + + done + + log "INFO" "Done." +} + +main() { + local GETTEXT_TRANSLATIONS_BASE_PATH + local GETTEXT_TRANSLATIONS_CODE_BASE_PATH + local GETTEXT_TRANSLATIONS_CODE_BASE_REGEX + local GETTEXT_TRANSLATIONS_EMAIL_ADDRESS + local GETTEXT_TRANSLATIONS_EMPTY_MESSAGE_MATCH + local GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME + local GETTEXT_TRANSLATIONS_LANGUAGES_BEING_SKIPPED + local GETTEXT_TRANSLATIONS_UTF8_OVERRIDE + + GETTEXT_TRANSLATIONS_CODE_BASE_REGEX="*.py" + GETTEXT_TRANSLATIONS_EMPTY_MESSAGE_MATCH='msgstr ""' + GETTEXT_TRANSLATIONS_EXTRACTION_FILE_NAME="base" + GETTEXT_TRANSLATIONS_LANGUAGES_BEING_SKIPPED=() + + _gettext_translations_args "$@" + +} + +main "$@" diff --git a/cookiecutter.json b/cookiecutter.json index eac9c752..b875cb4a 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -14,7 +14,7 @@ "_GITHUB_CI_DEFAULT_VERBOSE_NOTIFICATIONS": true, "_*DO_NOT_MODIFY_THIS_FILE*": "This file is created to assist with upgrading to future versions of this template.", "_copy_without_render": [ - ".cicd-tools/boxes/bootstrap/libraries/environment.sh", + ".cicd-tools/**/*", ".github/actions", "ansible_role/*", "scripts/*" diff --git a/markdown/OVERVIEW.md b/markdown/OVERVIEW.md index 8726a6be..6362bd2d 100644 --- a/markdown/OVERVIEW.md +++ b/markdown/OVERVIEW.md @@ -5,7 +5,7 @@ CICD-Tools provides four consumable resources that together form the basis for a complete CI solution: 1. Customized [pre-commit hooks](https://github.com/cicd-tools-org/pre-commit) for end-user projects. -2. A custom [Docker container](../.cicd-tools/container/Dockerfile) which supplies the required binary tools for the pre-commit hooks. +2. A custom [Docker container](../.cicd-tools/containers/utilities/Dockerfile) which supplies the required binary tools for the pre-commit hooks. 3. Remotely consumable [GitHub "Jobs"](../.github/workflows) that are actively maintained. 4. A custom [packaging system](https://github.com/cicd-tools-org/manifest/blob/main/manifest.json.asc) that securely delivers upgradable [Toolboxes](../cicd-tools/boxes) full of scripts for the workflows. @@ -25,7 +25,7 @@ However, this leads to a problem where the tools used to perform these quality c The [CICD-Tools container](https://ghcr.io/cicd-tools-org/cicd-tools) provides vetted binaries that are [integrated](https://github.com/cicd-tools-org/pre-commit/blob/main/.pre-commit-hooks.yaml) with the pre-commit hooks. -This allows a [single container definition](../.cicd-tools/container/Dockerfile) to [securely](../.cicd-tools/container/Dockerfile.sha256) provide most third party software. Where necessary, other trusted containers are leveraged to create a complete solution. Together these containers provide a way of leveraging third party tools without polluting your codebase with extra dependencies. +This allows a [single container definition](../.cicd-tools/containers/utilities/Dockerfile) to [securely](../.cicd-tools/containers/Dockerfile.sha256) provide most third party software. Where necessary, other trusted containers are leveraged to create a complete solution. Together these containers provide a way of leveraging third party tools without polluting your codebase with extra dependencies. Finally, the containers ensure the same tools are used to check the codebase locally, and in the CI. diff --git a/scripts/containers.sh b/scripts/containers.sh index e6a05cd2..1a400de2 100755 --- a/scripts/containers.sh +++ b/scripts/containers.sh @@ -12,8 +12,8 @@ source "$(dirname -- "${BASH_SOURCE[0]}")/../.cicd-tools/boxes/bootstrap/librari main() { log "INFO" "Building the CICD-Tools utility containers ..." - pushd .cicd-tools/container >> /dev/null - log "INFO" "Building AMD64 ..." + pushd .cicd-tools/containers/utilities >> /dev/null + log "INFO" " Building AMD64 ..." docker build \ --no-cache \ --platform linux/amd64 \ @@ -22,7 +22,7 @@ main() { --build-arg BUILD_ARG_ARCH_FORMAT_3=x86_64 \ --build-arg BUILD_ARG_ARCH_FORMAT_4=64-bit \ -t ghcr.io/cicd-tools-org/cicd-tools:linux-amd . - log "INFO" "Building ARM64 ..." + log "INFO" " Building ARM64 ..." docker build \ --no-cache \ --platform linux/arm64 \ @@ -33,6 +33,21 @@ main() { -t ghcr.io/cicd-tools-org/cicd-tools:linux-arm . popd >> /dev/null + log "INFO" "Building the CICD-Tools gettext containers ..." + + pushd .cicd-tools/containers/gettext >> /dev/null + log "INFO" " Building AMD64 ..." + docker build \ + --no-cache \ + --platform linux/amd64 \ + -t ghcr.io/cicd-tools-org/cicd-tools-gettext:linux-amd . + log "INFO" " Building ARM64 ..." + docker build \ + --no-cache \ + --platform linux/arm64 \ + -t ghcr.io/cicd-tools-org/cicd-tools-gettext:linux-arm . + popd >> /dev/null + log "INFO" "Containers successfully built." } diff --git a/{{cookiecutter.project_slug}}/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh b/{{cookiecutter.project_slug}}/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh new file mode 120000 index 00000000..6c3eab89 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.cicd-tools/boxes/bootstrap/pre-commit/gettext-translations.sh @@ -0,0 +1 @@ +../../../../../cicd-tools/boxes/0.1.0/pre-commit/gettext-translations.sh \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.github/workflows/workflow-push.yml b/{{cookiecutter.project_slug}}/.github/workflows/workflow-push.yml index af569c3e..a9470f24 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/workflow-push.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/workflow-push.yml @@ -202,6 +202,33 @@ jobs: {%- endraw %}{% endif %} {%- if cookiecutter.optional_workflow_linting == 'true' %}{% raw %} + translations_missing: + needs: + - configuration + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: cicd-tools-org/cicd-tools/.github/workflows/job-80-poetry-precommit_commit_stage_hook.yml@main + with: + CONCURRENCY: ${{ fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_concurrency_limit }} + PRECOMMIT_HOOK_ID: "gettext-translations-missing" + PRECOMMIT_HOOK_NAME: "Missing Translations" + PYTHON_VERSIONS: ${{ toJSON(fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_python_versions) }} + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_verbose_notifications }} + + translations_updates: + needs: + - configuration + secrets: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + uses: cicd-tools-org/cicd-tools/.github/workflows/job-80-poetry-precommit_commit_stage_hook.yml@main + with: + CONCURRENCY: ${{ fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_concurrency_limit }} + PRE_HOOK_COMMAND: "sed -i.bak 's,#: /Volumes/Code/Code/cicd-tools/cicd-tools/cicd-tool-box/,#/home/runner/cicd-tool-box/,g' python/locales/base.pot" + PRECOMMIT_HOOK_ID: "gettext-translations-update" + PRECOMMIT_HOOK_NAME: "Translation Updates" + PYTHON_VERSIONS: ${{ toJSON(fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_python_versions) }} + VERBOSE_NOTIFICATIONS: ${{ fromJSON(needs.configuration.outputs.JSON_FILE_DATA).ci_verbose_notifications }} + workflow_lint: needs: - configuration @@ -256,6 +283,8 @@ jobs: - shell_lint - spelling_vocabularies - start + - translations_missing + - translations_updates {%- endraw %}{% if cookiecutter.optional_toml_linting == 'true' %} - toml_lint {%- endif %}{% raw %} diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 59603a6d..3f334823 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: commitizen stages: [commit-msg] - repo: https://github.com/cicd-tools-org/pre-commit.git - rev: 0.5.0 + rev: 4c7c0aa1d5cf0c0b6bab56be36075a5dfaf706a1 hooks: - id: format-shell args: @@ -30,6 +30,29 @@ repos: {%- if cookiecutter.optional_toml_linting == 'true' %} - id: format-toml {%- endif %} + - id: gettext-translations-add + args: + - "-p" + - "python/locales" + - id: gettext-translations-compile + args: + - "-p" + - "python/locales" + - id: gettext-translations-missing + args: + - "-p" + - "python/locales" + - "-s" + - "en" + - id: gettext-translations-update + args: + - "-p" + - "python/locales" + - "-c" + - "python" + - "-e" + - "niall@niallbyrne.ca" + - "-u" - id: git-conflict-markers {%- if cookiecutter.optional_workflow_linting == 'true' %} - id: lint-github-workflow-header diff --git a/{{cookiecutter.project_slug}}/python/__init__.py b/{{cookiecutter.project_slug}}/python/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/{{cookiecutter.project_slug}}/python/locales/base.pot b/{{cookiecutter.project_slug}}/python/locales/base.pot new file mode 100644 index 00000000..5e07c8ea --- /dev/null +++ b/{{cookiecutter.project_slug}}/python/locales/base.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: niall@niallbyrne.ca\n" +"POT-Creation-Date: 2024-07-21 13:52-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: python/main.py:17 +msgid "Translation example string 1." +msgstr "" + +#: python/main.py:18 +msgid "Translation example string 2." +msgstr "" diff --git a/{{cookiecutter.project_slug}}/python/locales/en/LC_MESSAGES/base.po b/{{cookiecutter.project_slug}}/python/locales/en/LC_MESSAGES/base.po new file mode 100644 index 00000000..a3e8bf23 --- /dev/null +++ b/{{cookiecutter.project_slug}}/python/locales/en/LC_MESSAGES/base.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: niall@niallbyrne.ca\n" +"POT-Creation-Date: 2024-07-21 13:52-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: python/main.py:17 +msgid "Translation example string 1." +msgstr "" + +#: python/main.py:18 +msgid "Translation example string 2." +msgstr "" diff --git a/{{cookiecutter.project_slug}}/python/main.py b/{{cookiecutter.project_slug}}/python/main.py new file mode 100644 index 00000000..d1b97c4d --- /dev/null +++ b/{{cookiecutter.project_slug}}/python/main.py @@ -0,0 +1,22 @@ +"""Python example script.""" + +import gettext +import os + + +def main() -> None: + + translations = gettext.translation( + "base", + os.path.join(os.path.dirname(__file__), "locales"), + fallback=True, + ) + + _ = translations.gettext + + print(_("Translation example string 1.")) + print(_("Translation example string 2.")) + + +if __name__ == "__main__": + main()