"""Translation catalog for deterministic system and tool responses."""

from __future__ import annotations

from typing import Any

from kabot.i18n.locale import detect_locale

_KEY_ALIASES: dict[str, str] = {
    # Legacy keys kept for backward compatibility.
    "weather_need_location": "weather.need_location",
    "cron_remove_need_selector": "cron.remove.need_selector",
    "cron_update_need_selector": "cron.update.need_selector",
    "cron_update_incomplete": "cron.update.incomplete",
    "cron_time_unclear": "cron.time_unclear",
    "cycle_created": "cron.cycle_created",
}

_CATALOG: dict[str, dict[str, str]] = {
    "en": {
        "web_fetch.validation_error": "Request blocked: {error}",
        "web_fetch.timeout": "Request timed out after {seconds}s.",
        "web_fetch.request_error": "Request failed ({error_type}): {error}",
        "knowledge.file_not_found": "File not found: {path}",
        "knowledge.extract_failed": "Failed to extract readable text: {error}",
        "knowledge.no_readable_text": "No readable text found in this document.",
        "knowledge.save_failed": "Failed to save knowledge to long-term memory: {error}",
        "memory.manager_unavailable": "Memory manager is not available.",
        "memory.save_failed": "Failed to save memory: {error}",
        "memory.get_failed": "Failed to retrieve memory: {error}",
        "memory.save_success": "[OK] {category} saved: {preview}",
        "memory.save_category_failed": "[ERROR] Failed to save {category}",
        "memory.search.no_results": "No memories found for: '{query}'.",
        "memory.search.hint_categories": "HINT: Try searching with categories: {categories}",
        "memory.search.hint_keywords": "HINT: Check your keywords or use 'list_reminders' for schedules.",
        "memory.reminders.none": "No active reminders",
        "memory.reminders.header": "[LIST] Active reminders:\n\n{items}",
        "filesystem.file_not_found": "File not found: {path}",
        "filesystem.need_path": "Please provide the file path to read (example: config.json or C:\\path\\to\\file.json).",
        "filesystem.not_file": "Not a file: {path}",
        "filesystem.not_directory": "Not a directory: {path}",
        "filesystem.directory_not_found": "Directory not found: {path}",
        "filesystem.permission_denied": "Permission denied: {error}",
        "filesystem.read_error": "Failed to read file: {error}",
        "filesystem.write_error": "Failed to write file: {error}",
        "filesystem.edit_error": "Failed to edit file: {error}",
        "filesystem.list_error": "Failed to list directory: {error}",
        "filesystem.old_text_not_found_exact": "old_text was not found exactly. A similar string exists with different leading/trailing whitespace. Please match it exactly.",
        "filesystem.old_text_not_found": "old_text was not found in {path}. It must match exactly, including indentation.\n\nFILE CONTENT PREVIEW:\n---\n{snippet}\n---\nHINT: Use read_file to get the full content and copy the exact block you want to replace.",
        "filesystem.old_text_ambiguous": "old_text appears {count} times. Please provide more context so it is unique.",
        "filesystem.directory_empty": "Directory {path} is empty.",
        "message.no_target": "No target channel/chat specified.",
        "message.not_configured": "Message sending is not configured.",
        "message.send_error": "Failed to send message: {error}",
        "speedtest.failed": "Failed to perform speedtest: {error}",
        "speedtest.missing_dependency": "speedtest-cli is not installed. Please run 'pip install speedtest-cli' on the host.",
        "speedtest.error": "Speedtest failed: {error}",
        "weather.need_location": "I need a location to check the weather. Example: check weather in Cilacap.",
        "weather.fetch_failed": "I couldn't fetch weather for {location}. Please try another city name.",
        "weather.error": "Weather lookup failed: {error}",
        "image_gen.provider_missing": "Image generation isn't available yet because the image provider API key is not configured. Set up OpenAI or Gemini first, for example with 'kabot auth login'.",
        "image_gen.provider_unavailable": "Image generation with {provider} isn't available in this runtime yet. Configure a supported image provider first.",
        "image_gen.error": "Image generation failed: {error}",
        "stock.need_symbol": "Please include a stock ticker symbol (example: BBRI.JK, BBCA.JK, BMRI.JK, AAPL). Use web_search if you're unsure.",
        "stock.fetch_failed": "Could not fetch data for {symbol}. Please verify the ticker symbol using web search.",
        "stock.error": "Failed to fetch stock data: {error}",
        "stock.ambiguous_symbol": "I found multiple listings for '{company}'. Which market do you want?\n{options}\nPlease reply with one ticker symbol (for example: {example}).",
        "crypto.need_id": "Please provide exact CoinGecko IDs (example: bitcoin,ethereum). Use web_search if you're unsure.",
        "crypto.fetch_failed": "Error: Could not fetch crypto data for {coin}. Please retry and use web_search if needed to verify the CoinGecko ID.",
        "crypto.not_found": "Error: Cryptocurrency '{coin}' was not found. Please use exact CoinGecko IDs and use web_search if unsure.",
        "crypto.partial_missing": "Could not fetch crypto data for: {coins}.",
        "crypto.error": "Error fetching crypto data: {error}",
        "web_search.need_query": "Please provide a search query.",
        "web_search.need_topic": "Please provide the topic or keywords you want me to search.",
        "web_search.no_results": "I couldn't find relevant web results for: {query}.",
        "meta.missing_access_token": "Meta integration is missing access token. Provide `access_token` in the tool call, set THREADS_ACCESS_TOKEN/KABOT_META_ACCESS_TOKEN, or set integrations.meta.access_token in ~/.kabot/config.json.",
        "meta.unsupported_action": "Unsupported Meta action: '{action}'.",
        "meta.error": "Meta action failed: {error}",
        "cron.remove.need_selector": "To remove a schedule, provide `group_id` or schedule title. First run: `list reminder` to see groups.",
        "cron.update.need_selector": "To edit a schedule, provide `group_id` or schedule title. First run: `list reminder` to see groups.",
        "cron.update.incomplete": "Edit format is incomplete. Example: `update schedule grp_shift_a every 12 hours` or `rename grp_shift_a to Shift Team A`.",
        "cron.time_unclear": "I could not determine the reminder time. Example: remind me in 2 minutes, every day at 09:00 standup, or every 4 hours drink water.",
        "cron.cycle_created": "Created cycle '{title}' (group_id: {group_id}) with {job_count} jobs; repeats every {period_days} days.",
        "cron.add.error_message_required": "Error: message is required to create a schedule.",
        "cron.add.error_no_session_context": "Error: no session context available (channel/chat_id).",
        "cron.add.error_schedule_invalid": "Error: invalid schedule ({error}).",
        "cron.add.error_schedule_required": "Error: either at_time, every_seconds, or cron_expr is required.",
        "cron.add.error_policy_violation": "Error: schedule policy rejected this request ({error}).",
        "cron.add.created": "Created job '{name}' (id: {job_id})",
        "cron.add.created_group": "Created job '{name}' (id: {job_id}, group: {group_id}, title: {title})",
        "cron.list.empty": "No scheduled jobs.",
        "cron.list.header": "Scheduled jobs:",
        "cron.list_groups.empty": "No grouped schedules.",
        "cron.list_groups.header": "Schedule groups:",
        "cron.remove.error_job_id_required": "Error: job_id is required for remove.",
        "cron.remove.ok": "Removed job {job_id}",
        "cron.remove.not_found": "Job {job_id} not found",
        "cron.remove_group.error_not_found": "Error: group not found. Provide a valid group_id or title.",
        "cron.remove_group.ok": "Removed group '{title}' ({group_id}) with {removed} jobs",
        "cron.update.error_job_id_required": "Error: job_id is required for update.",
        "cron.update.ok": "Updated job '{name}' ({job_id})",
        "cron.update.not_found": "Job {job_id} not found",
        "cron.update_group.error_not_found": "Error: group not found. Provide a valid group_id or title.",
        "cron.update_group.error_schedule_invalid": "Error: invalid schedule ({error}).",
        "cron.update_group.error_nothing_to_update": "Error: nothing to update. Provide message, schedule, or new_title.",
        "cron.update_group.ok": "Updated group '{title}' ({group_id}) with {updated} jobs",
        "cron.run.error_job_id_required": "Error: job_id is required for run.",
        "cron.run.ok": "Executed job {job_id}",
        "cron.run.not_found": "Job {job_id} not found or disabled",
        "cron.runs.error_job_id_required": "Error: job_id is required for runs.",
        "cron.runs.empty": "No run history for job {job_id}",
        "cron.runs.header": "Run history for {job_id}:",
        "cron.status.running": "Running",
        "cron.status.stopped": "Stopped",
        "cron.status.summary": "Cron Service: {service_state}\nJobs: {jobs}\nNext wake: {next_wake}",
        "update.check.available": "Update available: {latest_version} (current: {current_version})",
        "update.check.up_to_date": "You're running the latest version: {current_version}",
        "update.check.commits_behind": "Your installation is {commits_behind} commits behind the latest release.",
        "update.check.error": "Could not check for updates: {error}",
        "update.install.success": "Successfully updated from {old_version} to {new_version}. Restart required.",
        "update.install.restart_confirm": "Update complete. Restart Kabot now?",
        "update.install.dirty_tree": "Cannot update: uncommitted changes in working tree. Please commit or stash first.",
        "update.install.failed": "Update failed: {error}",
        "runtime.status.queued": "Queued. Preparing your request...",
        "runtime.status.queued_merged": "Merged {count} queued message(s).",
        "runtime.status.thinking": "Processing your request, please wait...",
        "runtime.status.discovery": "Understanding the skill request and clarifying requirements...",
        "runtime.status.planning": "Drafting the skill plan and waiting for approval...",
        "runtime.status.approved": "Plan approved. Preparing execution...",
        "runtime.status.executing": "Implementing the approved skill plan...",
        "runtime.status.verified": "Implementation verified.",
        "runtime.status.tool": "Running the required tools, please wait...",
        "runtime.status.done": "Done.",
        "runtime.status.error": "An error occurred while processing your request.",
        "runtime.abort.ack": "Okay, I stopped this action and cleared pending follow-ups.",
    },
    "id": {
        "web_fetch.validation_error": "Permintaan diblokir: {error}",
        "web_fetch.timeout": "Permintaan timeout setelah {seconds} detik.",
        "web_fetch.request_error": "Permintaan gagal ({error_type}): {error}",
        "knowledge.file_not_found": "File tidak ditemukan: {path}",
        "knowledge.extract_failed": "Gagal mengekstrak teks yang bisa dibaca: {error}",
        "knowledge.no_readable_text": "Tidak ada teks yang bisa dibaca di dokumen ini.",
        "knowledge.save_failed": "Gagal menyimpan pengetahuan ke memori jangka panjang: {error}",
        "memory.manager_unavailable": "Memory manager tidak tersedia.",
        "memory.save_failed": "Gagal menyimpan memori: {error}",
        "memory.get_failed": "Gagal mengambil memori: {error}",
        "memory.save_success": "[OK] {category} tersimpan: {preview}",
        "memory.save_category_failed": "[ERROR] Gagal menyimpan {category}",
        "memory.search.no_results": "Memori untuk '{query}' belum ditemukan.",
        "memory.search.hint_categories": "PETUNJUK: Coba cari berdasarkan kategori: {categories}",
        "memory.search.hint_keywords": "PETUNJUK: Cek kata kunci kamu atau pakai 'list_reminders' untuk jadwal.",
        "memory.reminders.none": "Tidak ada pengingat aktif",
        "memory.reminders.header": "[LIST] Pengingat aktif:\n\n{items}",
        "filesystem.file_not_found": "File tidak ditemukan: {path}",
        "filesystem.need_path": "Tolong sertakan path file yang ingin dibaca (contoh: config.json atau C:\\path\\to\\file.json).",
        "filesystem.not_file": "Path ini bukan file: {path}",
        "filesystem.not_directory": "Path ini bukan folder: {path}",
        "filesystem.directory_not_found": "Folder tidak ditemukan: {path}",
        "filesystem.permission_denied": "Akses ditolak: {error}",
        "filesystem.read_error": "Gagal membaca file: {error}",
        "filesystem.write_error": "Gagal menulis file: {error}",
        "filesystem.edit_error": "Gagal mengedit file: {error}",
        "filesystem.list_error": "Gagal menampilkan isi folder: {error}",
        "filesystem.old_text_not_found_exact": "old_text tidak ketemu persis. Ada teks mirip tapi beda spasi di depan/belakang. Pastikan sama persis.",
        "filesystem.old_text_not_found": "old_text tidak ditemukan di {path}. Harus sama persis, termasuk indentasi.\n\nPREVIEW FILE:\n---\n{snippet}\n---\nHINT: pakai read_file untuk ambil isi penuh lalu salin blok yang mau diganti.",
        "filesystem.old_text_ambiguous": "old_text muncul {count} kali. Tolong tambahkan konteks supaya unik.",
        "filesystem.directory_empty": "Folder {path} kosong.",
        "message.no_target": "Channel/chat tujuan belum ditentukan.",
        "message.not_configured": "Pengiriman pesan belum dikonfigurasi.",
        "message.send_error": "Gagal mengirim pesan: {error}",
        "speedtest.failed": "Gagal menjalankan speedtest: {error}",
        "speedtest.missing_dependency": "speedtest-cli belum terpasang. Jalankan 'pip install speedtest-cli' di host.",
        "speedtest.error": "Speedtest gagal: {error}",
        "weather.need_location": "Saya butuh lokasi untuk cek cuaca. Contoh: cek suhu Cilacap.",
        "weather.fetch_failed": "Saya belum bisa ambil cuaca untuk {location}. Coba nama kota lain ya.",
        "weather.error": "Cek cuaca gagal: {error}",
        "image_gen.provider_missing": "Maaf, generate image belum bisa karena API key provider gambar belum dikonfigurasi. Siapkan OpenAI atau Gemini dulu, misalnya lewat 'kabot auth login'.",
        "image_gen.provider_unavailable": "Generate image lewat {provider} belum tersedia di runtime ini. Siapkan provider gambar yang didukung dulu.",
        "image_gen.error": "Generate image gagal: {error}",
        "stock.need_symbol": "Tolong sertakan kode ticker saham (contoh: BBRI.JK, BBCA.JK, BMRI.JK, AAPL). Kalau ragu, pakai web_search dulu.",
        "stock.fetch_failed": "Data untuk {symbol} belum bisa diambil. Tolong cek lagi kode tickernya via web_search.",
        "stock.error": "Gagal mengambil data saham: {error}",
        "stock.ambiguous_symbol": "Saya menemukan beberapa pilihan ticker untuk '{company}'. Mau pilih market yang mana?\n{options}\nBalas dengan satu kode ticker (contoh: {example}).",
        "crypto.need_id": "Tolong kirim ID CoinGecko yang tepat (contoh: bitcoin,ethereum). Kalau ragu, pakai web_search dulu.",
        "crypto.fetch_failed": "Error: data kripto untuk {coin} belum bisa diambil. Coba lagi dan pakai web_search jika perlu verifikasi ID CoinGecko.",
        "crypto.not_found": "Error: kripto '{coin}' tidak ditemukan. Gunakan ID CoinGecko yang tepat dan pakai web_search jika ragu.",
        "crypto.partial_missing": "Sebagian data kripto belum bisa diambil: {coins}.",
        "crypto.error": "Error mengambil data kripto: {error}",
        "web_search.need_query": "Tolong kirim kata kunci pencarian dulu.",
        "web_search.need_topic": "Tolong kirim topik atau kata kunci yang mau saya cari.",
        "web_search.no_results": "Saya belum menemukan hasil web yang relevan untuk: {query}.",
        "meta.missing_access_token": "Integrasi Meta belum punya access token. Isi `access_token` di tool call, set THREADS_ACCESS_TOKEN/KABOT_META_ACCESS_TOKEN, atau set integrations.meta.access_token di ~/.kabot/config.json.",
        "meta.unsupported_action": "Aksi Meta '{action}' belum didukung.",
        "meta.error": "Aksi Meta gagal: {error}",
        "cron.remove.need_selector": "Untuk hapus jadwal, sebutkan `group_id` atau judul jadwal. Gunakan dulu: `list reminder` untuk melihat daftar grup.",
        "cron.update.need_selector": "Untuk edit jadwal, sebutkan `group_id` atau judul jadwal. Gunakan dulu: `list reminder` untuk melihat daftar grup.",
        "cron.update.incomplete": "Format edit belum lengkap. Contoh: `ubah jadwal grp_shift_a tiap 12 jam` atau `ubah judul grp_shift_a jadi Shift Team A`.",
        "cron.time_unclear": "Saya belum bisa memastikan waktu pengingat. Contoh: ingatkan 2 menit lagi makan, setiap hari jam 09:00 standup, atau tiap 4 jam minum air.",
        "cron.cycle_created": "Siklus '{title}' berhasil dibuat (group_id: {group_id}) dengan {job_count} job; berulang tiap {period_days} hari.",
        "cron.add.error_message_required": "Error: pesan wajib diisi untuk membuat jadwal.",
        "cron.add.error_no_session_context": "Error: konteks sesi tidak tersedia (channel/chat_id).",
        "cron.add.error_schedule_invalid": "Error: jadwal tidak valid ({error}).",
        "cron.add.error_schedule_required": "Error: isi salah satu at_time, every_seconds, atau cron_expr.",
        "cron.add.error_policy_violation": "Error: kebijakan scheduler menolak permintaan ini ({error}).",
        "cron.add.created": "Berhasil membuat job '{name}' (id: {job_id})",
        "cron.add.created_group": "Berhasil membuat job '{name}' (id: {job_id}, grup: {group_id}, judul: {title})",
        "cron.list.empty": "Tidak ada job terjadwal.",
        "cron.list.header": "Daftar job:",
        "cron.list_groups.empty": "Tidak ada grup jadwal.",
        "cron.list_groups.header": "Grup jadwal:",
        "cron.remove.error_job_id_required": "Error: job_id wajib untuk hapus.",
        "cron.remove.ok": "Berhasil menghapus job {job_id}",
        "cron.remove.not_found": "Job {job_id} tidak ditemukan",
        "cron.remove_group.error_not_found": "Error: grup tidak ditemukan. Berikan group_id atau judul yang valid.",
        "cron.remove_group.ok": "Berhasil menghapus grup '{title}' ({group_id}) dengan {removed} job",
        "cron.update.error_job_id_required": "Error: job_id wajib untuk edit.",
        "cron.update.ok": "Berhasil mengubah job '{name}' ({job_id})",
        "cron.update.not_found": "Job {job_id} tidak ditemukan",
        "cron.update_group.error_not_found": "Error: grup tidak ditemukan. Berikan group_id atau judul yang valid.",
        "cron.update_group.error_schedule_invalid": "Error: jadwal tidak valid ({error}).",
        "cron.update_group.error_nothing_to_update": "Error: tidak ada perubahan. Berikan message, schedule, atau new_title.",
        "cron.update_group.ok": "Berhasil mengubah grup '{title}' ({group_id}) dengan {updated} job",
        "cron.run.error_job_id_required": "Error: job_id wajib untuk run.",
        "cron.run.ok": "Berhasil menjalankan job {job_id}",
        "cron.run.not_found": "Job {job_id} tidak ditemukan atau nonaktif",
        "cron.runs.error_job_id_required": "Error: job_id wajib untuk melihat riwayat run.",
        "cron.runs.empty": "Belum ada riwayat run untuk job {job_id}",
        "cron.runs.header": "Riwayat run untuk {job_id}:",
        "cron.status.running": "Aktif",
        "cron.status.stopped": "Berhenti",
        "cron.status.summary": "Layanan Cron: {service_state}\nJob: {jobs}\nBangun berikutnya: {next_wake}",
        "update.check.available": "Update tersedia: {latest_version} (saat ini: {current_version})",
        "update.check.up_to_date": "Anda sudah menggunakan versi terbaru: {current_version}",
        "update.check.commits_behind": "Instalasi Anda tertinggal {commits_behind} commit dari rilis terbaru.",
        "update.check.error": "Tidak dapat memeriksa update: {error}",
        "update.install.success": "Berhasil update dari {old_version} ke {new_version}. Restart diperlukan.",
        "update.install.restart_confirm": "Update selesai. Restart Kabot sekarang?",
        "update.install.dirty_tree": "Tidak dapat update: ada perubahan yang belum di-commit. Silakan commit atau stash terlebih dahulu.",
        "update.install.failed": "Update gagal: {error}",
        "runtime.status.queued": "Antrian diterima. Menyiapkan permintaan kamu...",
        "runtime.status.queued_merged": "Menggabungkan {count} pesan antrean sebelumnya.",
        "runtime.status.thinking": "Sedang memproses permintaan kamu, mohon tunggu sebentar...",
        "runtime.status.discovery": "Sedang memahami kebutuhan skill dan merapikan requirement dulu...",
        "runtime.status.planning": "Sedang menyusun plan skill dan menunggu persetujuan...",
        "runtime.status.approved": "Plan sudah disetujui. Menyiapkan eksekusi...",
        "runtime.status.executing": "Sedang mengerjakan plan skill yang sudah disetujui...",
        "runtime.status.verified": "Implementasi sudah diverifikasi.",
        "runtime.status.tool": "Sedang menjalankan tools yang dibutuhkan, mohon tunggu sebentar...",
        "runtime.status.done": "Selesai.",
        "runtime.status.error": "Terjadi kendala saat memproses permintaan kamu.",
        "runtime.abort.ack": "Siap, saya hentikan aksi ini dan membersihkan follow-up yang tertunda.",
    },
    "ms": {
        "web_fetch.validation_error": "Permintaan disekat: {error}",
        "web_fetch.timeout": "Permintaan tamat masa selepas {seconds} saat.",
        "web_fetch.request_error": "Permintaan gagal ({error_type}): {error}",
        "knowledge.file_not_found": "Fail tidak dijumpai: {path}",
        "knowledge.extract_failed": "Gagal mengekstrak teks yang boleh dibaca: {error}",
        "knowledge.no_readable_text": "Tiada teks yang boleh dibaca dalam dokumen ini.",
        "knowledge.save_failed": "Gagal simpan pengetahuan ke memori jangka panjang: {error}",
        "memory.manager_unavailable": "Pengurus memori tidak tersedia.",
        "memory.save_failed": "Gagal simpan memori: {error}",
        "memory.get_failed": "Gagal mendapatkan memori: {error}",
        "memory.save_success": "[OK] {category} disimpan: {preview}",
        "memory.save_category_failed": "[ERROR] Gagal simpan {category}",
        "memory.search.no_results": "Tiada memori ditemui untuk: '{query}'.",
        "memory.search.hint_categories": "PETUNJUK: Cuba cari ikut kategori: {categories}",
        "memory.search.hint_keywords": "PETUNJUK: Semak kata kunci anda atau guna 'list_reminders' untuk jadual.",
        "memory.reminders.none": "Tiada peringatan aktif",
        "memory.reminders.header": "[LIST] Peringatan aktif:\n\n{items}",
        "filesystem.file_not_found": "Fail tidak dijumpai: {path}",
        "filesystem.need_path": "Sila sertakan laluan fail yang ingin dibaca (contoh: config.json atau C:\\path\\to\\file.json).",
        "filesystem.not_file": "Laluan ini bukan fail: {path}",
        "filesystem.not_directory": "Laluan ini bukan folder: {path}",
        "filesystem.directory_not_found": "Folder tidak dijumpai: {path}",
        "filesystem.permission_denied": "Akses ditolak: {error}",
        "filesystem.read_error": "Gagal membaca fail: {error}",
        "filesystem.write_error": "Gagal menulis fail: {error}",
        "filesystem.edit_error": "Gagal mengedit fail: {error}",
        "filesystem.list_error": "Gagal memaparkan kandungan folder: {error}",
        "filesystem.old_text_not_found_exact": "old_text tidak dijumpai tepat. Terdapat teks serupa tetapi ruang depan/belakang berbeza. Sila padankan tepat.",
        "filesystem.old_text_not_found": "old_text tidak dijumpai dalam {path}. Ia mesti padan tepat termasuk indentasi.\n\nPRATONTON FAIL:\n---\n{snippet}\n---\nHINT: guna read_file untuk ambil kandungan penuh dan salin blok yang tepat.",
        "filesystem.old_text_ambiguous": "old_text muncul {count} kali. Sila beri konteks tambahan supaya unik.",
        "filesystem.directory_empty": "Folder {path} kosong.",
        "message.no_target": "Channel/chat sasaran belum ditetapkan.",
        "message.not_configured": "Penghantaran mesej belum dikonfigurasi.",
        "message.send_error": "Gagal menghantar mesej: {error}",
        "speedtest.failed": "Gagal jalankan speedtest: {error}",
        "speedtest.missing_dependency": "speedtest-cli belum dipasang. Jalankan 'pip install speedtest-cli' pada hos.",
        "speedtest.error": "Speedtest gagal: {error}",
        "weather.need_location": "Saya perlukan lokasi untuk semak cuaca. Contoh: semak suhu di Kuala Lumpur.",
        "weather.fetch_failed": "Saya belum dapat ambil cuaca untuk {location}. Cuba nama bandar lain.",
        "weather.error": "Semakan cuaca gagal: {error}",
        "image_gen.provider_missing": "Maaf, jana imej belum boleh kerana API key penyedia imej belum dikonfigurasi. Sediakan OpenAI atau Gemini dahulu, contohnya melalui 'kabot auth login'.",
        "image_gen.provider_unavailable": "Jana imej melalui {provider} belum tersedia pada runtime ini. Sediakan penyedia imej yang disokong dahulu.",
        "image_gen.error": "Jana imej gagal: {error}",
        "stock.need_symbol": "Sila sertakan simbol ticker saham (contoh: BBRI.JK, BBCA.JK, BMRI.JK, AAPL). Jika ragu, guna web_search dahulu.",
        "stock.fetch_failed": "Data untuk {symbol} belum dapat diambil. Sila semak semula ticker melalui web_search.",
        "stock.error": "Gagal ambil data saham: {error}",
        "stock.ambiguous_symbol": "Saya jumpa beberapa pilihan ticker untuk '{company}'. Anda mahu market yang mana?\n{options}\nBalas dengan satu simbol ticker (contoh: {example}).",
        "crypto.need_id": "Sila beri ID CoinGecko yang tepat (contoh: bitcoin,ethereum). Jika ragu, guna web_search dahulu.",
        "crypto.fetch_failed": "Ralat: data kripto untuk {coin} belum dapat diambil. Cuba lagi dan guna web_search jika perlu sahkan ID CoinGecko.",
        "crypto.not_found": "Ralat: kripto '{coin}' tidak ditemui. Guna ID CoinGecko yang tepat dan guna web_search jika ragu.",
        "crypto.partial_missing": "Sebahagian data kripto tidak dapat diambil: {coins}.",
        "crypto.error": "Ralat ambil data kripto: {error}",
        "web_search.need_query": "Sila beri kata kunci carian terlebih dahulu.",
        "web_search.need_topic": "Sila beri topik atau kata kunci yang anda mahu saya cari.",
        "web_search.no_results": "Saya belum jumpa hasil web yang relevan untuk: {query}.",
        "meta.missing_access_token": "Integrasi Meta tiada access token. Isi `access_token` dalam tool call, set THREADS_ACCESS_TOKEN/KABOT_META_ACCESS_TOKEN, atau set integrations.meta.access_token di ~/.kabot/config.json.",
        "meta.unsupported_action": "Tindakan Meta '{action}' belum disokong.",
        "meta.error": "Tindakan Meta gagal: {error}",
        "cron.remove.need_selector": "Untuk padam jadual, berikan `group_id` atau tajuk jadual. Jalankan dulu: `list reminder` untuk lihat senarai kumpulan.",
        "cron.update.need_selector": "Untuk kemas kini jadual, berikan `group_id` atau tajuk jadual. Jalankan dulu: `list reminder` untuk lihat senarai kumpulan.",
        "cron.update.incomplete": "Format kemas kini belum lengkap. Contoh: `update schedule grp_shift_a every 12 hours` atau `rename grp_shift_a to Shift Team A`.",
        "cron.time_unclear": "Saya tidak dapat memastikan masa peringatan. Contoh: ingatkan saya dalam 2 minit, setiap hari jam 09:00, atau setiap 4 jam minum air.",
        "cron.cycle_created": "Kitaran '{title}' berjaya dibuat (group_id: {group_id}) dengan {job_count} kerja; berulang setiap {period_days} hari.",
        "cron.add.error_message_required": "Ralat: mesej diperlukan untuk membuat jadual.",
        "cron.add.error_no_session_context": "Ralat: konteks sesi tiada (channel/chat_id).",
        "cron.add.error_schedule_invalid": "Ralat: jadual tidak sah ({error}).",
        "cron.add.error_schedule_required": "Ralat: isi salah satu at_time, every_seconds, atau cron_expr.",
        "cron.add.error_policy_violation": "Ralat: polisi scheduler menolak permintaan ini ({error}).",
        "cron.add.created": "Berjaya cipta job '{name}' (id: {job_id})",
        "cron.add.created_group": "Berjaya cipta job '{name}' (id: {job_id}, kumpulan: {group_id}, tajuk: {title})",
        "cron.list.empty": "Tiada job berjadual.",
        "cron.list.header": "Senarai job:",
        "cron.list_groups.empty": "Tiada kumpulan jadual.",
        "cron.list_groups.header": "Kumpulan jadual:",
        "cron.status.running": "Berjalan",
        "cron.status.stopped": "Berhenti",
        "cron.status.summary": "Servis Cron: {service_state}\nJob: {jobs}\nBangun seterusnya: {next_wake}",
        "update.check.available": "Kemas kini tersedia: {latest_version} (semasa: {current_version})",
        "update.check.up_to_date": "Anda sudah menggunakan versi terkini: {current_version}",
        "update.check.commits_behind": "Pemasangan anda ketinggalan {commits_behind} commit dari keluaran terbaru.",
        "update.check.error": "Tidak dapat memeriksa kemas kini: {error}",
        "update.install.success": "Berjaya kemas kini dari {old_version} ke {new_version}. Restart diperlukan.",
        "update.install.restart_confirm": "Kemas kini selesai. Restart Kabot sekarang?",
        "update.install.dirty_tree": "Tidak dapat kemas kini: ada perubahan yang belum di-commit. Sila commit atau stash dahulu.",
        "update.install.failed": "Kemas kini gagal: {error}",
        "runtime.status.queued": "Permintaan diterima. Menyiapkan permintaan anda...",
        "runtime.status.queued_merged": "Menggabungkan {count} mesej beratur sebelumnya.",
        "runtime.status.thinking": "Sedang memproses permintaan anda, sila tunggu sebentar...",
        "runtime.status.discovery": "Sedang memahami keperluan skill dan memperkemas requirement dahulu...",
        "runtime.status.planning": "Sedang menyediakan pelan skill dan menunggu kelulusan...",
        "runtime.status.approved": "Pelan telah diluluskan. Menyediakan pelaksanaan...",
        "runtime.status.executing": "Sedang melaksanakan pelan skill yang telah diluluskan...",
        "runtime.status.verified": "Pelaksanaan sudah disahkan.",
        "runtime.status.tool": "Sedang menjalankan alat yang diperlukan, sila tunggu sebentar...",
        "runtime.status.done": "Selesai.",
        "runtime.status.error": "Ralat semasa memproses permintaan anda.",
        "runtime.abort.ack": "Baik, saya hentikan tindakan ini dan kosongkan susulan yang tertunda.",
    },
    "th": {
        "weather.need_location": "ฉันต้องการตำแหน่งเพื่อเช็กสภาพอากาศ ตัวอย่าง: เช็กอุณหภูมิที่กรุงเทพฯ",
        "cron.remove.need_selector": "หากต้องการลบตาราง โปรดระบุ `group_id` หรือชื่อตาราง ใช้ `list reminder` เพื่อดูรายการกลุ่มก่อน",
        "cron.update.need_selector": "หากต้องการแก้ไขตาราง โปรดระบุ `group_id` หรือชื่อตาราง ใช้ `list reminder` เพื่อดูรายการกลุ่มก่อน",
        "cron.update.incomplete": "รูปแบบคำสั่งแก้ไขยังไม่ครบ ตัวอย่าง: `update schedule grp_shift_a every 12 hours` หรือ `rename grp_shift_a to Shift Team A`",
        "cron.time_unclear": "ฉันยังระบุเวลาการเตือนไม่ได้ ตัวอย่าง: เตือนฉันอีก 2 นาที, ทุกวันเวลา 09:00 หรือทุก 4 ชั่วโมงดื่มน้ำ",
        "cron.cycle_created": "สร้างรอบ '{title}' แล้ว (group_id: {group_id}) จำนวน {job_count} งาน; ทำซ้ำทุก {period_days} วัน",
        "update.check.available": "มีอัปเดตใหม่: {latest_version} (ปัจจุบัน: {current_version})",
        "update.check.up_to_date": "คุณใช้เวอร์ชันล่าสุดอยู่แล้ว: {current_version}",
        "update.check.commits_behind": "การติดตั้งของคุณล้าหลัง {commits_behind} commits จากรุ่นล่าสุด",
        "update.check.error": "ไม่สามารถตรวจสอบอัปเดต: {error}",
        "update.install.success": "อัปเดตสำเร็จจาก {old_version} เป็น {new_version} ต้องรีสตาร์ท",
        "update.install.restart_confirm": "อัปเดตเสร็จสิ้น รีสตาร์ท Kabot ตอนนี้เลยไหม?",
        "update.install.dirty_tree": "ไม่สามารถอัปเดต: มีการเปลี่ยนแปลงที่ยังไม่ได้ commit กรุณา commit หรือ stash ก่อน",
        "update.install.failed": "อัปเดตล้มเหลว: {error}",
    },
    "zh": {
        "weather.need_location": "我需要一个地点来查询天气。例如：查询北京天气。",
        "cron.remove.need_selector": "要删除日程，请提供 `group_id` 或日程标题。请先运行：`list reminder` 查看分组列表。",
        "cron.update.need_selector": "要编辑日程，请提供 `group_id` 或日程标题。请先运行：`list reminder` 查看分组列表。",
        "cron.update.incomplete": "编辑格式不完整。示例：`update schedule grp_shift_a every 12 hours` 或 `rename grp_shift_a to Shift Team A`。",
        "cron.time_unclear": "我还无法确定提醒时间。示例：2分钟后提醒我、每天09:00提醒站会、或每4小时喝水。",
        "cron.cycle_created": "已创建循环'{title}'（group_id: {group_id}），共 {job_count} 个任务；每 {period_days} 天重复。",
        "update.check.available": "有可用更新：{latest_version}（当前：{current_version}）",
        "update.check.up_to_date": "您正在使用最新版本：{current_version}",
        "update.check.commits_behind": "您的安装落后最新版本 {commits_behind} 个提交",
        "update.check.error": "无法检查更新：{error}",
        "update.install.success": "成功从 {old_version} 更新到 {new_version}。需要重启。",
        "update.install.restart_confirm": "更新完成。现在重启 Kabot 吗？",
        "update.install.dirty_tree": "无法更新：工作树有未提交的更改。请先提交或暂存。",
        "update.install.failed": "更新失败：{error}",
    },
    "es": {
        "weather.need_location": "Necesito una ubicación para verificar el clima. Ejemplo: verificar clima en Madrid.",
        "update.check.available": "Actualización disponible: {latest_version} (actual: {current_version})",
        "update.check.up_to_date": "Estás usando la última versión: {current_version}",
        "update.check.commits_behind": "Tu instalación está {commits_behind} commits detrás de la última versión.",
        "update.check.error": "No se pudo verificar actualizaciones: {error}",
        "update.install.success": "Actualizado exitosamente de {old_version} a {new_version}. Se requiere reinicio.",
        "update.install.restart_confirm": "Actualización completa. ¿Reiniciar Kabot ahora?",
        "update.install.dirty_tree": "No se puede actualizar: hay cambios sin confirmar en el árbol de trabajo. Por favor confirma o guarda primero.",
        "update.install.failed": "Actualización fallida: {error}",
    },
    "fr": {
        "weather.need_location": "J'ai besoin d'un emplacement pour vérifier la météo. Exemple: vérifier météo à Paris.",
        "update.check.available": "Mise à jour disponible: {latest_version} (actuelle: {current_version})",
        "update.check.up_to_date": "Vous utilisez la dernière version: {current_version}",
        "update.check.commits_behind": "Votre installation est en retard de {commits_behind} commits par rapport à la dernière version.",
        "update.check.error": "Impossible de vérifier les mises à jour: {error}",
        "update.install.success": "Mise à jour réussie de {old_version} vers {new_version}. Redémarrage requis.",
        "update.install.restart_confirm": "Mise à jour terminée. Redémarrer Kabot maintenant?",
        "update.install.dirty_tree": "Impossible de mettre à jour: il y a des modifications non validées dans l'arbre de travail. Veuillez valider ou mettre en cache d'abord.",
        "update.install.failed": "Échec de la mise à jour: {error}",
    },
    "ja": {
        "weather.need_location": "天気を確認するには場所が必要です。例：東京の天気を確認",
        "update.check.available": "アップデート可能：{latest_version}（現在：{current_version}）",
        "update.check.up_to_date": "最新バージョンを使用中：{current_version}",
        "update.check.commits_behind": "インストールは最新リリースより {commits_behind} コミット遅れています",
        "update.check.error": "アップデートを確認できませんでした：{error}",
        "update.install.success": "{old_version} から {new_version} へのアップデートに成功しました。再起動が必要です。",
        "update.install.restart_confirm": "アップデート完了。今すぐ Kabot を再起動しますか？",
        "update.install.dirty_tree": "アップデートできません：作業ツリーにコミットされていない変更があります。先にコミットまたはスタッシュしてください。",
        "update.install.failed": "アップデート失敗：{error}",
    },
    "ko": {
        "weather.need_location": "날씨를 확인하려면 위치가 필요합니다. 예: 서울 날씨 확인",
        "update.check.available": "업데이트 가능: {latest_version} (현재: {current_version})",
        "update.check.up_to_date": "최신 버전을 사용 중입니다: {current_version}",
        "update.check.commits_behind": "설치가 최신 릴리스보다 {commits_behind}개 커밋 뒤처져 있습니다",
        "update.check.error": "업데이트를 확인할 수 없습니다: {error}",
        "update.install.success": "{old_version}에서 {new_version}로 업데이트 성공. 재시작이 필요합니다.",
        "update.install.restart_confirm": "업데이트 완료. 지금 Kabot을 재시작하시겠습니까?",
        "update.install.dirty_tree": "업데이트할 수 없습니다: 작업 트리에 커밋되지 않은 변경 사항이 있습니다. 먼저 커밋하거나 스태시하세요.",
        "update.install.failed": "업데이트 실패: {error}",
    },
}


