Skip to content

i18n: Korean language pack and translation tooling (FEAT-43..46, MNT-75) - #1

Merged
yor42 merged 77 commits into
mainfrom
claude/funny-goldberg-lwk0li
Oct 12, 2026
Merged

yor42 merged 77 commits into
mainfrom
claude/funny-goldberg-lwk0li

Conversation

@yor42

@yor42 yor42 commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Adds the i18n framework, wires it into the Discord bot and the dashboard, and ships a fully translated Korean pack.

  • Framework (FEAT-43): llmcord_core/i18n (Translator, markers, UserError codes, safe formatter with Korean josa, locale negotiation), gettext catalogs under llmcord_core/locales, and scripts/i18n.py / scripts/i18n_check.py with a string ratchet.
  • Discord (FEAT-44): schema v22 (backup-before-upgrade migration), per-invoker and per-channel locale resolution, and localized slash-command metadata.
  • Dashboard (FEAT-45): viewer language, a language switcher, /lang, and a Bot language field in Server settings.
  • Korean (FEAT-46): all five catalogs at 100%, Korean item recipe names and output tables, and the native review applied (판돈 for ante, 잡담 for table talk).
  • Automation (MNT-75): coverage report and completeness gate, extraction-trap test, and a pseudo-locale browser smoke run in verify.sh and CI.
  • Docs: Korean glossary column, Language sections in the server guide and admin console docs, a wiki page, and the i18n runbook.

Verification

  • scripts/verify.sh passes: 4,021 tests OK.
  • Browser i18n suite passes. The main browser suite's test_dashboard_workflows_and_live_authorization fails in the cloud sandbox with net::ERR_TUNNEL_CONNECTION_FAILED, which is an environment network issue unrelated to this change.

Left to do

  • A pass on a Discord test server with a Korean client.
  • MNT-83 Korean follow-ups (see backlog).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mon6hXe9W6a6hrBYSGU6Y2


Generated by Claude Code

claude added 30 commits October 12, 2026 06:27
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
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
…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
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
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
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
claude added 29 commits October 12, 2026 13:31
…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
…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
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
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
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
@yor42
yor42 merged commit da5ef29 into main Oct 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants