Repository navigation
i18n: Korean language pack and translation tooling (FEAT-43..46, MNT-75) - #1
Merged
Merged
Conversation
Read-only scan of the bot and dashboard for multi-language support (ko, en, extensible): string inventory by module, gettext + Babel recommendation (the dashboard is NiceGUI, so no JS i18n library applies), catalog layout by domain (bot, commands, web, errors, common), audience-based locale policy for Discord, cookie-based dashboard language, Korean particle and register rules, a phased migration plan and the decisions that need a go-ahead before any code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Records the user's go-ahead (2026-10-12) on the i18n plan: decisions A-E as recommended, parallel Phase 1 batches allowed (F). Phase 0 is FEAT-43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Adds the translation-at-the-edge foundation agreed in D39. Nothing in the
existing code imports it yet, so bot and dashboard behaviour is unchanged.
- llmcord_core/i18n: locale normalization and negotiation, a safe message
formatter (bare named fields only) with Korean particle specs such as
{name:이/가}, gettext .po catalogs loaded lazily through Babel, the
Translator (t, n, p, np, error, number, list), Msg/N_/NP_ markers,
E_ error codes and UserError, a pseudo-locale, and the Discord
(command translator, invoker/channel locale) and dashboard (cookie,
account and Accept-Language order, per-client Quasar language) adapters.
- llmcord_core/locales: empty templates for the bot, commands, web, errors
and common domains, and a header-only ko catalog.
- scripts/i18n.py (extract, init, update, check, ratchet, stats) and
scripts/i18n_check.py (catalog checks and a hardcoded-string ratchet,
baseline in scripts/i18n_ratchet.json); wired into verify.sh and CI.
- Babel 2.18.0 added; ruff INT rules enabled for t, N_ and C_.
- 194 tests across tests/test_i18n_*.py.
scripts/verify.sh: all stages pass, 3708 tests OK (skipped=144).
A multi-lens review is in progress; its fixes follow in a separate commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Multi-lens review (security, contract, Korean, tests, tooling) with adversarial checks: 27 of 35 finding groups confirmed and fixed, plus a re-review round. - format: refuse 4+ digit format specs; linear final_sound; combined josa for Discord tags (also markdown-wrapped) and Latin-attached digits; strip markdown and fullwidth closers - Translator.error never raises; UserError survives copy/pickle - load_catalog: only en, pseudo and existing normalized locales reach the cache; anything else gets an uncached empty catalog - web_locale.current_locale reads the slot stack, 'en' outside a client - check: josa typos without a slash, non-literal conditionals, Plural-Forms vs CLDR, mapped i18n files, BOM-safe parsing - ratchet: counts HTTPException detail, project exceptions, savebar track, every module; --update refuses increases and unreadable baselines unless --allow-increase (baseline 693 ui / 529 raise) - docs: i18n-plan section 6.1 (Phase 0 as built), CLAUDE.md module map and verify wording, FEAT-43 done, spikes needing hardware to FEAT-44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
The Hold'em engine and store tests named test_known_defect_* had no expectedFailure decorator and pass, so their defects were already fixed. Drop the prefix and reword each docstring as "FEAT-33 (fixed): ..." per tests/CLAUDE.md, so the verify skill's expected-failures check (0) matches the known-defect count (0). Test-only rename; no behavior change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5F222cpy3vbXwBkS1cr7V
…defect tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
- i18n.english(): the cached English translator - discord_locale.invoker_t / channel_t: audience resolvers, English until FEAT-44, so Phase 1 call sites state their audience now - UserError accepts a legacy plain message so project error classes can subclass it before every raise site is converted - codes.py / terms.py: one section per Phase 1 batch so parallel batches merge cleanly - CLAUDE.md: decision G invariants (translate at the edge, audience decides the language, English text is the message id) - i18n-plan.md section 6.2: Phase 1 conventions, batch order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Phase 1 batch 1d (FEAT-43, D39). Command and group descriptions, @app_commands.describe option descriptions and static Choice names in discord_bot.py, favorites_discord.py, games_discord.py, games_doubt_discord.py and items_discord.py are wrapped in C_() with the exact current English as the msgid (115 strings). Names, option names and Choice values stay plain English (decision B). SkitBot.setup_hook registers LlmcordTranslator before any tree sync. With empty catalogs every translate() returns None, and discord.py adds no localization entry for None, so the synced payload is unchanged. Left in English: dynamic autocomplete choices (timezones, item names, character and space names) and the auto-generated choices of Literal[...] parameters. Catalogs regenerated (commands.pot, ko commands.po) and the ratchet lowered for the five files. Full verify: 3762 tests OK; no test edited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Phase 1 batch 1a (FEAT-43, D39). ConflictError, CurrencyError and GameError in admin_store.py now subclass UserError. Every raise in admin_store.py and store.py whose text can reach a person uses a code from the MNT-66 section of i18n/codes.py, with the exact old English as the message and f-string expressions as named params. Fragments built from English words (ante/buy-in/bet, the daily field names, the Soft/Hard cap, the spoken-move flag labels) became one code per case. str(err) is byte-identical, so the surfaces that still show str(err) are unchanged until their own batches switch to t.error(err). Left in English: internal guards (valid_timezone control flow, 'no button', 'not a table column', the PlayStyle TypeError), the operator-only "database is newer" startup error, stored audit and ledger text, DEFAULT_CURRENCY, and GameError(spec.illegal_text(...)), which comes from the game engines and is batch 1c's. Catalogs regenerated (errors.pot, ko errors.po); ratchet lowered (admin_store 156 -> 2, store 26 -> 2 raises). Offline suite: 3762 tests OK; no test edited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…ge through Translator Ephemeral replies use invoker_t(interaction); channel posts, progress status, failure notices and the reply footer use channel_t(guild), resolved where they render. Reply helpers take keyword t=None with the English fallback; store errors are shown through t.error(err). 13 codes in the MNT-70 section of codes.py. English output is byte-identical; no test was edited. Left English: character webhook text, log lines, custom_ids, and text built in catchup.py, errors.user_detail and the items/ registry (owned by later batches). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
errors.pot and ko errors.po were regenerated (extract, update, ratchet --update, check), not hand-merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Add Phrase and Translator.render to llmcord_core/i18n, render Msg and Phrase params in the target locale, and let Translator.error render any coded exception (CodedError mixin; IllegalMove uses it and stays a non-ValueError). The engines (blackjack, doubt, holdem, playstyle, registry) gain structured twins: rules_phrases, how_to_play_phrases, total_phrase, cards_phrase, hand_phrase, describe_phrases, Result.summary_phrases and Assist.current_phrase. The old English functions stay as wrappers with the same output, so English and existing tests are unchanged. Game names, nouns, poker ranks and play style groups are Msg terms. User-facing rule validation raises UserError codes; registry illegal_text returns an ErrorCode. Stored text (ledger reasons, round facts, table summaries) and model-facing text keep plain English. Engine plurals branch between two whole constants until a deferred plural marker exists (Phase 4). Ratchet: blackjack.py 15->9, doubt.py 20->16, holdem.py 21->15 raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
i18n-plan 6.2 gains "Engines and phrases" (Phrase, Translator.render, Msg and Phrase params, twins, CodedError, the engine plural gap, stored text) and the list of edge call sites that 1f, 1g and 1i switch to the twins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
The operator pages, the page frame, Server setup and Characters in dashboard.py take t = web_locale.current() and send every label, button, tab, notify, tooltip, dialog title, column, option label, placeholder and hint through it. Text inside props strings moves to element.props[...] so a translation cannot break the props parser. Text helpers (rejection_notice, format_usd, key_status, profile_view, role_options, server_usage_label, usage_chart_options, usage_feature_rows) take keyword-only t=None. Operator-visible raises become DASH_* codes. The play style editor renders the 1c phrases. Audit detail dicts stay English (stored). Text shown verbatim from backend.py, prices.py and config.py is left for batch 1b. The turn log, monitoring and lore panels are left for batch 1h. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
games_discord.py, games_doubt_discord.py, games_holdem_discord.py, games_spoken_discord.py and games_spoken_doubt.py send their text through t. Ephemeral replies, modals and pickers use invoker_t; boards, lobbies, thread names, timer-driven edits, closing and recovery messages use channel_t, resolved at each render and never kept in a timer, closure, View or Modal. Renderers take keyword-only t=None and every production caller passes it. Button and select text carries button.*/select.* contexts. The engine call sites use the 1c twins (rules_phrases, how_to_play_phrases, total_phrase, cards_phrase, hand_phrase, Assist.current_phrase), and store errors go through t.error. Model-facing text (table_block, table talk events, the showdown event) and log lines stay English. Ratchet: the ui entries for the five files are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Babel's catalog.update() marks a rewritten msgid fuzzy and copies the old msgstr. With an empty msgstr the flag means nothing to gettext or to a translator's tool, yet it stuck across updates and made `scripts/i18n.py check` warn (ko/bot after batch 1f). _sync now drops it where the translation is empty and keeps it where a translation was carried over. New tests in tests/test_i18n_fuzzy.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
turn_log_section, monitoring_panel, entry_editor, import_changes, imports_panel, rule_label and _prompt_options in dashboard.py take t = web_locale.current() or keyword-only t=None and send their labels, buttons, notifications, column labels, option labels and hints through t. Hints inside props strings move to element.props[...]. The prompt role, placement and source label dicts become N_/NP_ constants. Four operator-visible raises become DASH_* codes. Left English: warnings shown verbatim from lorebooks.py, cards.py and the import services (batch 1b), store identifiers such as import status and disposition, role identifiers, and the turn log content. presets_ui.py still reads the prompt label dicts untranslated (batch 1i). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
config.py, prompts.py, lorebooks.py, cards.py, avatars.py, backend.py, engine.py, names.py, holdem_store.py, items_store.py and items/ raise UserError codes (165, in the MNT-67 section of codes.py) where their text reaches a person: dashboard toasts, Discord replies and the startup console. ItemError, RecipeError and NameNotFound become UserError subclasses with the same str(), args, attributes and pickling. Item errors keep comparing equal to their old string code (_ItemCode) because existing tests compare error.code with it. Left English: provider and model-output diagnostics in models.py and engine.py, ModelConfigError and BudgetExceeded (not ValueErrors), stored lorebook warnings, built-in catalog integrity errors, and text returned rather than raised (backend problems, model-list and price messages, prompts.compatibility()), which needs structured values before the dashboard can translate it. auth.py, web.py and admin.py are left for part 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
i18n-plan section 5 and the proposed glossary: 다우트 (카드게임) or 다우트(Doubt) where the game name stands alone, plain 다우트 inside a sentence so particles work, 다우트! for the doubt call (user guidance). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…AUDE.md module map) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…ut in FEAT-46, own 404 page) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…problems, model list and price results) compatibility(), Backend.problems and result messages keep their English; compatibility_phrases(), problem_phrases and a phrase field sit next to them, and the dashboard renders those with the viewer's translator. Price lookups use a not_found flag instead of comparing the English sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
… at display ModelConfigError takes the CodedError mixin (still a RuntimeError, same text); models.py raises it with codes, one whole message per environment, and the play-style path re-raises it as a UserError the dashboard renders per viewer. The Monitoring turn log keeps storing its English stage text and translates the known parts when it shows them (usage.turn_log_stage); the stage word table moved to i18n/terms.py and is shared with the Discord failure reply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…urn log stage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2 # Conflicts: # llmcord_core/i18n/terms.py # llmcord_core/locales/ko/LC_MESSAGES/common.po # llmcord_core/locales/templates/common.pot
… bet-limit saves audit their limits Item errors' code is now the plain ErrorCode; the old string is the class attribute kind. Four test lines that compared .code with the old string now read .kind (approved for MNT-78). The Games tab's bet-limit save builds its audit detail from the saved min_bet/max_bet, so the row carries it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…anslated at display; own 404 page Prompt block names and emotion slot labels that still carry their built-in default are shown in the viewer's language; renamed or imported names show as typed, and saving an untouched translated default keeps the stored label. Unknown dashboard paths get a small 404 page in the viewer's language instead of NiceGUI's English one (status 404, nothing from the path shown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
All 2,033 messages in bot, commands, common, errors and web, with the approved glossary and 해요체. Particle specs after placeholders, counters in the text, Discord length limits checked by scripts/i18n.py check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…-81 c)
Recipe names may be locale-keyed; Recipe.name stays English and
display_name(recipe, locale) picks the audience's. Discord item replies
and the dashboard Shop tab show the localized name; stores keep English
and add the recipe key to item errors so the edge can swap the name.
Tarot orientation words per locale. tests/test_items_catalog.py: the
en-fallback test now uses an unsupported locale ('fr') because the
8-ball has Korean text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Also hoists shown_name() out of two button labels in items_discord.py so Babel extracts them (a nested call taking t hides the outer message). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
…tes in verify and CI (MNT-75) scripts/i18n.py coverage [--min PCT] [--locale ...] prints per-locale and per-domain coverage. check fails a locale pack below 100% or with fuzzy entries unless llmcord_core/locales/incomplete.txt lists it. The browser stage runs test_dashboard_browser*.py (main, 404, language), and the language suite gains a pseudo-locale smoke over every server tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Seven marker calls had another call taking t in their arguments, so Babel skipped the outer message (it stayed English in every locale, and 'space ref' was extracted as a bogus msgid). Inner values are now computed first; output is unchanged. tests/test_i18n_extraction.py scans every literal marker call and fails if one is missing from the templates. Korean added for the recovered messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
The kind was derived from the code name ('random_message' -> 'Random
message'), so Korean viewers saw English. It now comes from a small table
with the 'item kind' context; unknown kinds keep the derived text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Korean doubt and Hold'em button labels are read as their moves by the spoken-move readers; blackjack (model-only reader) pins the glossary words. Smoke tests: ko slash replies, public tables in a ko server and the audience rule, particle choice in real catalog messages, Korean command metadata within Discord limits; browser: Korean tab labels, html lang ko-KR, 한국어 marked current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
The user reviewed every screen on the review page. Applied the 15 suggestions; ante is now 판돈 instead of 앤티 everywhere (glossary updated, particles adjusted for the final consonant), shorter 잡담 on the table-talk button, clearer wording for move checks, the replay button and moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the i18n framework, wires it into the Discord bot and the dashboard, and ships a fully translated Korean pack.
llmcord_core/i18n(Translator, markers,UserErrorcodes, safe formatter with Korean josa, locale negotiation), gettext catalogs underllmcord_core/locales, andscripts/i18n.py/scripts/i18n_check.pywith a string ratchet./lang, and a Bot language field in Server settings.verify.shand CI.Verification
scripts/verify.shpasses: 4,021 tests OK.test_dashboard_workflows_and_live_authorizationfails in the cloud sandbox withnet::ERR_TUNNEL_CONNECTION_FAILED, which is an environment network issue unrelated to this change.Left to do
🤖 Generated with Claude Code
https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2
Generated by Claude Code