def _normalize_key(key: str) -> str:
    if key in _KEY_ALIASES:
        return _KEY_ALIASES[key]
    dotted = key.replace("_", ".")
    if dotted in _CATALOG["en"]:
        return dotted
    return key


def _looks_mojibake(value: str) -> bool:
    """Detect common UTF-8 mojibake artifacts from bad transcoding."""
    text = str(value or "")
    if not text:
        return False
    markers = ("Ã", "Â", "â€", "à¸", "Ð", "Ñ", "ã‚", "ì„", "Ãƒ")
    hits = sum(text.count(marker) for marker in markers)
    return hits >= 2


def tr(
    key: str,
    *,
    locale: str | None = None,
    text: str | None = None,
    **kwargs: Any,
) -> str:
    """Translate key to locale, falling back to English."""
    lang = locale or detect_locale(text)
    normalized_key = _normalize_key(key)
    locale_template = _CATALOG.get(lang, {}).get(normalized_key)
    english_template = _CATALOG["en"].get(normalized_key)
    if locale_template and english_template and _looks_mojibake(locale_template):
        template = english_template
    else:
        template = locale_template or english_template or key
    if not kwargs:
        return template
    try:
        return template.format(**kwargs)
    except Exception:
        # Never fail user response because of i18n formatting mismatch.
        return template
