diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 89c3d47..73b54f1 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -182,18 +182,13 @@ jobs: find . -path './vendor' -prune -o -type f -name '*.php' -print0 | xargs -0 -r -n1 php -l - name: Verify translation template is up to date + if: github.event_name == 'pull_request' + env: + BASE_REF: ${{ github.event.pull_request.base.sha }} run: | cd ${{ github.workspace }}/cacti/plugins/intropage - sudo chmod +x locales/build_gettext.sh - sudo ./locales/build_gettext.sh - grep -v '^"POT-Creation-Date:' locales/po/cacti.pot > /tmp/cacti.pot.new - git checkout -- locales/ - grep -v '^"POT-Creation-Date:' locales/po/cacti.pot > /tmp/cacti.pot.old - if ! cmp -s /tmp/cacti.pot.old /tmp/cacti.pot.new; then - echo "locales/po/cacti.pot is out of date. Run locales/build_gettext.sh and commit the updated locales/po/cacti.pot." - diff -u /tmp/cacti.pot.old /tmp/cacti.pot.new || true - exit 1 - fi + git config --global --add safe.directory ${{ github.workspace }}/cacti/plugins/intropage + php tests/bin/check-i18n-pot.php "$BASE_REF" - name: Set expected Cacti version for unit tests run: echo -n "${{ env.CACTI }}" | sudo tee ${{ github.workspace }}/cacti/plugins/intropage/tests/.cacti-version > /dev/null diff --git a/tests/bin/check-i18n-pot.php b/tests/bin/check-i18n-pot.php new file mode 100644 index 0000000..183db00 --- /dev/null +++ b/tests/bin/check-i18n-pot.php @@ -0,0 +1,263 @@ + + * + * Exits 1 when a required pot update is missing, 2 on bad input, 0 otherwise. + */ + +/* + * The gettext keywords build_gettext.sh passes to xgettext. A changed line is + * only interesting when it contains one of these calls. The negative lookbehind + * keeps PHP magic methods such as __construct()/__toString() out of the match. + */ +$i18n_pattern = '/(?, removed: array, files: array} + */ +function collect_i18n_changes($range, $i18n_pattern) { + $command = 'git diff --no-ext-diff --unified=0 --no-color ' . $range . ' -- "*.php"'; + $output = []; + $status = 0; + + exec($command, $output, $status); + + if ($status !== 0) { + fwrite(STDERR, "git diff failed\n"); + + exit(2); + } + + $added = []; + $removed = []; + $files = []; + $old_file = null; + $new_file = null; + + foreach ($output as $line) { + if (strncmp($line, '--- ', 4) === 0) { + $old_file = diff_path(substr($line, 4)); + + continue; + } + + if (strncmp($line, '+++ ', 4) === 0) { + $new_file = diff_path(substr($line, 4)); + + continue; + } + + if ($line === '' || $line[0] !== '+' && $line[0] !== '-') { + continue; + } + + if (strncmp($line, '+++', 3) === 0 || strncmp($line, '---', 3) === 0) { + continue; + } + + $added_line = ($line[0] === '+'); + $file = $added_line ? $new_file : $old_file; + + if ($file === null || !feeds_template($file)) { + continue; + } + + $content = substr($line, 1); + + if (!preg_match($i18n_pattern, $content)) { + continue; + } + + $files[$file] = true; + + if ($added_line) { + $added[] = normalise_line($content); + } else { + $removed[] = normalise_line($content); + } + } + + return ['added' => $added, 'removed' => $removed, 'files' => $files]; +} + +/** + * Turn a diff header path ("a/foo.php", "b/foo.php" or "/dev/null") into a plain + * repository-relative path, or null when the side does not exist. + * + * @param string $raw Path portion following the "--- "/"+++ " marker. + * + * @return string|null Repository-relative path, or null for /dev/null. + */ +function diff_path($raw) { + $raw = trim($raw); + + if ($raw === '/dev/null') { + return null; + } + + if (strncmp($raw, 'a/', 2) === 0 || strncmp($raw, 'b/', 2) === 0) { + $raw = substr($raw, 2); + } + + return $raw; +} + +/** + * Whether a path is scanned by build_gettext.sh, i.e. a PHP file no more than + * one directory below the plugin root. + * + * @param string $file Repository-relative path. + * + * @return bool + */ +function feeds_template($file) { + if (substr($file, -4) !== '.php') { + return false; + } + + return substr_count($file, '/') <= 1; +} + +/** + * Whether the pull request already touches the gettext template. + * + * @param string $range Git diff range expression. + * @param string $pot_path Repository-relative path of cacti.pot. + * + * @return bool + */ +function pot_updated($range, $pot_path) { + $command = 'git diff --no-ext-diff --name-only --no-color ' . $range; + $output = []; + $status = 0; + + exec($command, $output, $status); + + if ($status !== 0) { + fwrite(STDERR, "git diff failed\n"); + + exit(2); + } + + foreach ($output as $name) { + if (trim($name) === $pot_path) { + return true; + } + } + + return false; +} + +$changes = collect_i18n_changes($range, $i18n_pattern); + +sort($changes['added']); +sort($changes['removed']); + +$i18n_changed = ($changes['added'] !== $changes['removed']); + +if (!$i18n_changed) { + fwrite(STDOUT, "No translatable-string changes detected in the diff; locales/po/cacti.pot update not required.\n"); + + exit(0); +} + +if (pot_updated($range, $pot_path)) { + fwrite(STDOUT, "Translatable strings changed and locales/po/cacti.pot is included in the pull request. OK.\n"); + + exit(0); +} + +$files = array_keys($changes['files']); +sort($files); + +fwrite(STDERR, "This pull request changes i18n strings but does not update locales/po/cacti.pot.\n"); +fwrite(STDERR, "Run locales/build_gettext.sh and commit the regenerated locales/po/cacti.pot.\n"); +fwrite(STDERR, "\n"); +fwrite(STDERR, "Files with changed i18n calls:\n"); + +foreach ($files as $file) { + fwrite(STDERR, " - $file\n"); +} + +exit(1);