ci(chore): pull/push scripts

This commit is contained in:
Lemmy
2026-07-18 01:27:43 -04:00
parent a3287ecc1f
commit 3e87f3a086
3 changed files with 284 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env -S bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
API_BASE="${I18N_API_BASE:-https://i18n.noctalia.dev}"
PROJECT_SLUG="community-plugins"
if [[ $# -ne 0 ]]; then
echo "Usage: $0" >&2
exit 1
fi
for command in curl jq; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Error: $command is required" >&2
exit 1
fi
done
echo "Project: $PROJECT_SLUG"
echo "Output repository: $REPO_ROOT"
read -r -p "Pull translations and overwrite returned local files? [y/N] " reply
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
RESPONSE_FILE="$(mktemp)"
STAGING_DIR="$(mktemp -d)"
trap 'rm -f "$RESPONSE_FILE"; rm -rf "$STAGING_DIR"' EXIT
HTTP_CODE="$(curl --silent --show-error \
--output "$RESPONSE_FILE" \
--write-out '%{http_code}' \
"$API_BASE/api/projects/$PROJECT_SLUG/pull")"
if [[ "$HTTP_CODE" != "200" ]]; then
echo "Error: HTTP $HTTP_CODE" >&2
jq . "$RESPONSE_FILE" 2>/dev/null || cat "$RESPONSE_FILE"
exit 1
fi
if ! jq -e 'type == "object" and length > 0' "$RESPONSE_FILE" >/dev/null; then
echo "Error: API response must be a non-empty locale object" >&2
exit 1
fi
mapfile -t LOCALES < <(jq -r 'keys[]' "$RESPONSE_FILE")
FILE_COUNT=0
for locale in "${LOCALES[@]}"; do
if [[ ! "$locale" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
echo "Error: API returned an invalid locale: $locale" >&2
exit 1
fi
if ! jq -e --arg locale "$locale" '.[$locale] | type == "object" and length > 0' "$RESPONSE_FILE" >/dev/null; then
echo "Error: Locale payload must be a non-empty object: $locale" >&2
exit 1
fi
mapfile -t PLUGINS < <(jq -r --arg locale "$locale" '.[$locale] | keys[]' "$RESPONSE_FILE")
for plugin in "${PLUGINS[@]}"; do
if [[ ! "$plugin" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then
echo "Error: API returned an invalid plugin name: $plugin" >&2
exit 1
fi
if [[ ! -f "$REPO_ROOT/$plugin/plugin.toml" ]]; then
echo "Error: API returned unknown plugin: $plugin" >&2
exit 1
fi
if ! jq -e --arg locale "$locale" --arg plugin "$plugin" '
def valid_translation:
if type == "object" then all(.[]; valid_translation)
else type == "string"
end;
.[$locale][$plugin] | type == "object" and valid_translation
' "$RESPONSE_FILE" >/dev/null; then
echo "Error: Invalid translation payload for $plugin/$locale" >&2
exit 1
fi
mkdir -p "$STAGING_DIR/$plugin/translations"
jq --arg locale "$locale" --arg plugin "$plugin" \
'.[$locale][$plugin]' "$RESPONSE_FILE" \
>"$STAGING_DIR/$plugin/translations/$locale.json"
FILE_COUNT=$((FILE_COUNT + 1))
done
done
shopt -s nullglob
MANIFESTS=("$REPO_ROOT"/*/plugin.toml)
for manifest in "${MANIFESTS[@]}"; do
plugin="$(basename -- "$(dirname -- "$manifest")")"
if ! jq -e --arg plugin "$plugin" '.en[$plugin] | type == "object"' "$RESPONSE_FILE" >/dev/null; then
echo "Error: English payload is missing plugin: $plugin" >&2
exit 1
fi
done
while IFS= read -r -d '' staged_file; do
relative_path="${staged_file#"$STAGING_DIR/"}"
output_file="$REPO_ROOT/$relative_path"
output_dir="$(dirname -- "$output_file")"
mkdir -p "$output_dir"
temporary_file="$(mktemp "$output_dir/.i18n-pull.XXXXXX")"
cp -- "$staged_file" "$temporary_file"
mv -- "$temporary_file" "$output_file"
echo "Saved: $relative_path"
done < <(find "$STAGING_DIR" -type f -name '*.json' -print0 | sort -z)
echo "Successfully pulled $FILE_COUNT translation file(s)."
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env -S bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
API_BASE="${I18N_API_BASE:-https://i18n.noctalia.dev}"
PROJECT_SLUG="community-plugins"
OVERWRITE=false
SINGLE_LANG=""
usage() {
echo "Usage: COMMUNITY_PLUGINS_PUSH_SECRET=... $0 [--overwrite] [--lang <locale>]"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--overwrite)
OVERWRITE=true
shift
;;
--lang)
if [[ $# -lt 2 ]]; then
echo "Error: --lang requires a locale" >&2
usage >&2
exit 1
fi
SINGLE_LANG="$2"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "Error: Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
for command in curl jq; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Error: $command is required" >&2
exit 1
fi
done
if [[ -z "${COMMUNITY_PLUGINS_PUSH_SECRET:-}" ]]; then
echo "Error: COMMUNITY_PLUGINS_PUSH_SECRET is required" >&2
exit 1
fi
if [[ -n "$SINGLE_LANG" && ! "$SINGLE_LANG" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
echo "Error: Invalid locale: $SINGLE_LANG" >&2
exit 1
fi
shopt -s nullglob
MANIFESTS=("$REPO_ROOT"/*/plugin.toml)
if [[ ${#MANIFESTS[@]} -eq 0 ]]; then
echo "Error: No plugin manifests found in $REPO_ROOT" >&2
exit 1
fi
validate_translation_file() {
local file="$1"
if ! jq -e '
def valid_translation:
if type == "object" then all(.[]; valid_translation)
else type == "string"
end;
type == "object" and valid_translation
' "$file" >/dev/null; then
echo "Error: Translation must be a JSON object containing only objects and strings: $file" >&2
exit 1
fi
}
COMBINED_JSON='{}'
FILE_COUNT=0
for manifest in "${MANIFESTS[@]}"; do
plugin_dir="$(dirname -- "$manifest")"
plugin="$(basename -- "$plugin_dir")"
if [[ ! "$plugin" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then
echo "Error: Invalid plugin directory name: $plugin" >&2
exit 1
fi
if [[ -n "$SINGLE_LANG" ]]; then
translation_files=("$plugin_dir/translations/$SINGLE_LANG.json")
else
english_file="$plugin_dir/translations/en.json"
if [[ ! -f "$english_file" ]]; then
echo "Error: Missing English translation: $english_file" >&2
exit 1
fi
translation_files=("$plugin_dir"/translations/*.json)
fi
for file in "${translation_files[@]}"; do
[[ -f "$file" ]] || continue
locale="$(basename -- "$file" .json)"
if [[ ! "$locale" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
echo "Error: Invalid locale filename: $file" >&2
exit 1
fi
validate_translation_file "$file"
COMBINED_JSON="$(jq \
--arg locale "$locale" \
--arg plugin "$plugin" \
--slurpfile content "$file" \
'. + {($locale): ((.[$locale] // {}) + {($plugin): $content[0]})}' \
<<<"$COMBINED_JSON")"
echo "Loaded: $plugin/translations/$locale.json"
FILE_COUNT=$((FILE_COUNT + 1))
done
done
if [[ $FILE_COUNT -eq 0 ]]; then
if [[ -n "$SINGLE_LANG" ]]; then
echo "Error: No plugin provides locale: $SINGLE_LANG" >&2
else
echo "Error: No translation files found" >&2
fi
exit 1
fi
LOCALE_COUNT="$(jq 'keys | length' <<<"$COMBINED_JSON")"
echo "Project: $PROJECT_SLUG"
echo "Found $FILE_COUNT file(s) across $LOCALE_COUNT locale(s)"
read -r -p "Push these translations to $API_BASE? [y/N] " reply
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
PUSH_URL="$API_BASE/api/projects/$PROJECT_SLUG/push"
if [[ "$OVERWRITE" == true ]]; then
PUSH_URL="$PUSH_URL?overwrite=true"
echo "Overwrite mode enabled"
fi
RESPONSE_FILE="$(mktemp)"
trap 'rm -f "$RESPONSE_FILE"' EXIT
HTTP_CODE="$(curl --silent --show-error \
--output "$RESPONSE_FILE" \
--write-out '%{http_code}' \
--request POST \
--header "Authorization: Bearer $COMMUNITY_PLUGINS_PUSH_SECRET" \
--header "Content-Type: application/json" \
--data-binary @- \
"$PUSH_URL" <<<"$COMBINED_JSON")"
if [[ "$HTTP_CODE" != "200" ]]; then
echo "Error: HTTP $HTTP_CODE" >&2
jq . "$RESPONSE_FILE" 2>/dev/null || cat "$RESPONSE_FILE"
exit 1
fi
echo "Translations pushed successfully."
jq . "$RESPONSE_FILE" 2>/dev/null || cat "$RESPONSE_FILE"
+9
View File
@@ -126,6 +126,15 @@ error messages show the exact missing value, while maintainers review the useful
Write `translations/en.json` only. Every `label_key` and `description_key` in your manifest must resolve to a key in
it, and CI checks this. Do not add machine-translated locales; other languages are handled separately.
To test the latest translated locales from [Noctalia Translate](https://i18n.noctalia.dev) in a working checkout, run:
```sh
./.tools/i18n-pull.sh
```
The command asks for confirmation and overwrites the locale files returned by the translation service. It does not
delete local locale files that are absent from the export. Review the resulting diff before committing anything.
### Tags
The `tags` in `plugin.toml` are used for catalog search. Tags must be lowercase and selected from this list: