feat(install): i18n модуль + welcome banner

Структура локализации:
  install/i18n/en.sh    — английский словарь (дефолт, fallback)
  install/i18n/ru.sh    — русский словарь
  install/lib-i18n.sh   — лоадер + welcome banner

Поток:
  1. install.sh source'ит lib-i18n.sh и зовёт i18n_load_default →
     все строки на английском.
  2. Если onboarding нужен — печатается welcome banner ASCII-рамка
     на английском (язык ещё не выбран).
  3. onboarding_pick_language — единственный двуязычный шаг
     ("Choose language / Выберите язык"). По выбору вызывает
     i18n_load_lang ru|en — перегружает словарь.
  4. Все последующие шаги (transport / provider / model / auth /
     completion) идут на выбранном языке.

Fallback: если ru-словарь не имеет ключа — используется английское
значение (load_default вызывается до загрузки ru.sh, переменные
перезаписываются поверх).

lib-onboarding.sh переведён со смешанных hardcoded строк на
${STR_*} placeholders.

Тесты: bash -n всех 5 файлов чисто, i18n loader unit-тест показывает
EN/RU перегрузку, non-TTY smoke install --no-execute проходит.
This commit is contained in:
Parfii-bot 2026-05-17 15:35:10 +08:00
parent c844524f68
commit ab260f429e
5 changed files with 156 additions and 30 deletions

View file

@ -41,6 +41,10 @@ source "$LIB_DIR/lib-profile.sh"
source "$LIB_DIR/lib-args.sh" source "$LIB_DIR/lib-args.sh"
# shellcheck source=install/lib-menu.sh # shellcheck source=install/lib-menu.sh
source "$LIB_DIR/lib-menu.sh" source "$LIB_DIR/lib-menu.sh"
# shellcheck source=install/lib-i18n.sh
source "$LIB_DIR/lib-i18n.sh"
# Загружаем английский словарь по умолчанию — welcome banner идёт до выбора языка.
i18n_load_default
# shellcheck source=install/lib-onboarding.sh # shellcheck source=install/lib-onboarding.sh
source "$LIB_DIR/lib-onboarding.sh" source "$LIB_DIR/lib-onboarding.sh"
# shellcheck source=install/lib-plan.sh # shellcheck source=install/lib-plan.sh
@ -142,9 +146,13 @@ case "$PROFILE" in
esac esac
say "profile: $PROFILE" say "profile: $PROFILE"
# --- onboarding wizard (язык / транспорт / провайдер / модель / ключи) --- # --- welcome banner + onboarding wizard ----------------------------------
# Запускается только в TTY и при отсутствии ~/.claude/.onboarded. # Banner всегда EN — пользователь ещё не выбрал язык.
# Wizard: TTY + нет ~/.claude/.onboarded + не задан KEISEI_SKIP_ONBOARD.
# Skip: KEISEI_SKIP_ONBOARD=1 ./install.sh # Skip: KEISEI_SKIP_ONBOARD=1 ./install.sh
if onboarding_should_run; then
i18n_print_welcome
fi
onboarding_run onboarding_run
# --- prerequisites ------------------------------------------------------- # --- prerequisites -------------------------------------------------------

33
install/i18n/en.sh Normal file
View file

@ -0,0 +1,33 @@
# shellcheck shell=bash
# i18n/en.sh — English strings. Default before user picks language.
# Welcome banner (always EN, shown before language picker).
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Onboarding wizard steps
STR_ONBOARDING_INTRO="Onboarding wizard (5 steps)"
STR_PICK_LANGUAGE="Choose interface language:"
STR_PICK_TRANSPORT="Choose connection transport:"
STR_PICK_PROVIDER="Choose provider within"
STR_PICK_MODEL="Default model:"
# Transport descriptions
STR_TR_DIRECT_API="Direct provider API (key)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/role)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+key)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth subscription (ChatGPT)"
# Auth collection
STR_AUTH_INTRO="Auth for"
STR_AUTH_PROMPT="Enter values (Enter — leave empty, fill later)."
STR_AUTH_CURRENT_HINT="(current: <hidden>)"
# Completion
STR_DONE_TITLE="Onboarding complete"
STR_DONE_CONFIG="config:"
STR_DONE_SECRETS="secrets:"
STR_DONE_NEXT="Next: run ./install.sh or restart this script to apply profile"

