ci(chore): pull/push scripts
This commit is contained in:
Executable
+111
@@ -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)."
|
||||
Executable
+164
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user