v2.4.0 — internationalization (EN/RU) + custom git templates

- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
This commit is contained in:
anten-ka
2026-04-10 11:26:02 +03:00
parent 9c084f37ec
commit 0d087831d8
13 changed files with 2489 additions and 1002 deletions

View File

@@ -1,20 +1,29 @@
#!/bin/bash
# GoTelegram v2.2Каталог шаблонов сайтов
# Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout
# GoTelegram v2.4website templates catalog
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
# + custom git URL templates (user-supplied public repos)
CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json"
TEMPLATES_CACHE="/tmp/gotelegram_templates"
# ── Загрузка каталога ────────────────────────────────────────────────────────
# Custom git template limits
CUSTOM_GIT_MAX_SIZE_MB=100
CUSTOM_GIT_CLONE_TIMEOUT=90
# ── Catalog loading ────────────────────────────────────────────────────
load_catalog() {
if [ ! -f "$CATALOG_FILE" ]; then
log_error "Каталог шаблонов не найден: $CATALOG_FILE"
if type tf &>/dev/null; then
log_error "$(tf templates_catalog_not_found "$CATALOG_FILE")"
else
log_error "Templates catalog not found: $CATALOG_FILE"
fi
return 1
fi
return 0
}
# ── Категории ────────────────────────────────────────────────────────────────
# ── Categories ─────────────────────────────────────────────────────────
get_categories() {
jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null
}
@@ -24,13 +33,13 @@ get_category_name() {
jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null
}
# ── Шаблоны по категории ────────────────────────────────────────────────────
# ── Templates in a category ────────────────────────────────────────────
get_templates_by_category() {
local cat_id="$1"
jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null
}
# ── Информация о шаблоне ────────────────────────────────────────────────────
# ── Template info ──────────────────────────────────────────────────────
get_template_info() {
local tpl_id="$1"
jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null
@@ -42,16 +51,19 @@ get_template_field() {
jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null
}
# ── Интерактивный выбор категории ────────────────────────────────────────────
# ── Interactive category picker (returns category id or special __custom_git__/__random__) ──
select_category() {
load_catalog || return 1
echo "" >&2
echo -e " ${BOLD}${WHITE}📂 Категории шаблонов сайтов:${NC}" >&2
echo -e " ${BOLD}${WHITE}$(t templates_categories)${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
# First item: custom git URL template
printf " ${CYAN}%2d)${NC} ${GREEN}%s${NC}\n" 1 "$(t templates_custom_git)" >&2
local cats=()
local i=1
local i=2
while IFS='|' read -r id name icon count; do
[ "$count" -eq 0 ] && continue
local emoji
@@ -67,40 +79,52 @@ select_category() {
chart-bar) emoji="🔧" ;;
*) emoji="📄" ;;
esac
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}(%d шаблонов)${NC}\n" "$i" "$name" "$count" >&2
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}$(tf templates_count_fmt "$count")${NC}\n" "$i" "$name" >&2
cats+=("$id")
((i++))
done < <(get_categories)
printf " ${CYAN}%2d)${NC} 🎲 Случайный шаблон\n" "$i" >&2
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$(t templates_random)" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
echo -ne " ${WHITE}Выбор:${NC} " >&2
echo -ne " ${WHITE}$(t choose):${NC} " >&2
read -r choice
# Случайный
if [ "$choice" -eq "$i" ] 2>/dev/null; then
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
log_error "$(t invalid_choice)"
return 1
fi
# Custom git URL
if [ "$choice" -eq 1 ]; then
echo "__custom_git__"
return 0
fi
# Random
if [ "$choice" -eq "$i" ]; then
local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}"
echo "$random_cat"
return 0
fi
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
echo "${cats[$((choice-1))]}"
# Regular category (offset by 1 because item 1 is custom git)
if [ "$choice" -ge 2 ] && [ "$choice" -lt "$i" ]; then
echo "${cats[$((choice-2))]}"
return 0
fi
log_error "Неверный выбор"
log_error "$(t invalid_choice)"
return 1
}
# ── Интерактивный выбор шаблона ──────────────────────────────────────────────
# ── Interactive template picker ────────────────────────────────────────
select_template() {
local cat_id="$1"
local cat_name
cat_name=$(get_category_name "$cat_id")
echo "" >&2
echo -e " ${BOLD}${WHITE}📋 $cat_name — доступные шаблоны:${NC}" >&2
echo -e " ${BOLD}${WHITE}$(tf templates_list "$cat_name")${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
local tpls=()
@@ -112,29 +136,29 @@ select_template() {
done < <(get_templates_by_category "$cat_id")
if [ ${#tpls[@]} -eq 0 ]; then
log_info "В этой категории нет шаблонов"
log_info "$(t templates_cat_empty)"
return 1
fi
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo -ne " ${WHITE}Выбор (1-$((i-1))):${NC} " >&2
echo -ne " ${WHITE}$(t choose) (1-$((i-1))):${NC} " >&2
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
local selected_id="${tpls[$((choice-1))]}"
# Показываем превью
show_template_preview "$selected_id"
# Show preview
show_template_preview "$selected_id" || return 1
echo "$selected_id"
return 0
fi
log_error "Неверный выбор"
log_error "$(t invalid_choice)"
return 1
}
# ── Показ превью шаблона ────────────────────────────────────────────────────
# ── Template preview ───────────────────────────────────────────────────
show_template_preview() {
local tpl_id="$1"
local info
@@ -148,36 +172,36 @@ show_template_preview() {
description=$(echo "$info" | jq -r '.description // "—"')
echo "" >&2
echo -e " ${BOLD}${WHITE}🔍 Превью шаблона:${NC}" >&2
echo -e " ${BOLD}${WHITE}$(t templates_preview_title)${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
echo -e " ${WHITE}Название:${NC} $name" >&2
echo -e " ${WHITE}Источник:${NC} $source" >&2
echo -e " ${WHITE}Описание:${NC} $description" >&2
echo -e " ${WHITE}$(t templates_name)${NC} $name" >&2
echo -e " ${WHITE}$(t templates_source)${NC} $source" >&2
echo -e " ${WHITE}$(t templates_description)${NC} $description" >&2
if [ -n "$preview_url" ]; then
echo "" >&2
echo -e " ${GREEN}👁 Превью:${NC} ${CYAN}${preview_url}${NC}" >&2
echo -e " ${DIM}Откройте ссылку в браузере для просмотра шаблона${NC}" >&2
echo -e " ${GREEN}$(t templates_preview)${NC} ${CYAN}${preview_url}${NC}" >&2
echo -e " ${DIM}$(t templates_preview_hint)${NC}" >&2
fi
if [ -n "$repo_url" ]; then
echo -e " ${DIM}📦 Репо: ${repo_url}${NC}" >&2
echo -e " ${DIM}$(t templates_repo) ${repo_url}${NC}" >&2
fi
# Благодарность автору
# Thanks
echo "" >&2
echo -e " ${MAGENTA}💜 Спасибо авторам ${source} за открытый код!${NC}" >&2
echo -e " ${MAGENTA}$(tf templates_thanks "$source")${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
echo "" >&2
if ! confirm "Установить этот шаблон?"; then
if ! confirm "$(t templates_install_this)"; then
return 1
fi
return 0
}
# ── Скачивание шаблона ───────────────────────────────────────────────────────
# ── Template download (from catalog) ───────────────────────────────────
download_template() {
local tpl_id="$1"
local output_dir="${2:-$TEMPLATES_CACHE}"
@@ -194,9 +218,9 @@ download_template() {
rm -rf "$clone_dir"
mkdir -p "$clone_dir"
log_info "Скачивание шаблона \"$name\"..."
log_info "$(tf templates_downloading "$name")"
# Для HTML5 UP — отдельный репо с папками
# HTML5 UP — one repo with folders
if [ "$source" = "html5up" ]; then
local tmp_clone="/tmp/html5up_clone_$$"
rm -rf "$tmp_clone"
@@ -204,7 +228,7 @@ download_template() {
# Sparse checkout
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
if [ $? -ne 0 ]; then
# Fallback: полный clone
# Fallback: full clone
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
fi
@@ -217,7 +241,7 @@ download_template() {
fi
rm -rf "$tmp_clone"
# Для learning-zone — один большой репо
# learning-zone — one big repo
elif [ "$source" = "learning-zone" ]; then
local tmp_clone="/tmp/lz_clone_$$"
rm -rf "$tmp_clone"
@@ -236,14 +260,14 @@ download_template() {
fi
rm -rf "$tmp_clone"
# Для StartBootstrap — каждый шаблон в своём репо
# StartBootstrap — each template in its own repo
elif [ "$source" = "startbootstrap" ]; then
local sb_tmp="/tmp/sb_clone_$$"
rm -rf "$sb_tmp"
git clone --depth 1 "$repo_url" "$sb_tmp" 2>/dev/null
if [ -d "$sb_tmp" ]; then
rm -rf "$sb_tmp/.git"
# StartBootstrap хранит production-файлы в dist/
# StartBootstrap stores production files in dist/
if [ -f "$sb_tmp/dist/index.html" ]; then
cp -r "$sb_tmp/dist/"* "$clone_dir/"
elif [ -f "$sb_tmp/index.html" ]; then
@@ -260,7 +284,7 @@ download_template() {
fi
rm -rf "$sb_tmp"
# Для ThemeWagon / ColorlibHQ — каждый шаблон в отдельном репо
# ThemeWagon / ColorlibHQ — each template in its own repo
elif [ "$source" = "themewagon" ] || [ "$source" = "colorlib" ]; then
local tw_tmp="/tmp/tw_clone_$$"
rm -rf "$tw_tmp"
@@ -283,7 +307,7 @@ download_template() {
fi
rm -rf "$tw_tmp"
# Для dawidolko — один большой репо с папками (как learning-zone)
# dawidolko — one big repo with folders (similar to learning-zone)
elif [ "$source" = "dawidolko" ]; then
local tmp_clone="/tmp/dw_clone_$$"
rm -rf "$tmp_clone"
@@ -301,13 +325,13 @@ download_template() {
rm -rf "$tmp_clone"
fi
# Проверяем результат
# Check result
if [ -f "$clone_dir/index.html" ]; then
log_success "Шаблон \"$name\" скачан"
log_success "$(tf templates_downloaded "$name")"
echo "$clone_dir"
return 0
else
# fallback: ищем index.html в подпапках (нестандартная структура)
# fallback: find index.html in subfolders (non-standard structure)
local fallback_index
fallback_index=$(find "$clone_dir" -name "index.html" -type f 2>/dev/null | head -1)
if [ -n "$fallback_index" ]; then
@@ -315,33 +339,253 @@ download_template() {
fallback_dir=$(dirname "$fallback_index")
if [ "$fallback_dir" != "$clone_dir" ]; then
cp -r "$fallback_dir/"* "$clone_dir/"
log_success "Шаблон \"$name\" скачан (из подпапки)"
log_success "$(tf templates_downloaded_subfolder "$name")"
echo "$clone_dir"
return 0
fi
fi
log_error "Шаблон не содержит index.html"
log_dim "Путь: $clone_dir"
log_error "$(t templates_no_index)"
log_dim "$(tf templates_path "$clone_dir")"
ls -la "$clone_dir" 2>/dev/null >&2
return 1
fi
}
# ── Полный интерактивный процесс выбора ──────────────────────────────────────
# ── Custom git URL helpers ─────────────────────────────────────────────
# Validate a user-supplied git URL
# Accepts: https://host/path[.git][@branch]
# Rejects: ssh://, git://, file://, absolute file paths
_validate_custom_git_url() {
local url="$1"
# Must begin with https://
[[ "$url" =~ ^https:// ]] || return 1
# Reject shell metacharacters that could be exploited
[[ "$url" =~ [[:space:]\;\`\$\(\)\<\>\|\\\&] ]] && return 1
# Reasonable length limit
[ "${#url}" -gt 512 ] && return 1
return 0
}
# Parse URL → sets CUSTOM_GIT_CLEAN and CUSTOM_GIT_BRANCH globals
_parse_custom_git_url() {
local url="$1"
CUSTOM_GIT_CLEAN=""
CUSTOM_GIT_BRANCH=""
# Handle trailing @branch
if [[ "$url" =~ ^(https://[^@]+)@([A-Za-z0-9._/-]+)$ ]]; then
CUSTOM_GIT_CLEAN="${BASH_REMATCH[1]}"
CUSTOM_GIT_BRANCH="${BASH_REMATCH[2]}"
else
CUSTOM_GIT_CLEAN="$url"
fi
# Strip trailing slash
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN%/}"
# Append .git if missing (works better with git clone on some hosts)
if [[ ! "$CUSTOM_GIT_CLEAN" =~ \.git$ ]]; then
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN}.git"
fi
}
# Check repo size (in MB) by inspecting cloned directory
_clone_dir_size_mb() {
local dir="$1"
du -sm "$dir" 2>/dev/null | awk '{print $1}'
}
# ── Show detailed help for custom git template ─────────────────────────
show_custom_git_help() {
local line
line=$(printf '─%.0s' $(seq 1 60))
echo "" >&2
echo -e " ${BOLD}${GREEN}$(t custom_git_title)${NC}" >&2
echo -e " ${DIM}${line}${NC}" >&2
echo -e " $(t custom_git_help_1)" >&2
echo -e " $(t custom_git_help_2)" >&2
echo -e " $(t custom_git_help_3)" >&2
echo "" >&2
echo -e " ${BOLD}${WHITE}$(t custom_git_formats)${NC}" >&2
echo -e " ${CYAN}$(t custom_git_fmt_github)${NC}" >&2
echo -e " ${CYAN}$(t custom_git_fmt_gitlab)${NC}" >&2
echo -e " ${CYAN}$(t custom_git_fmt_gitext)${NC}" >&2
echo -e " ${CYAN}$(t custom_git_fmt_branch)${NC}" >&2
echo "" >&2
echo -e " ${BOLD}${WHITE}$(t custom_git_auto_detect)${NC}" >&2
echo -e " $(t custom_git_auto_1)" >&2
echo -e " $(t custom_git_auto_2)" >&2
echo -e " $(t custom_git_auto_3)" >&2
echo -e " $(t custom_git_auto_4)" >&2
echo "" >&2
echo -e " ${BOLD}${WHITE}$(t custom_git_requirements)${NC}" >&2
echo -e " ${YELLOW}$(t custom_git_req_1)${NC}" >&2
echo -e " ${YELLOW}$(t custom_git_req_2)${NC}" >&2
echo -e " ${YELLOW}$(t custom_git_req_3)${NC}" >&2
echo -e " ${YELLOW}$(t custom_git_req_4)${NC}" >&2
echo "" >&2
echo -e " ${BOLD}${WHITE}$(t custom_git_examples)${NC}" >&2
echo -e " ${DIM}$(t custom_git_ex_1)${NC}" >&2
echo -e " ${DIM}$(t custom_git_ex_2)${NC}" >&2
echo -e " ${DIM}${line}${NC}" >&2
echo "" >&2
}
# ── Download a custom git template ─────────────────────────────────────
# Prompts user for a URL (unless passed), clones, detects index.html,
# copies result into $output_dir/custom_<hash>, echoes the final path.
download_custom_git_template() {
local url="${1:-}"
local output_dir="${2:-$TEMPLATES_CACHE}"
show_custom_git_help
if [ -z "$url" ]; then
echo -ne " ${WHITE}$(t custom_git_enter_url)${NC} " >&2
read -r url
url=$(echo "$url" | tr -d '\r\n[:space:]')
fi
if [ -z "$url" ]; then
log_error "$(t custom_git_empty)"
return 1
fi
if ! _validate_custom_git_url "$url"; then
log_error "$(t custom_git_bad_url)"
return 1
fi
_parse_custom_git_url "$url"
local clean_url="$CUSTOM_GIT_CLEAN"
local branch="$CUSTOM_GIT_BRANCH"
# Stable-ish directory name from a hash of the original URL
local hash
hash=$(echo -n "$url" | md5sum 2>/dev/null | awk '{print $1}' | head -c 10)
[ -z "$hash" ] && hash=$(date +%s)
local tpl_id="custom_${hash}"
local clone_dir="$output_dir/${tpl_id}"
local tmp_clone="/tmp/custom_git_clone_$$"
rm -rf "$clone_dir" "$tmp_clone"
mkdir -p "$clone_dir"
log_info "$(t custom_git_cloning)"
# Clone with timeout so a hung server can't freeze the installer
local clone_status=0
local git_args=("clone" "--depth" "1")
[ -n "$branch" ] && git_args+=("--branch" "$branch")
git_args+=("$clean_url" "$tmp_clone")
if command -v timeout &>/dev/null; then
timeout "$CUSTOM_GIT_CLONE_TIMEOUT" git "${git_args[@]}" 2>/tmp/custom_git_err_$$
clone_status=$?
else
git "${git_args[@]}" 2>/tmp/custom_git_err_$$
clone_status=$?
fi
if [ $clone_status -ne 0 ] || [ ! -d "$tmp_clone" ]; then
local err_msg
err_msg=$(head -3 "/tmp/custom_git_err_$$" 2>/dev/null | tr '\n' ' ')
rm -f "/tmp/custom_git_err_$$"
rm -rf "$tmp_clone" "$clone_dir"
log_error "$(tf custom_git_clone_failed "${err_msg:-$clone_status}")"
return 1
fi
rm -f "/tmp/custom_git_err_$$"
# Drop .git before measuring size (we only care about payload)
rm -rf "$tmp_clone/.git"
# Size guard
local size_mb
size_mb=$(_clone_dir_size_mb "$tmp_clone")
if [ -n "$size_mb" ] && [ "$size_mb" -gt "$CUSTOM_GIT_MAX_SIZE_MB" ]; then
rm -rf "$tmp_clone" "$clone_dir"
log_error "$(tf custom_git_too_big "${size_mb}MB")"
return 1
fi
log_info "$(t custom_git_scanning)"
# Priority list of common static-site output folders
local candidates=("" "dist" "public" "build" "_site" "site" "docs" "out" "www")
local found_dir=""
for sub in "${candidates[@]}"; do
local try_dir="$tmp_clone"
[ -n "$sub" ] && try_dir="$tmp_clone/$sub"
if [ -f "$try_dir/index.html" ]; then
found_dir="$try_dir"
break
fi
done
# Fallback: search for any index.html in the repo (shallow depth first)
if [ -z "$found_dir" ]; then
local fallback_index
fallback_index=$(find "$tmp_clone" -maxdepth 4 -name "index.html" -type f 2>/dev/null | head -1)
if [ -n "$fallback_index" ]; then
found_dir=$(dirname "$fallback_index")
fi
fi
if [ -z "$found_dir" ] || [ ! -f "$found_dir/index.html" ]; then
rm -rf "$tmp_clone" "$clone_dir"
log_error "$(t custom_git_no_index)"
return 1
fi
# Show what we found (human-friendly relative path)
local rel_path="${found_dir#$tmp_clone}"
rel_path="${rel_path#/}"
[ -z "$rel_path" ] && rel_path="(root)"
log_dim "$(tf custom_git_found_at "$rel_path")"
# Copy the detected directory as the new template
cp -r "$found_dir"/* "$clone_dir/" 2>/dev/null
cp -r "$found_dir"/.[!.]* "$clone_dir/" 2>/dev/null
rm -rf "$tmp_clone"
if [ ! -f "$clone_dir/index.html" ]; then
rm -rf "$clone_dir"
log_error "$(t custom_git_no_index)"
return 1
fi
# Remember the URL so users can see what template they used
echo "$url" > "$clone_dir/.custom_git_source" 2>/dev/null
log_success "$(tf custom_git_installed "$url")"
echo "$clone_dir"
return 0
}
# ── Full interactive template selection ───────────────────────────────
interactive_template_selection() {
load_catalog || return 1
# Выбор категории
# Category selection
local cat_id
cat_id=$(select_category)
[ $? -ne 0 ] && return 1
# Выбор шаблона
# Custom git URL path
if [ "$cat_id" = "__custom_git__" ]; then
local template_dir
template_dir=$(download_custom_git_template)
[ $? -ne 0 ] && return 1
echo "$template_dir"
return 0
fi
# Template selection
local tpl_id
tpl_id=$(select_template "$cat_id")
[ $? -ne 0 ] && return 1
# Скачивание
# Download
local template_dir
template_dir=$(download_template "$tpl_id")
[ $? -ne 0 ] && return 1