33
install/i18n/ru.sh Normal file
View file

@ -0,0 +1,33 @@
# shellcheck shell=bash
# i18n/ru.sh — русские строки. Source'ится после выбора языка.
# Welcome-баннер всегда EN — на момент его показа выбор ещё не сделан.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Шаги мастера
STR_ONBOARDING_INTRO="Мастер первичной настройки (5 шагов)"
STR_PICK_LANGUAGE="Выберите язык интерфейса:"
STR_PICK_TRANSPORT="Выберите способ подключения:"
STR_PICK_PROVIDER="Выберите провайдера в группе"
STR_PICK_MODEL="Модель по умолчанию:"
# Описание транспортов
STR_TR_DIRECT_API="Прямой API провайдера (ключ)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/role)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+ключ)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Прокси (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth-подписка (ChatGPT)"
# Сбор ключей
STR_AUTH_INTRO="Аутентификация для"
STR_AUTH_PROMPT="Введите значения (Enter — оставить пустым, заполните позже)."
STR_AUTH_CURRENT_HINT="(текущее: <скрыто>)"
# Завершение
STR_DONE_TITLE="Первичная настройка завершена"
STR_DONE_CONFIG="конфиг:"
STR_DONE_SECRETS="секреты:"
STR_DONE_NEXT="Дальше: запустите ./install.sh или перезапустите этот скрипт для установки профиля"

47
install/lib-i18n.sh Normal file
View file

@ -0,0 +1,47 @@
# shellcheck shell=bash
# lib-i18n.sh — лоадер локализаций.
#
# Контракт:
# 1. На старте всегда source install/i18n/en.sh — экран приветствия
# показывается ДО выбора языка пользователем.
# 2. После onboarding_pick_language вызывается i18n_load_lang "$lang" —
# перегружает строки выбранного словаря.
# 3. Любая строка отсутствующая в словаре — fallback на en.sh уже в
# памяти (мы не unset'им переменные, ru перезаписывает поверх).
#
# Используется install.sh и install/lib-onboarding.sh.
# Корень i18n относительно LIB_DIR.
I18N_DIR="${LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/i18n"
i18n_load_default() {
# shellcheck source=install/i18n/en.sh
source "$I18N_DIR/en.sh"
}
i18n_load_lang() {
local lang="$1"
case "$lang" in
en)
i18n_load_default
;;
ru)
i18n_load_default # base (fallback values)
# shellcheck source=install/i18n/ru.sh
[ -f "$I18N_DIR/ru.sh" ] && source "$I18N_DIR/ru.sh"
;;
*)
i18n_load_default
;;
esac
}
# Welcome banner. Всегда EN. Запускается из install.sh до мастера.
i18n_print_welcome() {
echo ""
echo " ╔═══════════════════════════════════════════════════════╗"
echo "${STR_WELCOME_TITLE}"
echo "${STR_WELCOME_TAGLINE}"
echo " ╚═══════════════════════════════════════════════════════╝"
echo ""
}

View file

