#!/bin/bash set -euo pipefail # ───────────────────────────────────────────── # One-Click HTTPS Website Deployment # Theme: Young Developer Personal Homepage (English) # Cert: Let's Encrypt (domain) / Self-signed (IP) # OS: Ubuntu / Debian / CentOS / AlmaLinux / Rocky / Fedora / Alpine # ───────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' log() { echo -e "${GREEN}[✓]${NC} $*"; } info() { echo -e "${BLUE}[i]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } err() { echo -e "${RED}[✗]${NC} $*"; exit 1; } echo -e "${CYAN}" echo "╔══════════════════════════════════════════╗" echo "║ One-Click HTTPS Web Deploy ║" echo "║ Theme: Young Dev Personal Homepage ║" echo "║ Supports: Ubuntu/Debian/CentOS/Alpine ║" echo "╚══════════════════════════════════════════╝" echo -e "${NC}" [[ $EUID -ne 0 ]] && err "Please run as root or with sudo" # ── OS Detection ────────────────────────────── detect_os() { if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null source /etc/os-release OS_ID="${ID,,}" else err "Cannot detect OS — /etc/os-release not found." fi case "$OS_ID" in ubuntu|debian) PKG_UPDATE="apt-get update -qq" PKG_INSTALL="apt-get install -y -qq -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold" NGINX_PKG="nginx" CERTBOT_PKG="certbot" CERTBOT_NGINX_PKG="python3-certbot-nginx" OPENSSL_PKG="openssl" CURL_PKG="curl" NGINX_USER="www-data" NEED_EPEL=false USE_SYSTEMD=true export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a # suppress needrestart interactive prompt ;; centos|rhel|almalinux|rocky|ol) PKG_UPDATE="yum makecache -q" PKG_INSTALL="yum install -y -q" NGINX_PKG="nginx" CERTBOT_PKG="certbot" CERTBOT_NGINX_PKG="python3-certbot-nginx" OPENSSL_PKG="openssl" CURL_PKG="curl" NGINX_USER="nginx" NEED_EPEL=true USE_SYSTEMD=true ;; fedora) PKG_UPDATE="dnf makecache -q" PKG_INSTALL="dnf install -y -q" NGINX_PKG="nginx" CERTBOT_PKG="certbot" CERTBOT_NGINX_PKG="python3-certbot-nginx" OPENSSL_PKG="openssl" CURL_PKG="curl" NGINX_USER="nginx" NEED_EPEL=false USE_SYSTEMD=true ;; alpine) PKG_UPDATE="apk update -q" PKG_INSTALL="apk add -q" NGINX_PKG="nginx" CERTBOT_PKG="certbot" CERTBOT_NGINX_PKG="certbot-nginx" OPENSSL_PKG="openssl" CURL_PKG="curl" NGINX_USER="nginx" NEED_EPEL=false USE_SYSTEMD=false ;; *) err "Unsupported OS: ${PRETTY_NAME:-$OS_ID}. Supported: Ubuntu / Debian / CentOS / AlmaLinux / Rocky / Fedora / Alpine" ;; esac info "OS detected: ${PRETTY_NAME:-$OS_ID}" } detect_os # ── Service helpers (systemd vs OpenRC) ─────── nginx_enable_start() { if [[ "$USE_SYSTEMD" == true ]]; then systemctl enable nginx --quiet systemctl restart nginx else rc-update add nginx default 2>/dev/null || true rc-service nginx restart fi } nginx_reload() { "$NGINX_BIN" -s reload } # ── Let's Encrypt IP Certificate (shortlived, 6-day, browser-trusted) ────── # Returns 0 and updates SSL_CERT/SSL_KEY on success; returns 1 on any failure. # Requires: nginx running on port 80, port 80 reachable from internet. ACME_CERT_DIR="/etc/ssl/private/acmecert" try_acme_ip_cert() { local _ip="$1" _webroot="$2" _email="${3:-noreply@acme.sh}" local _acme=~/.acme.sh/acme.sh # Install acme.sh if missing if [[ ! -x "$_acme" ]]; then log "Installing acme.sh..." curl -fsSL https://get.acme.sh | sh -s "email=${_email}" >/dev/null 2>&1 || { warn "acme.sh install failed — falling back to self-signed" return 1 } fi [[ -x "$_acme" ]] || { warn "acme.sh not executable"; return 1; } log "Requesting Let's Encrypt IP certificate for ${_ip}..." log "(shortlived profile — 6-day cert, auto-renews, no browser warning)" # Use ECC key (ec-256) — faster and equally secure "$_acme" --issue \ --server letsencrypt \ --cert-profile shortlived \ -d "$_ip" \ -w "$_webroot" \ --keylength ec-256 \ --force \ --log /dev/null 2>&1 | tail -5 || true # Verify cert was issued local _cert_src=~/.acme.sh/${_ip}_ecc if [[ ! -f "${_cert_src}/fullchain.cer" ]]; then warn "IP certificate request failed (port 80 blocked? rate-limited? IP not reachable)" warn "Falling back to self-signed — browser will show security warning" return 1 fi # Determine nginx reload command (must be hardcoded path for cron context) local _reload_cmd if [[ "${BT_MODE:-false}" == true && "${BT_WEB:-}" == "nginx" ]]; then _reload_cmd="/www/server/nginx/sbin/nginx -s reload 2>/dev/null || true" else _reload_cmd="nginx -s reload 2>/dev/null || true" fi # Install cert to a stable path nginx can read mkdir -p "$ACME_CERT_DIR" "$_acme" --install-cert -d "$_ip" --ecc \ --key-file "$ACME_CERT_DIR/privkey.pem" \ --fullchain-file "$ACME_CERT_DIR/fullchain.pem" \ --reloadcmd "$_reload_cmd" >/dev/null 2>&1 || { warn "acme.sh install-cert failed" return 1 } chmod 600 "$ACME_CERT_DIR/privkey.pem" SSL_CERT="$ACME_CERT_DIR/fullchain.pem" SSL_KEY="$ACME_CERT_DIR/privkey.pem" log "IP certificate installed — auto-renewal configured via acme.sh cron" return 0 } # ── BaoTa conflict check ─────────────────────── # Must run before user questions so we can warn early BT_EXISTING_SITES=() check_baota_sites() { if [[ -f /www/server/panel/BT-Panel ]] || \ [[ -x /www/server/nginx/sbin/nginx ]] || \ [[ -x /www/server/apache/bin/httpd ]]; then # Scan both nginx and apache vhost directories while IFS= read -r conf; do fname=$(basename "$conf") [[ "$fname" == "0.default.conf" || "$fname" == "default.conf" ]] && continue BT_EXISTING_SITES+=("$conf") done < <(find /www/server/panel/vhost/nginx \ /www/server/panel/vhost/apache \ -maxdepth 1 -name "*.conf" 2>/dev/null) fi } # ── BaoTa early scan (read-only, safe before confirm) ─ check_baota_sites # ── IP Geolocation ───────────────────────────── COUNTRY_CODE="XX" COUNTRY_NAME="Unknown" LANG_CODE="en" LANG_DISPLAY="English" detect_locale() { command -v curl &>/dev/null || return local _code _json _code=$(curl -s --max-time 6 "https://ipapi.co/country_code/" 2>/dev/null \ | tr -cd 'A-Za-z' | tr '[:lower:]' '[:upper:]' | cut -c1-2) if [[ ${#_code} -eq 2 ]]; then COUNTRY_CODE="$_code" COUNTRY_NAME=$(curl -s --max-time 3 "https://ipapi.co/country_name/" 2>/dev/null \ | tr -d '\n\r' | head -c 64) else _json=$(curl -s --max-time 6 "http://ip-api.com/json/?fields=status,countryCode,country" 2>/dev/null) _code=$(echo "$_json" | grep -o '"countryCode":"[^"]*"' | cut -d'"' -f4 \ | tr -cd 'A-Za-z' | tr '[:lower:]' '[:upper:]' | cut -c1-2) [[ ${#_code} -eq 2 ]] && COUNTRY_CODE="$_code" COUNTRY_NAME=$(echo "$_json" | grep -o '"country":"[^"]*"' | cut -d'"' -f4) fi [[ -z "$COUNTRY_NAME" ]] && COUNTRY_NAME="$COUNTRY_CODE" case "$COUNTRY_CODE" in CN) LANG_CODE="zh-CN"; LANG_DISPLAY="中文(简体)" ;; TW|HK|MO) LANG_CODE="zh-TW"; LANG_DISPLAY="中文(繁體)" ;; JP) LANG_CODE="ja"; LANG_DISPLAY="日本語" ;; KR) LANG_CODE="ko"; LANG_DISPLAY="한국어" ;; RU|UA|BY|KZ) LANG_CODE="ru"; LANG_DISPLAY="Русский" ;; DE|AT|CH) LANG_CODE="de"; LANG_DISPLAY="Deutsch" ;; FR|BE) LANG_CODE="fr"; LANG_DISPLAY="Français" ;; ES|MX|AR|CL|CO|PE|VE|EC|BO|PY|UY|CR|PA|DO|HN|SV|GT|NI|CU) LANG_CODE="es"; LANG_DISPLAY="Español" ;; BR|PT) LANG_CODE="pt"; LANG_DISPLAY="Português" ;; IT) LANG_CODE="it"; LANG_DISPLAY="Italiano" ;; VN) LANG_CODE="vi"; LANG_DISPLAY="Tiếng Việt" ;; ID|MY) LANG_CODE="id"; LANG_DISPLAY="Bahasa Indonesia" ;; SA|AE|EG|MA|DZ|TN|IQ|SY|JO|KW|QA|BH|OM|YE|LB|SD|LY) LANG_CODE="ar"; LANG_DISPLAY="العربية" ;; *) LANG_CODE="en"; LANG_DISPLAY="English" ;; esac info "Server location: ${COUNTRY_NAME} (${COUNTRY_CODE}) — site language: ${LANG_DISPLAY}" } detect_locale # Prompt defaults — change by detected language case "$LANG_CODE" in zh-CN) L_DEFAULT_NAME="小明"; L_DEFAULT_BIO="我喜欢把想法变成真实存在于网络上的东西。" ;; zh-TW) L_DEFAULT_NAME="小明"; L_DEFAULT_BIO="我喜歡把想法變成真實存在於網路上的東西。" ;; ja) L_DEFAULT_NAME="タロウ"; L_DEFAULT_BIO="アイデアをインターネット上の現実に変えるのが好きです。" ;; ko) L_DEFAULT_NAME="민준"; L_DEFAULT_BIO="아이디어를 인터넷 위의 현실로 만드는 걸 좋아합니다." ;; ru) L_DEFAULT_NAME="Алекс"; L_DEFAULT_BIO="Люблю воплощать идеи в реальные вещи в интернете." ;; de) L_DEFAULT_NAME="Alex"; L_DEFAULT_BIO="Ich verwandle Ideen in echte Dinge im Internet." ;; fr) L_DEFAULT_NAME="Alex"; L_DEFAULT_BIO="J'adore transformer des idées en choses concrètes sur internet." ;; es) L_DEFAULT_NAME="Alex"; L_DEFAULT_BIO="Me encanta convertir ideas en cosas reales en internet." ;; pt) L_DEFAULT_NAME="Alex"; L_DEFAULT_BIO="Adoro transformar ideias em coisas reais na internet." ;; it) L_DEFAULT_NAME="Alex"; L_DEFAULT_BIO="Adoro trasformare le idee in cose reali su internet." ;; id) L_DEFAULT_NAME="Budi"; L_DEFAULT_BIO="Saya suka mengubah ide menjadi sesuatu yang nyata di internet." ;; vi) L_DEFAULT_NAME="Minh"; L_DEFAULT_BIO="Tôi thích biến ý tưởng thành những thứ thực sự trên internet." ;; ar) L_DEFAULT_NAME="أحمد"; L_DEFAULT_BIO="أحب تحويل الأفكار إلى أشياء حقيقية على الإنترنت." ;; *) L_DEFAULT_NAME="Alex Chen"; L_DEFAULT_BIO="I build things that live on the internet." ;; esac # ── build_locale_content: sets all L_ HTML strings ────────── # Called AFTER DEV_NAME / DEV_AGE / DEV_BIO are known build_locale_content() { case "$LANG_CODE" in zh-CN) L_HTML_LANG="zh-CN" L_HTML_DIR="" L_BODY_FONT_EXTRA=",'PingFang SC','Microsoft YaHei','Source Han Sans SC'" L_TITLE_SUFFIX="— 开发者主页" L_NAV_ABOUT="关于"; L_NAV_SKILLS="技能"; L_NAV_PROJECTS="项目"; L_NAV_CONTACT="联系" L_HERO_GREETING="嘿 👋 我是" L_HERO_AGE_LINE="[ ${DEV_AGE}岁 · 开发者 & 创造者 ]" L_HERO_BTN1="▶ 查看项目"; L_HERO_BTN2="✉ 联系我" L_ABOUT_KW="// 关于我"; L_ABOUT_H2="了解一下 about_me" L_ABOUT_P1="你好!我是 ${DEV_NAME},${DEV_AGE}岁,自学编程,喜欢把脑海里的想法变成互联网上真实存在的东西。" L_ABOUT_P2="写代码的起点是想做自己的游戏。然后就停不下来了——总有新东西可以学,每次修完 bug 都有种小小的成就感。" L_ABOUT_P3="不敲代码的时候,我在研究计算机原理、刷技术视频,或者想怎么把分数再打高一点。" L_FACTS_KW="// 一点趣事" L_FACT1="☕ 靠零食和好奇心驱动"; L_FACT2="🌙 最好的代码写于深夜" L_FACT3="🐛 Bug 只是被误解的功能"; L_FACT4="🌍 梦想做出百万人在用的东西" L_TERM_COMMENT="// 我的个人简历" L_JSON_STATUS="持续学习中"; L_JSON_GOAL="做出让人喜欢的东西" L_SKILLS_KW="// 技能"; L_SKILLS_H2="我掌握的 know()" L_PROJ_KW="// 项目"; L_PROJ_H2="我做过的 built()" L_PROJ_DEMO="演示"; L_PROJ_CODE="代码" L_P1_NAME="贪吃蛇游戏"; L_P1_DESC="用 Python 和 Pygame 从零实现的经典贪吃蛇。难度随分数递增,有最高分记录和自制像素图形。" L_P2_NAME="实时天气看板"; L_P2_DESC="调用 OpenWeather API 展示实时天气和预报,带动态图标。纯原生 JavaScript 实现。" L_P3_NAME="Discord 机器人"; L_P3_DESC="多功能 Bot:频道管理、播放音乐、趣味问答,支持自定义指令。" L_P4_NAME="Markdown 笔记本"; L_P4_DESC="简洁笔记应用,支持实时预览、本地存储和深色/亮色切换。不依赖框架,纯 HTML/CSS/JS。" L_P5_NAME="记账 CLI 工具"; L_P5_DESC="命令行记账软件,追踪收支、生成月报、导出 CSV,数据存于 SQLite。" L_P6_NAME="本站"; L_P6_DESC="部署在 VPS 上的个人主页,Nginx + HTTPS。纯 HTML & CSS,无框架,快速轻量,完全属于自己。" L_CONTACT_KW="// 联系"; L_CONTACT_H2="打个招呼 hello()" L_CONTACT_P="有想聊的项目?想一起做点什么?还是单纯想交流技术?我的邮件随时开着——通常当天回复。" L_FOOTER_LEFT="设计 & 构建:${DEV_NAME},${DEV_AGE}岁" L_FOOTER_RIGHT="© $(date +%Y) · 用 ☕ & 🐛 做的" ;; zh-TW) L_HTML_LANG="zh-TW"; L_HTML_DIR="" L_BODY_FONT_EXTRA=",'PingFang TC','Microsoft JhengHei','Source Han Sans TC'" L_TITLE_SUFFIX="— 開發者主頁" L_NAV_ABOUT="關於"; L_NAV_SKILLS="技能"; L_NAV_PROJECTS="專案"; L_NAV_CONTACT="聯絡" L_HERO_GREETING="嗨 👋 我是" L_HERO_AGE_LINE="[ ${DEV_AGE}歲 · 開發者 & 創造者 ]" L_HERO_BTN1="▶ 查看專案"; L_HERO_BTN2="✉ 聯絡我" L_ABOUT_KW="// 關於我"; L_ABOUT_H2="認識一下 about_me" L_ABOUT_P1="嗨!我是 ${DEV_NAME},${DEV_AGE}歲,自學程式設計,喜歡把腦海中的想法變成網路上真實存在的東西。" L_ABOUT_P2="寫程式的起點是想做自己的遊戲。從此停不下來了——永遠有新東西可以學,每次修完 bug 都有種小小的成就感。" L_ABOUT_P3="不寫程式的時候,我在研究電腦原理、刷技術影片,或是想辦法突破自己的高分紀錄。" L_FACTS_KW="// 有趣的事" L_FACT1="☕ 靠零食和好奇心驅動"; L_FACT2="🌙 最好的程式碼寫於深夜" L_FACT3="🐛 Bug 只是被誤解的功能"; L_FACT4="🌍 夢想做出百萬人在用的東西" L_TERM_COMMENT="// 我的個人簡歷" L_JSON_STATUS="持續學習中"; L_JSON_GOAL="做出讓人喜歡的東西" L_SKILLS_KW="// 技能"; L_SKILLS_H2="我掌握的 know()" L_PROJ_KW="// 專案"; L_PROJ_H2="我做過的 built()" L_PROJ_DEMO="展示"; L_PROJ_CODE="程式碼" L_P1_NAME="貪吃蛇遊戲"; L_P1_DESC="用 Python 和 Pygame 從零實現的貪吃蛇。難度隨分數提升,有最高分記錄和像素圖形。" L_P2_NAME="即時天氣看板"; L_P2_DESC="串接 OpenWeather API,展示即時天氣與預報,帶動態圖示。純原生 JavaScript。" L_P3_NAME="Discord 機器人"; L_P3_DESC="多功能 Bot:頻道管理、播放音樂、趣味問答,支援自訂指令。" L_P4_NAME="Markdown 筆記本"; L_P4_DESC="簡潔筆記應用,即時預覽、本地儲存、深色/亮色切換。不依賴框架,純 HTML/CSS/JS。" L_P5_NAME="記帳 CLI 工具"; L_P5_DESC="命令列記帳軟體,追蹤收支、月報統計、匯出 CSV,資料存於 SQLite。" L_P6_NAME="本站"; L_P6_DESC="部署在 VPS 上的個人主頁,Nginx + HTTPS。純 HTML & CSS,無框架,快速輕量。" L_CONTACT_KW="// 聯絡"; L_CONTACT_H2="打聲招呼 hello()" L_CONTACT_P="有想聊的專案?想一起做點什麼?或是單純想交流技術?我的信箱隨時開著——通常當天回覆。" L_FOOTER_LEFT="設計 & 構建:${DEV_NAME},${DEV_AGE}歲" L_FOOTER_RIGHT="© $(date +%Y) · 用 ☕ & 🐛 做的" ;; ja) L_HTML_LANG="ja"; L_HTML_DIR="" L_BODY_FONT_EXTRA=",'Hiragino Kaku Gothic ProN','Noto Sans CJK JP','Yu Gothic'" L_TITLE_SUFFIX="— 開発者ポートフォリオ" L_NAV_ABOUT="プロフィール"; L_NAV_SKILLS="スキル"; L_NAV_PROJECTS="プロジェクト"; L_NAV_CONTACT="連絡" L_HERO_GREETING="こんにちは 👋 私は" L_HERO_AGE_LINE="[ ${DEV_AGE}歳 · デベロッパー & クリエイター ]" L_HERO_BTN1="▶ プロジェクトを見る"; L_HERO_BTN2="✉ 連絡する" L_ABOUT_KW="// プロフィール"; L_ABOUT_H2="about_me について" L_ABOUT_P1="こんにちは!${DEV_NAME}です。${DEV_AGE}歳の独学プログラマーで、頭の中のアイデアをインターネット上の実物に変えることが大好きです。" L_ABOUT_P2="コードを書き始めたきっかけは自分でゲームを作りたかったから。今は止まれません——常に新しいことを学べて、バグが直るたびに達成感があります。" L_ABOUT_P3="コードを書いていないときは、コンピュータの仕組みを調べたり、技術系の動画を見たり、ハイスコアを更新しようとしたりしています。" L_FACTS_KW="// 豆知識" L_FACT1="☕ お菓子と好奇心で動いている"; L_FACT2="🌙 最高のコードは深夜に生まれる" L_FACT3="🐛 バグはただ誤解された機能"; L_FACT4="🌍 数百万人が使うものを作る夢" L_TERM_COMMENT="// 個人プロフィール" L_JSON_STATUS="常に学習中"; L_JSON_GOAL="人に使われるものを作る" L_SKILLS_KW="// スキル"; L_SKILLS_H2="できること know()" L_PROJ_KW="// プロジェクト"; L_PROJ_H2="作ったもの built()" L_PROJ_DEMO="デモ"; L_PROJ_CODE="コード" L_P1_NAME="スネークゲーム"; L_P1_DESC="Python と Pygame でゼロから作ったスネークゲーム。スコアに応じて速度が上がり、ハイスコア保存とドット絵グラフィック付き。" L_P2_NAME="天気ダッシュボード"; L_P2_DESC="OpenWeather API で取得したリアルタイム天気とアニメーションアイコンを表示。バニラ JavaScript のみで実装。" L_P3_NAME="Discord Bot"; L_P3_DESC="モデレーション・音楽再生・クイズ・カスタムコマンドに対応した多機能 Bot。" L_P4_NAME="Markdown ノートアプリ"; L_P4_DESC="リアルタイムプレビュー・ローカル保存・ダーク/ライト切替。フレームワーク不使用、純粋な HTML/CSS/JS。" L_P5_NAME="家計簿 CLI ツール"; L_P5_DESC="収支管理・月次レポート・CSV エクスポートができるコマンドラインツール。SQLite でデータ保存。" L_P6_NAME="このサイト"; L_P6_DESC="VPS に Nginx と HTTPS でデプロイした個人サイト。フレームワークなし、純粋な HTML & CSS で高速軽量。" L_CONTACT_KW="// 連絡"; L_CONTACT_H2="hello() と言ってね" L_CONTACT_P="面白いプロジェクトの話、コラボのお誘い、技術の雑談など何でも歓迎です。メールはいつでも開いていて、大体その日のうちに返信します。" L_FOOTER_LEFT="デザイン & 構築:${DEV_NAME}(${DEV_AGE}歳)" L_FOOTER_RIGHT="© $(date +%Y) · ☕ & 🐛 で作った" ;; ko) L_HTML_LANG="ko"; L_HTML_DIR="" L_BODY_FONT_EXTRA=",'Apple SD Gothic Neo','Noto Sans KR','Malgun Gothic'" L_TITLE_SUFFIX="— 개발자 포트폴리오" L_NAV_ABOUT="소개"; L_NAV_SKILLS="스킬"; L_NAV_PROJECTS="프로젝트"; L_NAV_CONTACT="연락" L_HERO_GREETING="안녕하세요 👋 저는" L_HERO_AGE_LINE="[ ${DEV_AGE}세 · 개발자 & 메이커 ]" L_HERO_BTN1="▶ 프로젝트 보기"; L_HERO_BTN2="✉ 연락하기" L_ABOUT_KW="// 소개"; L_ABOUT_H2="저에 대해 about_me" L_ABOUT_P1="안녕하세요! 저는 ${DEV_NAME}이에요. ${DEV_AGE}살의 독학 개발자로, 머릿속 아이디어를 인터넷 위의 현실로 만드는 걸 좋아합니다." L_ABOUT_P2="코딩을 시작한 건 내 게임을 만들고 싶어서였어요. 지금은 멈출 수가 없어요—항상 새로운 걸 배울 수 있고, 버그를 고칠 때마다 성취감이 생깁니다." L_ABOUT_P3="코딩을 안 할 때는 컴퓨터 작동 원리를 공부하거나, 기술 영상을 보거나, 하이스코어를 깨려고 노력하고 있어요." L_FACTS_KW="// 재미있는 사실" L_FACT1="☕ 간식과 호기심으로 작동 중"; L_FACT2="🌙 최고의 코드는 자정 이후에 탄생" L_FACT3="🐛 버그는 오해받은 기능"; L_FACT4="🌍 수백만 명이 쓰는 걸 만들고 싶다" L_TERM_COMMENT="// 개인 프로필" L_JSON_STATUS="항상 배우는 중"; L_JSON_GOAL="사람들이 좋아하는 걸 만들기" L_SKILLS_KW="// 스킬"; L_SKILLS_H2="내가 아는 것들 know()" L_PROJ_KW="// 프로젝트"; L_PROJ_H2="내가 만든 것들 built()" L_PROJ_DEMO="데모"; L_PROJ_CODE="코드" L_P1_NAME="스네이크 게임"; L_P1_DESC="Python과 Pygame으로 처음부터 만든 스네이크 게임. 점수에 따라 속도 증가, 최고점수 저장, 픽셀 아트 그래픽." L_P2_NAME="날씨 대시보드"; L_P2_DESC="OpenWeather API로 실시간 날씨를 가져와 애니메이션 아이콘과 예보를 보여주는 앱. 바닐라 JS로 제작." L_P3_NAME="Discord 봇"; L_P3_DESC="관리·음악 재생·퀴즈·커스텀 명령어를 지원하는 다기능 봇." L_P4_NAME="Markdown 노트 앱"; L_P4_DESC="실시간 미리보기, 로컬 저장, 다크/라이트 모드 지원. 프레임워크 없이 순수 HTML/CSS/JS로 제작." L_P5_NAME="가계부 CLI 도구"; L_P5_DESC="수입/지출 추적, 월별 요약, CSV 내보내기가 가능한 커맨드라인 도구. SQLite 저장." L_P6_NAME="이 사이트"; L_P6_DESC="VPS에 Nginx와 HTTPS로 배포한 개인 포트폴리오. 프레임워크 없이 순수 HTML & CSS, 빠르고 가볍다." L_CONTACT_KW="// 연락"; L_CONTACT_H2="hello() 해줘요" L_CONTACT_P="멋진 프로젝트 아이디어가 있나요? 같이 뭔가 만들고 싶나요? 아니면 그냥 기술 얘기가 하고 싶나요? 메일은 항상 열려 있어요—보통 당일에 답장해요." L_FOOTER_LEFT="디자인 & 제작:${DEV_NAME}(${DEV_AGE}세)" L_FOOTER_RIGHT="© $(date +%Y) · ☕ & 🐛 으로 만들었어요" ;; ru) L_HTML_LANG="ru"; L_HTML_DIR="" L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Страница разработчика" L_NAV_ABOUT="Обо мне"; L_NAV_SKILLS="Навыки"; L_NAV_PROJECTS="Проекты"; L_NAV_CONTACT="Контакт" L_HERO_GREETING="Привет 👋 Я —" L_HERO_AGE_LINE="[ ${DEV_AGE}-летний разработчик & создатель ]" L_HERO_BTN1="▶ Мои проекты"; L_HERO_BTN2="✉ Написать мне" L_ABOUT_KW="// обо мне"; L_ABOUT_H2="Немного about_me" L_ABOUT_P1="Привет! Я ${DEV_NAME}, ${DEV_AGE}-летний самоучка, который любит превращать идеи в реальные вещи в интернете." L_ABOUT_P2="Начал программировать, потому что хотел сделать свою игру. Теперь остановиться невозможно — всегда есть что-то новое, а каждый исправленный баг ощущается как маленькая победа." L_ABOUT_P3="Когда я не за кодом, читаю о том, как работают компьютеры, смотрю технические видео или пытаюсь побить свой рекорд." L_FACTS_KW="// интересные факты" L_FACT1="☕ Работает на снеках и любопытстве"; L_FACT2="🌙 Лучший код пишется после полуночи" L_FACT3="🐛 Баги — это просто непонятые фичи"; L_FACT4="🌍 Мечта — создать то, чем пользуются миллионы" L_TERM_COMMENT="// мой персональный профиль" L_JSON_STATUS="всегда учусь"; L_JSON_GOAL="сделать что-то крутое" L_SKILLS_KW="// навыки"; L_SKILLS_H2="Что я умею()" L_PROJ_KW="// проекты"; L_PROJ_H2="Что я построил()" L_PROJ_DEMO="демо"; L_PROJ_CODE="код" L_P1_NAME="Игра «Змейка»"; L_P1_DESC="Классическая змейка на Python и Pygame с нуля: скорость растёт с очками, есть рекорд и пиксельная графика." L_P2_NAME="Погодная панель"; L_P2_DESC="Приложение на чистом JS с реалтайм-погодой через OpenWeather API и анимированными иконками прогноза." L_P3_NAME="Discord-бот"; L_P3_DESC="Многофункциональный бот: модерация, музыка, викторина и пользовательские команды." L_P4_NAME="Заметки на Markdown"; L_P4_DESC="Минималистичное приложение с живым превью, локальным хранилищем и тёмной/светлой темой. Без фреймворков." L_P5_NAME="Трекер бюджета (CLI)"; L_P5_DESC="Командная строка для учёта доходов и расходов, экспорта в CSV и отчётов по месяцам. База — SQLite." L_P6_NAME="Этот сайт"; L_P6_DESC="Личный портфолио на VPS с Nginx и HTTPS. Чистый HTML & CSS без фреймворков — быстро и по-своему." L_CONTACT_KW="// контакт"; L_CONTACT_H2="Скажи hello()" L_CONTACT_P="Есть идея для проекта? Хочешь поработать вместе? Или просто поговорить о коде? Мой ящик всегда открыт — отвечаю в тот же день." L_FOOTER_LEFT="Придумал и сделал ${DEV_NAME} в ${DEV_AGE} лет" L_FOOTER_RIGHT="© $(date +%Y) · Сделано с ☕ & 🐛" ;; de) L_HTML_LANG="de"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Entwickler-Portfolio" L_NAV_ABOUT="Über mich"; L_NAV_SKILLS="Skills"; L_NAV_PROJECTS="Projekte"; L_NAV_CONTACT="Kontakt" L_HERO_GREETING="Hallo 👋 Ich bin" L_HERO_AGE_LINE="[ ${DEV_AGE}-jähriger Entwickler & Maker ]" L_HERO_BTN1="▶ Meine Projekte"; L_HERO_BTN2="✉ Kontakt aufnehmen" L_ABOUT_KW="// über mich"; L_ABOUT_H2="Ein bisschen about_me" L_ABOUT_P1="Hey! Ich bin ${DEV_NAME}, ein ${DEV_AGE}-jähriger Autodidakt, der Ideen in echte Dinge im Internet verwandelt." L_ABOUT_P2="Ich hab mit dem Coden angefangen, weil ich eigene Spiele bauen wollte. Jetzt kann ich nicht mehr aufhören — es gibt immer was Neues zu lernen." L_ABOUT_P3="Wenn ich nicht code, lese ich über Computer, schaue Tech-YouTube oder versuche meinen Highscore zu brechen." L_FACTS_KW="// Spaß-Fakten" L_FACT1="☕ Läuft auf Snacks und Neugier"; L_FACT2="🌙 Bester Code entsteht nach Mitternacht" L_FACT3="🐛 Bugs sind nur missverstandene Features"; L_FACT4="🌍 Traum: etwas bauen, das Millionen nutzen" L_TERM_COMMENT="// mein persönliches Profil" L_JSON_STATUS="immer am Lernen"; L_JSON_GOAL="etwas Cooles veröffentlichen" L_SKILLS_KW="// Skills"; L_SKILLS_H2="Was ich kann()" L_PROJ_KW="// Projekte"; L_PROJ_H2="Was ich gebaut habe()" L_PROJ_DEMO="Demo"; L_PROJ_CODE="Code" L_P1_NAME="Snake-Spiel"; L_P1_DESC="Klassisches Snake von Grund auf in Python mit Pygame. Steigende Schwierigkeit, Highscore-Speicherung und Pixel-Art." L_P2_NAME="Wetter-Dashboard"; L_P2_DESC="Echtzeit-Wetter-App mit OpenWeather API und animierten Icons. Mit reinem JavaScript gebaut." L_P3_NAME="Discord-Bot"; L_P3_DESC="Mehrzweck-Bot mit Moderation, Musik, Quiz und benutzerdefinierten Befehlen." L_P4_NAME="Markdown-Notiz-App"; L_P4_DESC="Minimalistische Notiz-App mit Live-Vorschau, LocalStorage und Dark/Light-Modus. Kein Framework." L_P5_NAME="Budget-Tracker (CLI)"; L_P5_DESC="Kommandozeilen-Tool für Einnahmen & Ausgaben, CSV-Export und SQLite-Datenspeicherung." L_P6_NAME="Diese Website"; L_P6_DESC="Persönliches Portfolio auf einem VPS mit Nginx und HTTPS. Reines HTML & CSS — kein Framework, schnell und leicht." L_CONTACT_KW="// Kontakt"; L_CONTACT_H2="Sag Hallo()" L_CONTACT_P="Hast du eine coole Projektidee? Möchtest du zusammenarbeiten oder einfach über Code reden? Mein Posteingang ist immer offen — ich antworte meistens noch am selben Tag." L_FOOTER_LEFT="Designed & gebaut von ${DEV_NAME} — ${DEV_AGE} Jahre" L_FOOTER_RIGHT="© $(date +%Y) · Gemacht mit ☕ & 🐛" ;; fr) L_HTML_LANG="fr"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Portfolio développeur" L_NAV_ABOUT="À propos"; L_NAV_SKILLS="Compétences"; L_NAV_PROJECTS="Projets"; L_NAV_CONTACT="Contact" L_HERO_GREETING="Salut 👋 Je suis" L_HERO_AGE_LINE="[ développeur de ${DEV_AGE} ans & créateur ]" L_HERO_BTN1="▶ Mes projets"; L_HERO_BTN2="✉ Me contacter" L_ABOUT_KW="// à propos"; L_ABOUT_H2="Un peu about_me" L_ABOUT_P1="Salut ! Je suis ${DEV_NAME}, un développeur autodidacte de ${DEV_AGE} ans qui adore transformer des idées en choses concrètes sur internet." L_ABOUT_P2="J'ai commencé à coder pour créer mes propres jeux. Maintenant je ne peux plus m'arrêter — il y a toujours quelque chose de nouveau à apprendre." L_ABOUT_P3="Quand je ne code pas, je lis sur le fonctionnement des ordinateurs, je regarde des vidéos tech ou j'essaie de battre mon high score." L_FACTS_KW="// anecdotes" L_FACT1="☕ Fonctionne aux snacks et à la curiosité"; L_FACT2="🌙 Le meilleur code s'écrit après minuit" L_FACT3="🐛 Les bugs sont juste des features incomprises"; L_FACT4="🌍 Rêve de construire quelque chose utilisé par des millions" L_TERM_COMMENT="// mon profil personnel" L_JSON_STATUS="toujours en apprentissage"; L_JSON_GOAL="livrer quelque chose de cool" L_SKILLS_KW="// compétences"; L_SKILLS_H2="Ce que je sais faire()" L_PROJ_KW="// projets"; L_PROJ_H2="Ce que j'ai construit()" L_PROJ_DEMO="démo"; L_PROJ_CODE="code" L_P1_NAME="Jeu Snake"; L_P1_DESC="Le classique Snake en Python avec Pygame, de zéro. Vitesse croissante, meilleur score sauvegardé et pixel art." L_P2_NAME="Dashboard Météo"; L_P2_DESC="App météo temps réel avec l'API OpenWeather et des icônes animées. En JavaScript pur." L_P3_NAME="Bot Discord"; L_P3_DESC="Bot multifonctions : modération, musique, quiz et commandes personnalisées." L_P4_NAME="App Notes Markdown"; L_P4_DESC="App de notes minimaliste avec aperçu live, stockage local et mode sombre/clair. Sans framework." L_P5_NAME="Budget Tracker (CLI)"; L_P5_DESC="Outil en ligne de commande pour suivre revenus et dépenses, exporter en CSV, stocker dans SQLite." L_P6_NAME="Ce site"; L_P6_DESC="Portfolio personnel déployé sur VPS avec Nginx et HTTPS. HTML & CSS pur, sans framework — rapide et léger." L_CONTACT_KW="// contact"; L_CONTACT_H2="Dis bonjour()" L_CONTACT_P="Tu as une idée de projet sympa ? Tu veux collaborer ou juste parler de code ? Ma boîte mail est toujours ouverte — je réponds généralement dans la journée." L_FOOTER_LEFT="Conçu & construit par ${DEV_NAME} — ${DEV_AGE} ans" L_FOOTER_RIGHT="© $(date +%Y) · Fait avec ☕ & 🐛" ;; es) L_HTML_LANG="es"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Portfolio de desarrollador" L_NAV_ABOUT="Sobre mí"; L_NAV_SKILLS="Habilidades"; L_NAV_PROJECTS="Proyectos"; L_NAV_CONTACT="Contacto" L_HERO_GREETING="Hola 👋 Soy" L_HERO_AGE_LINE="[ desarrollador de ${DEV_AGE} años & creador ]" L_HERO_BTN1="▶ Ver proyectos"; L_HERO_BTN2="✉ Contactarme" L_ABOUT_KW="// sobre mí"; L_ABOUT_H2="Un poco about_me" L_ABOUT_P1="¡Hola! Soy ${DEV_NAME}, un desarrollador autodidacta de ${DEV_AGE} años que ama convertir ideas en cosas reales en internet." L_ABOUT_P2="Empecé a programar porque quería hacer mis propios juegos. Ahora no puedo parar — siempre hay algo nuevo que aprender y cada bug corregido se siente como una victoria." L_ABOUT_P3="Cuando no estoy programando, leo sobre cómo funcionan los ordenadores, veo vídeos de tecnología o intento superar mi récord." L_FACTS_KW="// curiosidades" L_FACT1="☕ Funciona con snacks y curiosidad"; L_FACT2="🌙 El mejor código se escribe pasada la medianoche" L_FACT3="🐛 Los bugs son features incomprendidas"; L_FACT4="🌍 Sueño con construir algo que usen millones" L_TERM_COMMENT="// mi perfil personal" L_JSON_STATUS="siempre aprendiendo"; L_JSON_GOAL="lanzar algo guay" L_SKILLS_KW="// habilidades"; L_SKILLS_H2="Lo que sé hacer()" L_PROJ_KW="// proyectos"; L_PROJ_H2="Lo que he construido()" L_PROJ_DEMO="demo"; L_PROJ_CODE="código" L_P1_NAME="Juego Snake"; L_P1_DESC="El clásico Snake en Python con Pygame desde cero. Velocidad creciente, guardado de récord y pixel art." L_P2_NAME="Panel del Tiempo"; L_P2_DESC="App del tiempo en tiempo real con la API de OpenWeather e iconos animados. En JavaScript puro." L_P3_NAME="Bot de Discord"; L_P3_DESC="Bot multipropósito con moderación, música, trivial y comandos personalizados." L_P4_NAME="App de Notas Markdown"; L_P4_DESC="App de notas minimalista con vista previa en vivo, almacenamiento local y modo oscuro/claro. Sin frameworks." L_P5_NAME="Rastreador de Presupuesto (CLI)"; L_P5_DESC="Herramienta CLI para rastrear ingresos y gastos, exportar a CSV y almacenar en SQLite." L_P6_NAME="Esta web"; L_P6_DESC="Portfolio personal en VPS con Nginx y HTTPS. HTML & CSS puro sin frameworks — rápido, ligero y mío." L_CONTACT_KW="// contacto"; L_CONTACT_H2="Di hola()" L_CONTACT_P="¿Tienes una idea de proyecto chula? ¿Quieres colaborar o simplemente hablar de código? Mi correo siempre está abierto — suelo responder el mismo día." L_FOOTER_LEFT="Diseñado & construido por ${DEV_NAME} — ${DEV_AGE} años" L_FOOTER_RIGHT="© $(date +%Y) · Hecho con ☕ & 🐛" ;; pt) L_HTML_LANG="pt"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Portfolio do desenvolvedor" L_NAV_ABOUT="Sobre mim"; L_NAV_SKILLS="Habilidades"; L_NAV_PROJECTS="Projetos"; L_NAV_CONTACT="Contato" L_HERO_GREETING="Olá 👋 Eu sou" L_HERO_AGE_LINE="[ desenvolvedor de ${DEV_AGE} anos & criador ]" L_HERO_BTN1="▶ Ver projetos"; L_HERO_BTN2="✉ Entrar em contato" L_ABOUT_KW="// sobre mim"; L_ABOUT_H2="Um pouco about_me" L_ABOUT_P1="Olá! Eu sou ${DEV_NAME}, um desenvolvedor autodidata de ${DEV_AGE} anos que adora transformar ideias em coisas reais na internet." L_ABOUT_P2="Comecei a programar porque queria fazer meus próprios jogos. Agora não consigo parar — sempre há algo novo para aprender e cada bug corrigido parece uma vitória." L_ABOUT_P3="Quando não estou programando, leio sobre como os computadores funcionam, assisto vídeos de tecnologia ou tento bater meu recorde." L_FACTS_KW="// curiosidades" L_FACT1="☕ Roda com snacks e curiosidade"; L_FACT2="🌙 O melhor código é escrito depois da meia-noite" L_FACT3="🐛 Bugs são só features incompreendidas"; L_FACT4="🌍 Sonho em construir algo usado por milhões" L_TERM_COMMENT="// meu perfil pessoal" L_JSON_STATUS="sempre aprendendo"; L_JSON_GOAL="lançar algo incrível" L_SKILLS_KW="// habilidades"; L_SKILLS_H2="O que eu sei fazer()" L_PROJ_KW="// projetos"; L_PROJ_H2="O que eu construí()" L_PROJ_DEMO="demo"; L_PROJ_CODE="código" L_P1_NAME="Jogo da Cobrinha"; L_P1_DESC="A clássica cobrinha em Python com Pygame do zero. Velocidade crescente, salva recorde e tem pixel art." L_P2_NAME="Painel do Tempo"; L_P2_DESC="App de clima em tempo real com API do OpenWeather e ícones animados. Feito em JavaScript puro." L_P3_NAME="Bot do Discord"; L_P3_DESC="Bot multiuso com moderação, música, quiz e comandos personalizados." L_P4_NAME="App de Notas Markdown"; L_P4_DESC="App minimalista com prévia ao vivo, armazenamento local e modo escuro/claro. Sem frameworks." L_P5_NAME="Rastreador de Orçamento (CLI)"; L_P5_DESC="Ferramenta CLI para rastrear receitas e despesas, exportar para CSV e armazenar em SQLite." L_P6_NAME="Este site"; L_P6_DESC="Portfolio pessoal em VPS com Nginx e HTTPS. HTML & CSS puro sem frameworks — rápido, leve e meu." L_CONTACT_KW="// contato"; L_CONTACT_H2="Diz olá()" L_CONTACT_P="Tem uma ideia de projeto legal? Quer colaborar ou só bater um papo sobre código? Meu e-mail está sempre aberto — costumo responder no mesmo dia." L_FOOTER_LEFT="Projetado & construído por ${DEV_NAME} — ${DEV_AGE} anos" L_FOOTER_RIGHT="© $(date +%Y) · Feito com ☕ & 🐛" ;; it) L_HTML_LANG="it"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Portfolio sviluppatore" L_NAV_ABOUT="Chi sono"; L_NAV_SKILLS="Competenze"; L_NAV_PROJECTS="Progetti"; L_NAV_CONTACT="Contatti" L_HERO_GREETING="Ciao 👋 Sono" L_HERO_AGE_LINE="[ sviluppatore di ${DEV_AGE} anni & creatore ]" L_HERO_BTN1="▶ I miei progetti"; L_HERO_BTN2="✉ Contattami" L_ABOUT_KW="// chi sono"; L_ABOUT_H2="Un po' di about_me" L_ABOUT_P1="Ciao! Sono ${DEV_NAME}, uno sviluppatore autodidatta di ${DEV_AGE} anni che ama trasformare idee in cose reali su internet." L_ABOUT_P2="Ho cominciato a programmare perché volevo creare i miei videogiochi. Ora non riesco a smettere — c'è sempre qualcosa di nuovo da imparare." L_ABOUT_P3="Quando non sto programmando, leggo di computer, guardo video tech o cerco di battere il mio record." L_FACTS_KW="// curiosità" L_FACT1="☕ Funziona a snack e curiosità"; L_FACT2="🌙 Il codice migliore nasce dopo mezzanotte" L_FACT3="🐛 I bug sono solo feature fraintese"; L_FACT4="🌍 Sogno di creare qualcosa usato da milioni" L_TERM_COMMENT="// il mio profilo personale" L_JSON_STATUS="sempre in apprendimento"; L_JSON_GOAL="pubblicare qualcosa di figo" L_SKILLS_KW="// competenze"; L_SKILLS_H2="Cosa so fare()" L_PROJ_KW="// progetti"; L_PROJ_H2="Cosa ho costruito()" L_PROJ_DEMO="demo"; L_PROJ_CODE="codice" L_P1_NAME="Gioco Snake"; L_P1_DESC="Il classico Snake in Python con Pygame da zero. Velocità crescente, salvataggio del record e pixel art." L_P2_NAME="Dashboard Meteo"; L_P2_DESC="App meteo in tempo reale con API OpenWeather e icone animate. In JavaScript puro." L_P3_NAME="Bot Discord"; L_P3_DESC="Bot multifunzione con moderazione, musica, quiz e comandi personalizzati." L_P4_NAME="App Note Markdown"; L_P4_DESC="App note minimalista con anteprima live, storage locale e modalità scura/chiara. Senza framework." L_P5_NAME="Budget Tracker (CLI)"; L_P5_DESC="Strumento CLI per tracciare entrate e uscite, esportare in CSV e salvare su SQLite." L_P6_NAME="Questo sito"; L_P6_DESC="Portfolio personale su VPS con Nginx e HTTPS. HTML & CSS puro senza framework — veloce e leggero." L_CONTACT_KW="// contatti"; L_CONTACT_H2="Di' ciao()" L_CONTACT_P="Hai un'idea di progetto interessante? Vuoi collaborare o semplicemente parlare di codice? La mia mail è sempre aperta — rispondo di solito in giornata." L_FOOTER_LEFT="Progettato & costruito da ${DEV_NAME} — ${DEV_AGE} anni" L_FOOTER_RIGHT="© $(date +%Y) · Fatto con ☕ & 🐛" ;; id) L_HTML_LANG="id"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Portofolio Pengembang" L_NAV_ABOUT="Tentang"; L_NAV_SKILLS="Keahlian"; L_NAV_PROJECTS="Proyek"; L_NAV_CONTACT="Kontak" L_HERO_GREETING="Halo 👋 Saya" L_HERO_AGE_LINE="[ pengembang ${DEV_AGE} tahun & kreator ]" L_HERO_BTN1="▶ Lihat Proyek"; L_HERO_BTN2="✉ Hubungi Saya" L_ABOUT_KW="// tentang saya"; L_ABOUT_H2="Sedikit tentang about_me" L_ABOUT_P1="Halo! Saya ${DEV_NAME}, seorang pengembang otodidak berusia ${DEV_AGE} tahun yang suka mengubah ide menjadi sesuatu yang nyata di internet." L_ABOUT_P2="Saya mulai coding karena ingin membuat game sendiri. Sekarang tidak bisa berhenti — selalu ada hal baru untuk dipelajari dan setiap bug yang diperbaiki terasa seperti kemenangan kecil." L_ABOUT_P3="Ketika tidak coding, saya membaca tentang cara kerja komputer, menonton video teknologi, atau berusaha memecahkan rekor saya sendiri." L_FACTS_KW="// fakta seru" L_FACT1="☕ Berjalan dengan camilan dan rasa ingin tahu"; L_FACT2="🌙 Kode terbaik ditulis setelah tengah malam" L_FACT3="🐛 Bug hanyalah fitur yang disalahpahami"; L_FACT4="🌍 Impian membangun sesuatu yang digunakan jutaan orang" L_TERM_COMMENT="// profil pribadi saya" L_JSON_STATUS="selalu belajar"; L_JSON_GOAL="kirimkan sesuatu yang keren" L_SKILLS_KW="// keahlian"; L_SKILLS_H2="Yang saya bisa()" L_PROJ_KW="// proyek"; L_PROJ_H2="Yang saya bangun()" L_PROJ_DEMO="demo"; L_PROJ_CODE="kode" L_P1_NAME="Game Ular"; L_P1_DESC="Game ular klasik dari nol pakai Python dan Pygame. Kecepatan naik seiring skor, ada simpan rekor dan pixel art." L_P2_NAME="Dasbor Cuaca"; L_P2_DESC="Aplikasi cuaca real-time dengan API OpenWeather dan ikon animasi. Dibuat dengan JavaScript murni." L_P3_NAME="Bot Discord"; L_P3_DESC="Bot serbaguna dengan moderasi, putar musik, kuis, dan perintah kustom." L_P4_NAME="Aplikasi Catatan Markdown"; L_P4_DESC="Aplikasi catatan minimalis dengan pratinjau langsung, penyimpanan lokal, dan mode gelap/terang. Tanpa framework." L_P5_NAME="Pelacak Anggaran (CLI)"; L_P5_DESC="Alat CLI untuk melacak pemasukan dan pengeluaran, ekspor CSV, dan penyimpanan SQLite." L_P6_NAME="Situs ini"; L_P6_DESC="Portofolio pribadi di VPS dengan Nginx dan HTTPS. HTML & CSS murni tanpa framework — cepat dan ringan." L_CONTACT_KW="// kontak"; L_CONTACT_H2="Katakan halo()" L_CONTACT_P="Punya ide proyek keren? Ingin berkolaborasi? Atau sekadar ngobrol soal kode? Kotak masuk saya selalu terbuka — biasanya saya balas di hari yang sama." L_FOOTER_LEFT="Dirancang & dibangun oleh ${DEV_NAME} — umur ${DEV_AGE}" L_FOOTER_RIGHT="© $(date +%Y) · Dibuat dengan ☕ & 🐛" ;; vi) L_HTML_LANG="vi"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Trang cá nhân lập trình viên" L_NAV_ABOUT="Giới thiệu"; L_NAV_SKILLS="Kỹ năng"; L_NAV_PROJECTS="Dự án"; L_NAV_CONTACT="Liên hệ" L_HERO_GREETING="Xin chào 👋 Tôi là" L_HERO_AGE_LINE="[ lập trình viên ${DEV_AGE} tuổi & người sáng tạo ]" L_HERO_BTN1="▶ Xem dự án"; L_HERO_BTN2="✉ Liên hệ tôi" L_ABOUT_KW="// giới thiệu"; L_ABOUT_H2="Đôi điều về about_me" L_ABOUT_P1="Xin chào! Tôi là ${DEV_NAME}, lập trình viên tự học ${DEV_AGE} tuổi, thích biến ý tưởng thành thứ gì đó thực sự trên internet." L_ABOUT_P2="Tôi bắt đầu học code vì muốn tự làm game. Bây giờ không dừng được nữa — luôn có thứ mới để học và mỗi bug được sửa là một chiến thắng nhỏ." L_ABOUT_P3="Khi không code, tôi đọc về cách máy tính hoạt động, xem video công nghệ hoặc cố gắng phá kỷ lục của bản thân." L_FACTS_KW="// sự thật thú vị" L_FACT1="☕ Chạy bằng đồ ăn vặt và sự tò mò"; L_FACT2="🌙 Code hay nhất được viết sau nửa đêm" L_FACT3="🐛 Bug chỉ là tính năng bị hiểu nhầm"; L_FACT4="🌍 Ước mơ xây dựng thứ gì đó triệu người dùng" L_TERM_COMMENT="// hồ sơ cá nhân" L_JSON_STATUS="luôn học hỏi"; L_JSON_GOAL="ra mắt thứ gì đó thú vị" L_SKILLS_KW="// kỹ năng"; L_SKILLS_H2="Những gì tôi biết()" L_PROJ_KW="// dự án"; L_PROJ_H2="Những gì tôi đã xây dựng()" L_PROJ_DEMO="demo"; L_PROJ_CODE="code" L_P1_NAME="Game Rắn"; L_P1_DESC="Game rắn cổ điển từ đầu bằng Python và Pygame. Tốc độ tăng theo điểm, lưu kỷ lục và đồ họa pixel art." L_P2_NAME="Bảng Thời Tiết"; L_P2_DESC="Ứng dụng thời tiết thực tế với API OpenWeather và biểu tượng hoạt hình. Viết bằng JavaScript thuần." L_P3_NAME="Bot Discord"; L_P3_DESC="Bot đa năng: kiểm duyệt, phát nhạc, trắc nghiệm và lệnh tùy chỉnh." L_P4_NAME="Ứng dụng Ghi chú Markdown"; L_P4_DESC="Ứng dụng ghi chú tối giản với xem trước trực tiếp, lưu trữ cục bộ và chế độ tối/sáng. Không framework." L_P5_NAME="Quản lý Chi tiêu (CLI)"; L_P5_DESC="Công cụ dòng lệnh theo dõi thu chi, xuất CSV và lưu trữ SQLite." L_P6_NAME="Trang này"; L_P6_DESC="Portfolio cá nhân trên VPS với Nginx và HTTPS. HTML & CSS thuần, không framework — nhanh và nhẹ." L_CONTACT_KW="// liên hệ"; L_CONTACT_H2="Nói xin chào()" L_CONTACT_P="Có ý tưởng dự án thú vị? Muốn hợp tác? Hay chỉ muốn nói chuyện về code? Hộp thư của tôi luôn mở — thường phản hồi trong ngày." L_FOOTER_LEFT="Thiết kế & xây dựng bởi ${DEV_NAME} — ${DEV_AGE} tuổi" L_FOOTER_RIGHT="© $(date +%Y) · Làm bằng ☕ & 🐛" ;; ar) L_HTML_LANG="ar"; L_HTML_DIR=' dir="rtl"' L_BODY_FONT_EXTRA=",'Cairo','Noto Kufi Arabic','Tahoma'" L_TITLE_SUFFIX="— صفحة المطوّر الشخصية" L_NAV_ABOUT="نبذة عني"; L_NAV_SKILLS="المهارات"; L_NAV_PROJECTS="المشاريع"; L_NAV_CONTACT="التواصل" L_HERO_GREETING="مرحباً 👋 أنا" L_HERO_AGE_LINE="[ مطوّر عمره ${DEV_AGE} سنوات & صانع ]" L_HERO_BTN1="▶ استعرض مشاريعي"; L_HERO_BTN2="✉ تواصل معي" L_ABOUT_KW="// نبذة عني"; L_ABOUT_H2="قليل من about_me" L_ABOUT_P1="مرحباً! أنا ${DEV_NAME}، مطوّر يتعلم ذاتياً في عمر ${DEV_AGE} سنوات، أحب تحويل الأفكار إلى أشياء حقيقية على الإنترنت." L_ABOUT_P2="بدأت البرمجة لأنني أردت صنع ألعابي الخاصة. الآن لا أستطيع التوقف — هناك دائماً شيء جديد لتعلّمه، وكل خطأ يُحلّ يبدو إنجازاً صغيراً." L_ABOUT_P3="حين لا أبرمج، أقرأ عن آلية عمل الحواسيب، أشاهد مقاطع التقنية، أو أحاول تجاوز رقمي القياسي." L_FACTS_KW="// حقائق طريفة" L_FACT1="☕ يعمل بالوجبات الخفيفة والفضول"; L_FACT2="🌙 أفضل الأكواد تُكتب بعد منتصف الليل" L_FACT3="🐛 الأخطاء البرمجية مجرد ميزات مسيئة الفهم"; L_FACT4="🌍 حلمي بناء شيء يستخدمه الملايين" L_TERM_COMMENT="// ملفي الشخصي" L_JSON_STATUS="أتعلم دائماً"; L_JSON_GOAL="إطلاق شيء رائع" L_SKILLS_KW="// المهارات"; L_SKILLS_H2="ما أُتقنه ()" L_PROJ_KW="// المشاريع"; L_PROJ_H2="ما بنيته ()" L_PROJ_DEMO="عرض"; L_PROJ_CODE="كود" L_P1_NAME="لعبة الأفعى"; L_P1_DESC="لعبة الأفعى الكلاسيكية مبنية من الصفر بـ Python وPygame. السرعة ترتفع مع النقاط وتحفظ الرقم القياسي." L_P2_NAME="لوحة الطقس"; L_P2_DESC="تطبيق طقس فوري يعرض البيانات من OpenWeather API مع أيقونات متحركة. مبني بـ JavaScript الخالص." L_P3_NAME="بوت Discord"; L_P3_DESC="بوت متعدد الوظائف: إدارة القناة، تشغيل الموسيقى، مسابقات، وأوامر مخصصة." L_P4_NAME="تطبيق ملاحظات Markdown"; L_P4_DESC="تطبيق ملاحظات بسيط مع معاينة فورية وتخزين محلي ووضعَي الإضاءة والظلام. بدون أطر عمل." L_P5_NAME="متتبع الميزانية (CLI)"; L_P5_DESC="أداة سطر أوامر لتتبع الدخل والمصروفات وتصدير CSV وتخزين البيانات في SQLite." L_P6_NAME="هذا الموقع"; L_P6_DESC="محفظة شخصية على خادم VPS مع Nginx وHTTPS. HTML & CSS نقي بلا أطر عمل — سريع وخفيف." L_CONTACT_KW="// التواصل"; L_CONTACT_H2="قل مرحباً()" L_CONTACT_P="لديك فكرة مشروع مثيرة؟ تريد التعاون أو مجرد الحديث عن البرمجة؟ صندوق بريدي مفتوح دائماً — عادةً أرد في نفس اليوم." L_FOOTER_LEFT="صُمِّم وبُني بواسطة ${DEV_NAME} — عمر ${DEV_AGE}" L_FOOTER_RIGHT="© $(date +%Y) · صُنع بـ ☕ & 🐛" ;; *) # English (default) L_HTML_LANG="en"; L_HTML_DIR=""; L_BODY_FONT_EXTRA="" L_TITLE_SUFFIX="— Young Developer" L_NAV_ABOUT="about"; L_NAV_SKILLS="skills"; L_NAV_PROJECTS="projects"; L_NAV_CONTACT="contact" L_HERO_GREETING="Hi there 👋 I'm" L_HERO_AGE_LINE="[ ${DEV_AGE}-year-old developer & builder ]" L_HERO_BTN1="▶ View My Projects"; L_HERO_BTN2="✉ Get in Touch" L_ABOUT_KW="// about me"; L_ABOUT_H2="A little bit about_me" L_ABOUT_P1="Hey! I'm ${DEV_NAME}, a ${DEV_AGE}-year-old self-taught developer who loves turning ideas into real things on the internet." L_ABOUT_P2="I started coding because I wanted to build my own games. Now I can't stop — there's always something new to learn, and every bug fixed feels like a victory." L_ABOUT_P3="When I'm not coding, I'm probably reading about how computers work, watching tech YouTube, or trying to beat my high score." L_FACTS_KW="// fun facts" L_FACT1="☕ Runs on snacks and curiosity"; L_FACT2="🌙 Best code written after midnight" L_FACT3="🐛 Bugs are just features in disguise"; L_FACT4="🌍 Dreams of building something millions use" L_TERM_COMMENT="// my personal readme" L_JSON_STATUS="always learning"; L_JSON_GOAL="ship something cool" L_SKILLS_KW="// skills"; L_SKILLS_H2="What I know()" L_PROJ_KW="// projects"; L_PROJ_H2="Things I've built()" L_PROJ_DEMO="demo"; L_PROJ_CODE="code" L_P1_NAME="Snake Game"; L_P1_DESC="A classic Snake game built from scratch in Python using Pygame. Features increasing speed, high-score saving, and custom pixel art." L_P2_NAME="Weather Dashboard"; L_P2_DESC="Real-time weather app that fetches data from the OpenWeather API and displays forecasts with animated icons. Built with vanilla JS." L_P3_NAME="Discord Bot"; L_P3_DESC="A multi-purpose Discord bot that handles moderation, plays music, runs trivia quizzes, and responds to custom commands." L_P4_NAME="Markdown Notes App"; L_P4_DESC="A clean, minimal notes app with live Markdown preview, local storage, and dark/light mode. No frameworks — just HTML, CSS, JS." L_P5_NAME="Budget Tracker CLI"; L_P5_DESC="Command-line tool for tracking income and expenses. Exports reports to CSV, shows monthly summaries, and stores data in SQLite." L_P6_NAME="This Website"; L_P6_DESC="My personal portfolio — deployed on a VPS with Nginx and HTTPS. Built with pure HTML & CSS, no frameworks. Fast, lightweight, mine." L_CONTACT_KW="// contact"; L_CONTACT_H2="Say hello()" L_CONTACT_P="Got a cool project idea? Want to collaborate? Or just want to talk code? My inbox is always open — I try to reply within a day." L_FOOTER_LEFT="Designed & built by ${DEV_NAME} — age ${DEV_AGE}" L_FOOTER_RIGHT="© $(date +%Y) · Made with ☕ & 🐛" ;; esac } # ── Interactive Config ───────────────────────── echo -e "${YELLOW}>>> Please answer a few setup questions${NC}\n" read -rp "Domain name (leave blank for IP-only access): " DOMAIN DOMAIN=$(echo "$DOMAIN" | tr -d '[:space:]') LE_EMAIL="" if [[ -n "$DOMAIN" ]]; then read -rp "Email for Let's Encrypt notifications: " LE_EMAIL [[ -z "$LE_EMAIL" ]] && err "An email is required for Let's Encrypt" CERT_MODE="letsencrypt" else read -rp "Email for SSL notifications (optional, press Enter to skip): " LE_EMAIL CERT_MODE="selfsigned" info "No domain — will attempt Let's Encrypt IP certificate (no browser warning)" info "Requires port 80 publicly accessible. Falls back to self-signed if unavailable." fi read -rp "Your name [default: ${L_DEFAULT_NAME}]: " DEV_NAME DEV_NAME="${DEV_NAME:-${L_DEFAULT_NAME}}" while true; do read -rp "Your age [default: 14]: " DEV_AGE DEV_AGE="${DEV_AGE:-14}" [[ "$DEV_AGE" =~ ^[0-9]+$ ]] && break warn "Age must be a number. Please try again." done read -rp "One-line bio [default: ${L_DEFAULT_BIO}]: " DEV_BIO DEV_BIO="${DEV_BIO:-${L_DEFAULT_BIO}}" read -rp "GitHub username (optional, leave blank to skip): " GITHUB_USER GITHUB_USER="${GITHUB_USER:-}" read -rp "Contact email [default: hello@example.com]: " CONTACT_EMAIL CONTACT_EMAIL="${CONTACT_EMAIL:-hello@example.com}" read -rp "Website root directory [default: /var/www/html]: " WEB_ROOT WEB_ROOT="${WEB_ROOT:-/var/www/html}" read -rp "Enable firewall rules? (y/N): " ENABLE_UFW ENABLE_UFW="${ENABLE_UFW:-n}" echo "" info "Configuration summary:" echo " Certificate : $CERT_MODE" [[ -n "$DOMAIN" ]] && echo " Domain : $DOMAIN" echo " Developer : $DEV_NAME (age $DEV_AGE)" [[ -n "$GITHUB_USER" ]] && echo " GitHub : $GITHUB_USER" echo " Contact : $CONTACT_EMAIL" echo " Web root : $WEB_ROOT" echo "" # ── BaoTa Panel Detection ───────────────────── if [[ -f /www/server/panel/BT-Panel ]] || \ [[ -x /www/server/nginx/sbin/nginx ]] || \ [[ -x /www/server/apache/bin/httpd ]]; then BT_MODE=true # Detect which web server BaoTa is running if [[ -x /www/server/nginx/sbin/nginx ]] && \ /www/server/nginx/sbin/nginx -v &>/dev/null 2>&1; then BT_WEB="nginx" WEB_BIN="/www/server/nginx/sbin/nginx" NGINX_BIN="$WEB_BIN" NGINX_CONF_DIR="/www/server/panel/vhost/nginx" info "BaoTa Panel detected — web server: nginx" elif [[ -x /www/server/apache/bin/httpd ]]; then BT_WEB="apache" WEB_BIN="/www/server/apache/bin/apachectl" NGINX_BIN="$WEB_BIN" # keep NGINX_BIN defined to avoid nounset errors NGINX_CONF_DIR="/www/server/panel/vhost/apache" info "BaoTa Panel detected — web server: apache" else err "BaoTa detected but neither nginx nor apache binary found" fi # BaoTa runs its web server as 'www', not the OS default (www-data / nginx) NGINX_USER="www" # Warn about existing BaoTa sites if [[ ${#BT_EXISTING_SITES[@]} -gt 0 ]]; then echo "" warn "BaoTa Panel has existing site config(s):" for s in "${BT_EXISTING_SITES[@]}"; do echo " $s"; done warn "This script will add a NEW config (website.conf)." warn "Existing sites will NOT be touched — but check for port 80/443 conflicts." echo "" read -rp "Understood — continue anyway? (y/N): " BT_CONFIRM [[ "${BT_CONFIRM:-n}" =~ ^[Yy]$ ]] || { info "Cancelled."; exit 0; } fi else BT_MODE=false BT_WEB="" WEB_BIN="nginx" NGINX_BIN="nginx" NGINX_CONF_DIR="/etc/nginx/sites-available" info "No BaoTa Panel — standard nginx setup" fi read -rp "Start installation? (Y/n): " CONFIRM [[ "${CONFIRM:-y}" =~ ^[Nn]$ ]] && { info "Cancelled."; exit 0; } # ── Install Dependencies ─────────────────────── # EPEL must be installed before certbot on RHEL-based systems if [[ "$NEED_EPEL" == true ]]; then log "Enabling EPEL repository..." yum install -y -q epel-release 2>/dev/null || true fi log "Updating package lists..." $PKG_UPDATE log "Installing dependencies..." $PKG_INSTALL $CURL_PKG $OPENSSL_PKG if [[ "$BT_MODE" == false ]]; then log "Installing Nginx..." $PKG_INSTALL $NGINX_PKG fi if [[ "$CERT_MODE" == "letsencrypt" ]]; then log "Installing Certbot..." $PKG_INSTALL $CERTBOT_PKG # certbot-nginx plugin only needed in standard mode [[ "$BT_MODE" == false ]] && $PKG_INSTALL $CERTBOT_NGINX_PKG fi # ── Pre-compute name parts ──────────────────── # Splits "First Last" → two spans; single-word names get an empty last span DEV_FIRST="${DEV_NAME%% *}" DEV_LAST="${DEV_NAME#* }" [[ "$DEV_FIRST" == "$DEV_LAST" ]] && DEV_LAST="" # ── Build locale content (now that DEV_NAME/DEV_AGE/DEV_BIO are known) ─ build_locale_content # ── Safe-init cert vars (avoids nounset errors) ─ SSL_CERT="" SSL_KEY="" # ── Build GitHub HTML block (includes wrapper div; empty string when no username) ─ GH_SVG='' if [[ -n "$GITHUB_USER" ]]; then GITHUB_HTML="
" else GITHUB_HTML="" fi # ── Website Content ──────────────────────────── log "Creating web root: $WEB_ROOT" mkdir -p "$WEB_ROOT" log "Generating personal homepage..." cat > "$WEB_ROOT/index.html" <${L_HERO_GREETING}
${L_HERO_AGE_LINE}
${DEV_BIO}
${GITHUB_HTML}${L_ABOUT_P1}
${L_ABOUT_P2}
${L_ABOUT_P3}