@ -93,23 +93,28 @@ onboarding_models_for_provider() {
# UI: язык # UI: язык
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
onboarding_pick_language() { onboarding_pick_language() {
# На этом шаге язык ещё не выбран — экран на двух языках одновременно.
if command -v whiptail >/dev/null 2>&1; then if command -v whiptail >/dev/null 2>&1; then
ONBOARDING_LANG=$(whiptail --title "KeiSei · Language / Язык" --radiolist \ ONBOARDING_LANG=$(whiptail --title "KeiSei · Language / Язык" --radiolist \
"Choose interface language / Выберите язык:" 12 60 2 \ "Choose interface language / Выберите язык:" 12 60 2 \
"ru" "Русский" ON \ "en" "English" ON \
"en" "English" OFF \ "ru" "Русский" OFF \
3>&1 1>&2 2>&3) || ONBOARDING_LANG="ru" 3>&1 1>&2 2>&3) || ONBOARDING_LANG="en"
else else
echo "" >&2 echo "" >&2
echo "Choose language / Выберите язык:" >&2 echo "Choose language / Выберите язык:" >&2
echo " 1) ru — Русский (default)" >&2 echo " 1) en — English (default)" >&2
echo " 2) en — English" >&2 echo " 2) ru — Русский" >&2
read -r -p "[1-2, default 1]: " ans read -r -p "[1-2, default 1]: " ans
case "$ans" in case "$ans" in
2) ONBOARDING_LANG="en" ;; 2) ONBOARDING_LANG="ru" ;;
*) ONBOARDING_LANG="ru" ;; *) ONBOARDING_LANG="en" ;;
esac esac
fi fi
# Перегружаем словарь — все последующие строки на выбранном языке.
if command -v i18n_load_lang >/dev/null 2>&1; then
i18n_load_lang "$ONBOARDING_LANG"
fi
} }
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
@ -118,21 +123,20 @@ onboarding_pick_language() {
onboarding_pick_transport() { onboarding_pick_transport() {
local transports local transports
transports=$(onboarding_list_transports) transports=$(onboarding_list_transports)
local title="${ONBOARDING_LANG:-ru}" local prompt="${STR_PICK_TRANSPORT:-Choose connection transport:}"
local prompt; [ "$title" = "ru" ] && prompt="Выберите способ подключения:" || prompt="Choose connection transport:"
if command -v whiptail >/dev/null 2>&1; then if command -v whiptail >/dev/null 2>&1; then
local args=() local args=()
while IFS= read -r tr; do while IFS= read -r tr; do
local desc local desc
case "$tr" in case "$tr" in
direct-api) desc="Прямой API провайдера (ключ)" ;; direct-api) desc="${STR_TR_DIRECT_API:-Direct provider API}" ;;
aws-bedrock) desc="AWS Bedrock (IAM/role)" ;; aws-bedrock) desc="${STR_TR_AWS_BEDROCK:-AWS Bedrock}" ;;
azure-openai) desc="Azure OpenAI (deployment+key)" ;; azure-openai) desc="${STR_TR_AZURE_OPENAI:-Azure OpenAI}" ;;
google-vertex) desc="Google Vertex AI (GCP)" ;; google-vertex) desc="${STR_TR_GOOGLE_VERTEX:-Google Vertex AI}" ;;
local) desc="Локально (Ollama/MLX/LMStudio)" ;; local) desc="${STR_TR_LOCAL:-Local}" ;;
proxy) desc="Прокси (LiteLLM/OpenRouter)" ;; proxy) desc="${STR_TR_PROXY:-Proxy}" ;;
subscription) desc="OAuth-подписка (ChatGPT)" ;; subscription) desc="${STR_TR_SUBSCRIPTION:-OAuth subscription}" ;;
*) desc="$tr" ;; *) desc="$tr" ;;
esac esac
args+=("$tr" "$desc" "OFF") args+=("$tr" "$desc" "OFF")
@ -173,12 +177,13 @@ onboarding_pick_provider() {
while IFS=$'\t' read -r id dn ae; do while IFS=$'\t' read -r id dn ae; do
args+=("$id" "$dn" "OFF") args+=("$id" "$dn" "OFF")
done <<< "$rows" done <<< "$rows"
local prompt="${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:"
ONBOARDING_PROVIDER=$(whiptail --title "KeiSei · Provider" --radiolist \ ONBOARDING_PROVIDER=$(whiptail --title "KeiSei · Provider" --radiolist \
"Provider within $ONBOARDING_TRANSPORT:" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ "$prompt" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \
|| ONBOARDING_PROVIDER=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') || ONBOARDING_PROVIDER=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}')
else else
echo "" >&2 echo "" >&2
echo "Providers within $ONBOARDING_TRANSPORT:" >&2 echo "${STR_PICK_PROVIDER:-Providers within} $ONBOARDING_TRANSPORT:" >&2
declare -a ids=() declare -a ids=()
local i=1 local i=1
while IFS=$'\t' read -r id dn ae; do while IFS=$'\t' read -r id dn ae; do
@ -213,11 +218,11 @@ onboarding_pick_model() {
args+=("$id" "$dn" "OFF") args+=("$id" "$dn" "OFF")
done <<< "$rows" done <<< "$rows"
ONBOARDING_MODEL=$(whiptail --title "KeiSei · Model" --radiolist \ ONBOARDING_MODEL=$(whiptail --title "KeiSei · Model" --radiolist \
"Default model:" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ "${STR_PICK_MODEL:-Default model:}" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \
|| ONBOARDING_MODEL=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') || ONBOARDING_MODEL=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}')
else else
echo "" >&2 echo "" >&2
echo "Models for $lookup:" >&2 echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2
declare -a ids=() declare -a ids=()
local i=1 local i=1
while IFS=$'\t' read -r id dn; do while IFS=$'\t' read -r id dn; do
@ -241,15 +246,15 @@ onboarding_collect_auth() {
[ -z "$ae" ] || [ "$ae" = "_" ] && return # local / subscription — нет ключей [ -z "$ae" ] || [ "$ae" = "_" ] && return # local / subscription — нет ключей
echo "" >&2 echo "" >&2
echo "Auth для $ONBOARDING_PROVIDER ($ae):" >&2 echo "${STR_AUTH_INTRO:-Auth for} $ONBOARDING_PROVIDER ($ae):" >&2
echo "Введите значения (Enter — оставить пустым, заполним позже)." >&2 echo "${STR_AUTH_PROMPT:-Enter values (Enter — leave empty, fill later).}" >&2
local IFS_old="$IFS"; IFS=',' local IFS_old="$IFS"; IFS=','
for key in $ae; do for key in $ae; do
IFS="$IFS_old" IFS="$IFS_old"
local cur="${!key:-}" local cur="${!key:-}"
local prompt_msg="$key" local prompt_msg="$key"
[ -n "$cur" ] && prompt_msg="$key (текущее: <скрыто>)" [ -n "$cur" ] && prompt_msg="$key ${STR_AUTH_CURRENT_HINT:-(current: <hidden>)}"
# silent read — значение не светит в терминале # silent read — значение не светит в терминале
read -r -s -p " $prompt_msg = " val read -r -s -p " $prompt_msg = " val
echo "" >&2 echo "" >&2
@ -305,9 +310,9 @@ onboarding_run() {
onboarding_should_run || return 0 onboarding_should_run || return 0
if command -v say >/dev/null 2>&1; then if command -v say >/dev/null 2>&1; then
say "onboarding wizard (5 шагов)" say "${STR_ONBOARDING_INTRO:-Onboarding wizard (5 steps)}"
else else
echo "── KeiSei onboarding (5 шагов) ──" >&2 echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (5 steps)} ──" >&2
fi fi
onboarding_pick_language onboarding_pick_language
@ -319,8 +324,8 @@ onboarding_run() {
onboarding_write_config onboarding_write_config
if command -v say >/dev/null 2>&1; then if command -v say >/dev/null 2>&1; then
say "onboarding: $ONBOARDING_TRANSPORT / $ONBOARDING_PROVIDER / $ONBOARDING_MODEL" say "${STR_DONE_TITLE:-onboarding complete}: $ONBOARDING_TRANSPORT / $ONBOARDING_PROVIDER / $ONBOARDING_MODEL"
say " config: $ONBOARDING_CONFIG" say " ${STR_DONE_CONFIG:-config:} $ONBOARDING_CONFIG"
[ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " secrets: $SECRETS_ENV (chmod 600)" [ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " ${STR_DONE_SECRETS:-secrets:} $SECRETS_ENV (chmod 600)"
fi fi
} }