Skip to content

WIP: migrate Ultimate Geography to Rust Brain Brew rewrite with federation - #736

Closed
jeprecated wants to merge 6 commits into
anki-geo:masterfrom
jeprecated:master
Closed

WIP: migrate Ultimate Geography to Rust Brain Brew rewrite with federation#736
jeprecated wants to merge 6 commits into
anki-geo:masterfrom
jeprecated:master

Conversation

@jeprecated

@jeprecated jeprecated commented May 25, 2026

Copy link
Copy Markdown
Member

Hello all! Jordan here. I changed my github username from ohare93, if you're confused 😁 anyways, I finally got around to it! Deck Federation is now in Brain Brew!

This is a work-in-progress migration to the new Rust Brain Brew workflow, paired with jeprecated/brain-brew#60 things are looking very bright!

What changed

  • Replaces legacy Python Brain Brew recipes with a Rust Brain Brew brainbrew.yaml manifest.
  • Adds deck.yaml as the canonical English source.
  • Adds language, variant, and Hardcore Geography overlays.
  • Adds CI to verify and export all configured targets.
  • Current local status: brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media passes for 71
    targets.

Related issues / unlocked work

Available for questions, more to come soon! 👀

@jeprecated
jeprecated requested review from aplaice and axelboc May 25, 2026 09:49
@jeprecated

Copy link
Copy Markdown
Member Author

Example of a single translation PR in this new system, here's the outstanding PR #735 converted into this new system: jeprecated#16

If any strings were missed in the translation overlay it would throw an error 👌

When we make changes to the base English deck all those strings will throw errors for each language (which is the correct behaviour!).

@aplaice

aplaice commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Wow! This looks like a huge amount of work!

I wish my review below were more positive, but at the moment I'm not convinced that this is a net improvement.

It enables things that are currently tricky (e.g. flexibly allowing different note types!) and AFAIU generally makes future extensibility easier.

However, IMO it makes routine tasks that are currently straightforward more finicky and tedious. (OTOH it's possible that I'm misunderstanding how a new, improved workflow would work!)

I greatly appreciate how you've continued working on this for so long and I'm generally very excited about improvements to our tooling!

(Details/discussion below; I've been sitting on it for several weeks now, being hesitant to post the comment (sorry for the resultant delay!!), but I can't think of any particular improvements to the text.)

YAML

I'm not sure that replacing everything with YAML is, on the whole, advantageous, and, if yes, I'm not convinced the proposed structure is most convenient.

HTML/CSS

For the HTML/CSS files (deck descriptions and templates) it's IMO a minor but clear downgrade both for editing (one has to worry about proper indentation for the literal blocks) and discoverability (rather than being accessed via standard directory navigation one has to find the correct sub-key in the YAML).

CSV vs. YAML

(field data (old src/data))

Both YAML and CSV have flaws as formats, so this comparison is much trickier.

The new structure is far more verbose, but that's not necessarily a bad thing (being explicit is often valuable). (Though why do we need fields: {field.X: ...} rather than just fields: {X: ...}?)

Both CSV and YAML have their annoying quoting idiosyncrasies, though at least with CSVs users can avoid them by opening in a spreadsheet editor. (With the old anki-dm, this regularly lead to issues due to quoting inconsistencies, but with the current BrainBrew it seems to work fine.)

I'm not sure about grouping by (country, field) rather than (country, language) (i.e. having, say, all translations of country info together). There's no perfect solution since we're trying to flatten 3D data (country × field × language) into 2D or 1D, but I think that we had discussed this during the anki-dm -> brainbrew transition and decided that given the usual edit patterns it's more convenient to have all translations of a field together, rather than all fields of a country together, and I think I still agree. (i.e. we had decided to have per-field CSVs (capital.csv etc.) rather than per-translation ones (french.csv etc.))

English field as key

I'm also not convinced about using the English version of the field as the key for translations. It's a frequent, though not universal convention in localisation software (GNU gettext does this, fluent doesn't), and it means that the source version is available side-by-side with the translation, as well as (like you note) enforcing the updating of translations when the source changes. However, here, it has several disadvantages:

  1. It's often ugly:

    e.g. 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (modré pozadí, červený a bílý kříž), Norsko (červené pozadí, modrý a bílý kříž)'. (Obviously, our current CSVs are worse in that instead of having two such monstrosities per line we have ~16, but in the YAML version we have this for every language (~15 times) rather than only once, and the point of flattening to "1D" should be to avoid such long lines as far as possible.)

  2. At least for the country info field, the translations aren't always intended to be direct translations, so having the English version as key is misleading and means that having to update the English key, in all translations, when it changes, is just an inconvenience (rather than a correctness check). It also means that we need empty keys (foobar: '') and "additions".

    (NB I appreciate how all these many edge-cases are handled, technically, but I imagine that from a translator PoV the inconsistencies would get annoying quickly.)

    In other words: because the translations are no longer "independent", when the translator wants to add (say) a country info, they have to go back to deck.yaml, look up whether there's an English country info (and what it is) in deck.yaml, and either use the English country info as key or add a new addition: {notes.note.COUNTRY.fields.field.country-info: ..., instead of just searching for the country in country_info.csv and either adding a new field to an existing row or adding a new row.

  3. Lack of context (minor).

    (Again mainly for country/capital info.) In some cases it's useful to have the country name and not just the English country info. (We encourage using the terms present on the given Wikipedia page, so for instance to best translate "Autonomous community of Spain." it's helpful to know that this is for the Canary Islands, so one can look up how the Canary Islands are categorised in one's language on Wikipedia, instead of just directly translating the phrase.)

  4. (As noted above regarding grouping): when adding a new country, having to copy-paste the English keys (for each of the fields) into each of the translations sounds very tedious. (Similarly when updating, say, a capital info (e.g. after the capital was moved), and having to change both the English and the translated version in each file.)

    OTOH having separate files for each language indexed with the English key opens up a new workflow: only translate what one can do easily and leave the rest broken. These remaining translations would then be picked up by more competent translators later on. (We'd only ensure that none of the builds are broken before release.)

    This wasn't possible previously because we didn't have any way of indicating that a translation was missing (rather than just deliberately empty) or wasn't up-to-date. However, I'm not sure how "cost-effective" this is — translators later would have poorer access to the original context. Leaving translated builds broken for extensive periods of time is also not great practice...

Partial aside

The grouping and disambiguation of strings that repeat in English (say Independent state claimed by Georgia. or Island of Indonesia.) is really cool and even on a quick scan, it's allowed me to catch some errors/inconsistences (nárokováný vs. nárokovaný in Abkhazia/South Ossetia country info:cs, 'Indonéský ostrov.' vs. 'Ostrov Indonésie.' in Bali/Java/Sumatra country info:cs, weirdness in Azores country info:da ('Selvstændig region Autonomous region i Portugal.')).) (I'll need to remember to compile all such cases and fix them.)

This does point to a major advantage of using the English field as the key, but I think that the cons discussed above outweigh the pros.

Hardcore geography

If I understand correctly, the way the "overlapping" notes (those that have some cards in UG and some in HG) have been made to work is that in HG the notes have the "missing" fields added (via "field-fills"), so that we only have a single copy of each note, with both the UG and (old) HG cards. (Also, HG now contains UG rather than being an addition (?).) This is pretty elegant (and clean), but I'm not sure if it's the most convenient behaviour.

The problem is that now if someone imports UG, imports HG, and then updates UG (without updating HG) all of the HG cards of the overlapping notes will disappear. I believe that they won't be deleted unless one runs Tools > Empty cards, and hence they can be recovered by updating HG (but it might vary depending on Anki version). (NB the initial "import UG" step isn't necessary, but it is how I'd expect most users to start and illustrates why people might end up confused — they'd be thinking that they're only updating their UG import and that it shouldn't affect HG.)

We could obviously warn people that if they import HG they should only update via HG, but I expect such warnings to frequently fail. The issue being (usually (?)) recoverable (if one doesn't empty cards in the meantime) makes me less convinced which behaviour is the "correct" one, since the alternative of having two notes (one for UG and one for HG) in the "overlapping" cases is aesthetically displeasing.

(A similar (worse) problem would also occur if one were to use a deck that adds a completely new card (say a hypothetical currency UG adding country->currency cards). Import UG -> import currency UG -> update UG and you lose all your country->currency cards (immediately deleted, though with a warning).)

Modifying "fully" UG cards

One small, but potentially very useful thing that the new brain brew facilitates for decks like HG is modifying cards that are solely in UG (i.e. not the "overlapping" notes, but notes which are in UG and have no cards in HG).

When we removed the small dependent territory capitals from UG (and moved them to HG) we also removed the capital hints from the notes which no longer had any collisions within UG (whose "conflictors" were moved to HG) — Basseterre (Saint Kitts and Nevis) which had conflicted with Guadeloupe and Georgetown (Guyana) which had conflicted with the Cayman Islands. (The capital hints on the other side — in Guadeloupe and the Cayman Islands — were kept.)

The new field-fills mechanism allows easily modifying these notes in HG to re-add the capital hints.

(Technically, we could achieve this with our current set-up by cloning the notes from UG to HG (keeping the same guid) and re-adding the capital hints to the clone, but it would be far more brittle and harder to maintain.)

Nix as a requirement?

I'm hesitant about having nix as a dependency, but given that (AFAIU) it wouldn't be really required for most contributors, it's probably not a major issue.


In any case, thanks very much for your continuing efforts to improve BrainBrew and the tooling around AUG!

@axelboc

axelboc commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Hey @jeprecated and @aplaice, hope you both are well! 😊

I did have a look through the diff a few weeks ago but got quickly overwhelmed 😅 — though it's amazing to see this federated deck concept taking shape. 🤩

Like @aplaice, I'm not convinced that the proposed format for translations is ideal. It's the main thing that stood out to me. Having all the translations in one file helps bring consistency but the flat list with the English text as key is really confusing. I think the translation file should follow the structure of deck.yaml with some simplifications — e.g. with French:

australia:
  guid: ...
  country: Australie
  capital: Canberra
  capital-hint: ''
  ...

austria:
  guid: ...
  country: Autriche
  capital: Vienne
  capital-hint: ''
  ...

I also agree that the HTML and CSS should live in .html and .css files. More generally, I think there's a need for the real content of the deck and its extensions (including translations, templates, etc.) to be stored in less verbose ways, closer to what the CSV format used to provide.

As an example, let's take overlays/variants/experimental.yaml. I could imagine a file called region-codes.yaml to store the actual region codes in a much simpler way:

abkhazia: ''
afghanistan: AF
...

The experimental.yaml file could then reference the path to region-codes.yaml.


The diff includes the Hardcore Geography extension. Perhaps it's to show a demonstration of the federation system but I would of course prefer to keep this extension in a separate repo.


Could you please explain the concept of a "variant" and how differs from an "extension"?

@jeprecated

Copy link
Copy Markdown
Member Author

Hello sirs!

Sorry for the delay in replying, I've been swamped with work. Both in the professional sense and also doing more work on this PR!

Wow! This looks like a huge amount of work!

It enables things that are currently tricky (e.g. flexibly allowing different note types!) and AFAIU generally makes future extensibility easier.

I greatly appreciate how you've continued working on this for so long and I'm generally very excited about improvements to our tooling!

Big thinking work, but it actually didn't take long to do. I've thought about this problem on and off since my email to you two about this rewrite idea 10 months ago, but the actual effort for my initial PR was < 6 hours of actual work. More on that below.

I wish my review below were more positive, but at the moment I'm not convinced that this is a net improvement.

However, IMO it makes routine tasks that are currently straightforward more finicky and tedious. (OTOH it's possible that I'm misunderstanding how a new, improved workflow would work!)

(Details/discussion below; I've been sitting on it for several weeks now, being hesitant to post the comment (sorry for the resultant delay!!), but I can't think of any particular improvements to the text.)

No worries at all, sir. I am not perturbed. Next time don't feel the need to delay, I was not expecting to simply merge this in. I truly meant it when I said:

This is a work-in-progress migration to the new Rust Brain Brew workflow

Though perhaps I could have been clearer on my expectations, the amount of effort I've actually put in here, and my workflow. Though that gets into some of my personal situation, AI/Agentic Development, and other stuff like that - which I don't want to turn this PR into a discussion/debate on these topics. But just for the context of what is to come, and to set your expectations on the state of my work and how I get it done (and why you can trust it) then here's a collapsed box explaining just that:

How I developed and validated this migration

I did all of the work for this Brain Brew rust migration using Agentic Development (with AI). In my spare time, while running 5 other projects.

Wow Jordan you mean you vibe coded it? "Brain Brew Rust Rewrite, make no mistakes [enter]"

No! 😁 Allow me to explain by first adding context.

I created my own startup 4 months ago: Self-Deprecated https://selfdeprecated.ai/ an AI Solutions company, to help bring clients up to speed on Agentic Development. On how to use Agents, what their strengths are, when not to trust them, and how to make sure your "self-healing loop" is the best it can be in order to get the output you desired, while remaining in a "human-out-the-loop" for as much as possible.

I use Hunk (https://github.com/modem-dev/hunk/) to get the Agent to make PR comments on it's changes, and I review the actual code. Do I read absolutely every line always? Absolutely not, and nor should I. It depends on the system being created/changes. Hunk helps one have the Agent walk them through a change/PR step by step, while looking at the code, explaining in as much detail what the changes were for and how they relate to the other code segments.

For this work (migrating an existing tool from one language to another, though with some bit feature changes too) I had an amazingly easy job. The bulk of the work is in thinking and designing the system, while the core migration is unusually straightforward to test as an output-equivalence problem. Brain Brew’s job (before and after the rewrite) is to produce CrowdAnki decks. Given the same accepted source, it either produces the same parsed JSON and media or it produces a regression, apart from differences we have explicitly reviewed and classified. The migration evidence compares deck and model identity, fields, cards/templates/CSS, notes/GUIDs/tags, descriptions, configuration, and media bytes.

Yet at the same time I've add a lot more features! Though almost none of these change the output in the end, they just give better composition and flexibility to the data as it's stored in the repo. Such as combining decks together for the federation.

In short:

  • I sell my services as a "Senior Agentic Developer" to both companies and individual developers to skill themselves up.

  • I have comprehensively tested the migration’s CrowdAnki output surface. In this PR, strict verification covers 74 main and 26 companion targets, CI exports all 100, and eight parsed-JSON goldens detect current-output drift.

    Brain Brew itself also now contains a complete pinned copy of this UG source, including all 607 media assets and the expected parsed CrowdAnki output for every one of the 100 targets. Its mandatory offline fixture test rebuilds and compares all 100 outputs and validates the real media bytes, so future changes to Brain Brew cannot silently alter UG output.

    Separately, ten representative pinned historical builds reject unclassified identity, content, template, configuration, or media changes. The known capital-hint localisation fallback remains explicitly documented and deferred rather than hidden by the comparison.

  • The updating of this system is now much easier than it was before, due to both the better setup of the codebase, and also the proliferation of these new tools.

So all of the above said:

  • This is a project with a very fast improvement timeframe
  • The maintainer / translator use case / workflow is top priority
  • It is unusually testable for a migration.
    • Strict verification covers all 74 main and 26 companion targets, and CI transactionally exports all 100.
    • Eight parsed-JSON goldens detect current-output drift, while pinned historical builds reject unclassified semantic or media changes.
    • It's really a pleasure to work in.

Work Done to Address the comments

Rather than line by line reply to each comments (as we three are want to do) I think it'd be faster for everyone if I simply address the things that I have improved. Some I always intended to do, and some are direct responses to good feedback from yourselves

Translation overlay format cleanup

Example: old and new translation overlay formats

Before: old alpha schema mixed several intents under translations. "changes", "additions", each of which could be general or field-specific

translations:
  changes:
    Taiwan: "台湾(Taiwan)"
    Taipei: "台北(Taipei)"

    Georgia:
      notes.note.georgia.fields.field.country: Georgien
      notes.note.us-georgia.fields.field.region: Georgia

    Partially recognised state claimed by China.:
      notes.note.taiwan.fields.field.country-info: "中国宣称对台湾拥有主权,但仅被部分国家承认"

  additions:
    notes.note.yellow-sea.fields.field.country-info: "虽然国际上普遍认为黄海包含了渤海,但在中国,它们通常被视为两个独立的海域。"

After: reusable translations, contextual translations, and typed target adaptations are separate

translations:
  direct:
    Taiwan: "台湾(Taiwan)"
    Taipei: "台北(Taipei)"

  contextual:
    notes.note:
      andorra.fields.field.flag-similarity.message.variables.country_1:
        Moldova: "摩尔多瓦"

target_adaptations:
  notes.note.taiwan.fields.field.country-info:
    intent: adapt
    ownership: translation
    expected_source: Partially recognised state claimed by China.
    target: "中国宣称对台湾拥有主权,但仅被部分国家承认"
    reason: target-language geopolitical wording

  notes.note.yellow-sea.fields.field.country-info:
    intent: adapt
    ownership: translation
    expected_source: ""
    target: "虽然国际上普遍认为黄海(Yellow Sea)包含了渤海(Bohai Sea),但在中国(China),它们通常被视为两个独立的海域。"
    reason: "migrated from legacy translations.target_additions; review and describe its target-language purpose"

  notes.note.czech-republic.fields.field.country-info:
    intent: delete
    ownership: translation
    expected_source: Also known as Czechia.
    reason: "migrated legacy target adaptation; review and describe its target-language purpose"

Reviewed source text that should intentionally remain unchanged can be listed under translations.no_change instead of being copied into a direct source-to-source translation. Eleven of UG's language overlays use that distinction.

Better authoring formats

  • Added !include support for external descriptions, CSS, card-template HTML, and other supported YAML values.
Example: external description, CSS, and template includes
deck:
  description: !include descriptions/ultimate-geography/en.html

note_types:
  note-type.ultimate-geography:
    styling: !include styles/ultimate-geography/card.css
    card_templates:
      template.country-capital:
        question_format: !include templates/ultimate-geography/country-capital/question.html
        answer_format: !include templates/ultimate-geography/country-capital/answer.html
  • Added structured translatable messages with format, ref, text, and literal
Examples: reusable message parts and translated formats

Before: one long translation key

# src/data/flag_similarity.csv, represented as YAML for comparison
field.flag-similarity: Iceland (blue background, red and white cross), Norway (red background, blue and white cross)
# The Norwegian translation was also one indivisible string
translations:
  direct:
    "Iceland (blue background, red and white cross), Norway (red background, blue and white cross)": "Island (blå bakgrunn, rødt og hvitt kors), Norge (rød bakgrunn, blått og hvitt kors)"

After: reusable translated parts

# deck.yaml, notes.note.faroe-islands
field.flag-similarity:
  format: "{country_1} ({description_1}), {country_2} ({description_2})"
  variables:
    country_1:
      ref: notes.note.iceland.fields.field.country
    country_2:
      ref: notes.note.norway.fields.field.country
    description_1:
      text: blue background, red and white cross
    description_2:
      text: red background, blue and white cross
# overlays/languages/nb.yaml
translations:
  direct:
    Iceland: Island
    Norway: Norge
    blue background, red and white cross: "blå bakgrunn, rødt og hvitt kors"
    red background, blue and white cross: "rød bakgrunn, blått og hvitt kors"

Translating the format itself

Base source:

field.flag-similarity:
  format: "{country_1} ({description_1}), {country_2} ({description_2})"
  variables:
    country_1:
      ref: notes.note.iceland.fields.field.country
    country_2:
      ref: notes.note.norway.fields.field.country
    description_1:
      text: blue background, red and white cross
    description_2:
      text: red background, blue and white cross

The pieces can be translated normally:

translations:
  direct:
    Iceland: Island
    Norway: Norge
    blue background, red and white cross: "blå bakgrunn, rødt og hvitt kors"
    red background, blue and white cross: "rød bakgrunn, blått og hvitt kors"

But the format string itself can also be translated when the target language needs different punctuation, spacing, order, or glue.

Example: Simplified Chinese punctuation
translations:
  direct:
    Iceland: "冰岛"
    Norway: "挪威"
    blue background, red and white cross: "蓝底,红白交叉"
    red background, blue and white cross: "红底,蓝白交叉"
    "{country_1} ({description_1}), {country_2} ({description_2})": "{country_1}({description_1})、{country_2}({description_2})"

Output:

  冰岛(蓝底,红白交叉)、挪威(红底,蓝白交叉)
Example: description before country
translations:
  direct:
    "{country_1} ({description_1}), {country_2} ({description_2})": "{description_1}: {country_1}; {description_2}: {country_2}"

Output:

  blue background, red and white cross: Iceland; red background, blue and white cross: Norway
Example: sentence-style wording
translations:
  direct:
    "{country_1} ({description_1}), {country_2} ({description_2})": "{country_1}: {description_1}; {country_2}: {description_2}."

Output:

  Iceland: blue background, red and white cross; Norway: red background, blue and white cross.
Example: contextual format translation

Use contextual if only one field should use a different format:

translations:
  contextual:
    notes.note:
      faroe-islands.fields.field.flag-similarity.message.format:
        "{country_1} ({description_1}), {country_2} ({description_2})": "{country_1} resembles {country_2}: {description_1} vs {description_2}"

Output for that field only:

  Iceland resembles Norway: blue background, red and white cross vs red background, blue and white cross

Structured media references and integrity

  • Added stable !image references for image-only note fields instead of storing media paths or generated <img> HTML as field content.
  • Hoisted the base media declarations into media.yaml, with stable IDs, safe relative paths, and committed SHA-256 hashes.
  • UG now uses 602 structured image references and 607 hashed media declarations. The remaining five Experimental assets are scripts and styles referenced by card templates, where !image intentionally does not apply.
  • Strict verification checks the real bytes and hashes before export, while clean-tree export stages only the declared assets. It also validates included HTML/CSS and the configured parsed-JSON goldens.
Example: single/multiple images, media declarations, and strict verification

A field can contain one stable image reference or an ordered image sequence, such as UG's blurred and normal Bolivia flags:

# deck.yaml
notes:
  note.bolivia:
    fields:
      field.flag:
        - !image media.ug-flag-bolivia-blur-svg
        - !image media.ug-flag-bolivia-svg
      field.map: !image media.ug-map-bolivia-png

media: !include media.yaml

The stable IDs resolve through the separately maintained declaration map:

# media.yaml
media.ug-flag-bolivia-blur-svg:
  path: ug-flag-bolivia-blur.svg
  sha256: f13669cab4afb991b9851a9c55bb94be5a2c91303a6f8bbb4407a9ffd67951c7
media.ug-flag-bolivia-svg:
  path: ug-flag-bolivia.svg
  sha256: 3010bf58668ac58ae5a1b614867cf94c53b229f2d26679c0ca04cae6d936ced1
media.ug-map-bolivia-png:
  path: ug-map-bolivia.png
  sha256: d461a4cb0d4b845fdc61de1123efca9cc766aaea10dabd95202a1beb980fca7d

This means a file can be renamed by updating its declaration without rewriting every note-field reference. Brain Brew renders the references as safe Anki-compatible <img> tags during export.

brainbrew media hash --manifest brainbrew.yaml --all-targets --media-root media
brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media
brainbrew export crowdanki \
  --manifest brainbrew.yaml \
  --target en-standard \
  --media-root media \
  --out build/crowdanki/en-standard

!image is deliberately limited to whole image-only note fields. Mixed text and images, custom attributes, card templates, styling, scripts, and links remain ordinary HTML/CSS references.

Rust crate release

I had never intend to put Nix/NixOS as a dev dependency, that's just what I happen to use (because it is truly excellent). I simply left it in my test PR rather than put in the effort to actually release anything properly, before it got approved and the effort was worth it 😁

  • Released brainbrew v1.0.0-alpha.3
  • Published brainbrew, brain-brew-core, and brain-brew-formats v1.0.0-alpha.3 to crates.io
  • Added a normal Cargo installation path, so Nix is not required for contributors
  • Kept CI and migration evidence reproducible through the immutable Brain Brew revision 6ee570d427a1a8eec92c22668442f9b7186f9ba7
Example: installation and normal CLI usage

Before

  # Mostly contributor/developer style usage
  nix run . -- --help
  cargo run -- compose --manifest brainbrew.yaml --target de-standard

After

  cargo install brainbrew --version 1.0.0-alpha.3 --locked
  brainbrew compose --manifest brainbrew.yaml --target de-standard

Language-first project metadata

  • Added manifest-level languages
  • Added translation_profile for structural fields, metadata categories, and progress grouping
Example: language-first manifest metadata

Before

targets:
  de-standard:
    overlays:
      - overlay.translation.de
  de-extended:
    overlays:
      - overlay.variant.extended
      - overlay.translation.de

Tools had to infer language/variant meaning from target names.

After

languages:
  en:
    display_name: English
    source: true
    primary_target: standard
    targets:
      experimental: en-experimental
      extended: en-extended
      hardcore-extended: en-hardcore-extended
      hardcore-standard: en-hardcore-standard
      standard: en-standard

  de:
    display_name: German
    translation_overlays:
      base: overlay.translation.de
      hardcore: overlay.translation.hardcore.de
    primary_target: standard
    targets:
      experimental: de-experimental
      extended: de-extended
      hardcore-extended: de-hardcore-extended
      hardcore-standard: de-hardcore-standard
      standard: de-standard

translation_profile:
  structural_fields:
    - field.flag
    - field.map
  metadata_categories:
    - key: deck-metadata
      label: Deck metadata
      paths:
        - deck.name
        - deck.description

This means that all deck extensions can have their own translations, yet still the whole can be understood to be under one language group. This will help tools show all the translations for one language, for all decks/extensions.

Safer extension composition

  • Added sparse extension overlays that can introduce fields, notes, card templates, and media without copying the whole base deck.
  • Added blank-only field_fills, which let Hardcore fill fields owned by UG but fail if another overlay or later base version has already populated them.
  • Destructive changes use explicit replace or override intents with an exact expected base, so upstream drift becomes a conflict instead of being silently overwritten.
Examples: field additions, blank-only fills, and expected-base checks

Experimental adds one field definition and supplies values only where needed:

id: overlay.variant.experimental
kind: extension
field_additions:
  note-type.ultimate-geography:
    fields:
      field.region-code: Region code
    values:
      note.afghanistan:
        field.region-code: AF
      note.albania:
        field.region-code: AL

Hardcore can fill fields that already exist in the shared note model:

id: overlay.extension.hardcore.field-fills
kind: extension
field_fills:
  note.hardcore-bali:
    field.capital: Denpasar
    field.flag:
      - !image media.ug-flag-bali-blur-png
      - !image media.ug-flag-bali-png

A value that intentionally replaces existing source records exactly what it expects to replace:

note_types:
  note-type.ultimate-geography:
    intent: merge
    variables:
      variant.name-suffix:
        intent: replace
        value: " [Extended]"
        expected_base:
          value: ""

Translator workflows

  • Added translation coverage summaries
  • Added translator context views
  • Added interactive apply actions for direct/contextual/no-change/ignore choices
Example: translator CLI workflow

Before

  brainbrew verify --manifest brainbrew.yaml --target da-standard

Then manually inspect YAML failures and decide what was missing/stale/unchanged.

After

  brainbrew translations --manifest brainbrew.yaml --all-targets --summary
  brainbrew translations --manifest brainbrew.yaml --target da-standard --context --status missing
  brainbrew translations --manifest brainbrew.yaml --target da-standard --context --status missing --apply --interactive

Report, summary, and context modes are read-only. Summary mode provides compact per-language and per-overlay counts; context mode shows missing or stale text with its source, target, note, field, card, and duplicate-source context.

--apply edits only the selected translation scope. Non-interactive apply inserts deterministic source-to-source stubs for missing text; interactive apply lets the translator choose direct, contextual, reviewed no_change, ignore, or skip actions.

When English source text changes, an outdated translation can be retained as an explicit stale record rather than being treated as current. Under UG’s lenient policy, Brain Brew warns about that record and continues using its target text until a maintainer resolves it after review; strict translation coverage rejects unresolved stale records. Orphaned dictionary keys, invalid contextual paths or target adaptations, and broken references still fail normal verification.

Deck Workbench

Added a local "Workbench", a webpage one can run which shows the deck contents and allows for editing inline while previewing a Note/Card/Field.

image
Example: Workbench workflow
  brainbrew workbench serve \
    --manifest brainbrew.yaml \
    --enable-write

Then use the local Workbench to review content and edit its underlying source or translation fields:

  - note fields
  - source and translated strings
  - produced cards
  - metadata checklist items
  - comparison languages
  - source edits with translation impact preview

This was a stretch goal I had in mind for a while, but I decided to take a crack at it now. Brain Brew 1.0.0-alpha.3 includes Workbench write support in the normal release. It starts read-only; --enable-write explicitly enables local edits.

The Workbench shows deck content in context, including source and translated text, cards, metadata, and other languages for comparison. Edits remain drafts until someone confirms Apply, which then updates the canonical YAML and owned translation overlays in the local working tree. The write workflow is still being hardened, so it should be used on a version-controlled checkout.

My longer-term goal is to make this a complete translator-facing GUI where people can update translations, create new ones, review the resulting changes, and compare their work with other languages without needing to edit YAML directly.

❗ This tool is very much a work in progress! It still has some sharp edges and strange display stuttering. But these will be fixed! This is just how I imagine a tool could support the workflow much better.

Anyways all of this is up for future consideration/work, but I hope you at least like the direction. I'm open to any and all ideas, it of course matters what people would want to do / how they work want to work. But the more different options the better, in my books!

Discussion Points

Things here are up for discussion. I have taken my own liberties in designing a system I think is good, but am open to being wrong about! Support for the below in Brain Brew does not dictate that UG need use it too. As I said just above more options are better!

Yaml as a format

I hope that the above improvements will help with yaml being the main storage format. Especially when taken as it being the git repo source of truth, not that everyone needs to work via the yaml files (though I personally think it's much nicer to do so now, compared to the csvs before).

I have kept this Brain Brew rewrite working in the same way the old one did: a hub and spoke type of format system. The old system translated everything into "Deck Parts" before then translating to/from CrowdAnki/CSV. This new system has the same general hub-and-spoke shape, except I have made the Canonical Deck YAML itself the stored intermediate representation. That makes it straightforward to add CSV export and import back in, including a workflow where someone exports to CSV, edits it, and imports it again. The CSV adapter is not implemented in alpha.3 yet; it is an option I can add if that is something people want. I still think the yaml is much better than that old system.

English as the Translation field

I truly believe that this is a necessary evil (hopefully less evil now that I've gotten rid of the big long horrible strings). I've also considered using stable note IDs or country codes as translation keys, with English treated as just another translation. Those identifiers solve identity, but by themselves give translators even less context: they still need the source text, note, field, card, and related translations to understand what they are editing. The new context views and Workbench are intended to make the current English-source model much easier to work with.

The workbench editing workflow will help a lot with this problem too.

Hardcore geography

  • Where should it be stored: this repo/another repo?
  • What should it contain?
  • How should it be organised?

All I present for it is that it is doable to have it here in this repo! It was simpler for my demo to have it here too.

The problem is that now if someone imports UG, imports HG, and then updates UG (without updating HG) all of the HG cards of the overlapping notes will disappear.

Oh this was totally an oversight on my part. I have preserved the historical UG note GUIDs rather than allowing them to be regenerated, as well as the 45 meaningful historical Hardcore note GUIDs. The canonical source uses readable stable note IDs for composition, while explicit adapter_ids.crowdanki:guid values preserve the separate identity Anki uses during import and update.

I also reorganised Hardcore so that standalone and companion exports reuse the same 45-note content overlay. The companion keeps its own deck identity, while its note GUIDs, fields, tags, and note model stay aligned with the corresponding standalone deck. The migration evidence collector checks this relationship for all 12 localised Hardcore language pairs.

That gives us an explicit structural check for the identity/model behaviour behind this import-update concern, rather than relying only on generated GUID overrides. I would still want the final workflow exercised in Anki before treating that as a complete end-to-end import guarantee.

@jeprecated

jeprecated commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Ahh! 🙈 Somehow I accidentally pushed up the changes into master here. I have now reverted it, which required me to disable the branch security force push policy. My deepest apologies. It was up for about an hour, I believe.

This is very confusing as the branch policy already had "Require Pull Requests" so I'm not even sure how I was able to do it in the first place 🤔

Either way I'll update my own local remotes to make sure this cannot happen again. Feel free and revoke my access to master anyways, I'm completely fine with that 🙏

Edit: Ahhh I see. All branch protection rules have the following setting one must tick, otherwise none of it applies to Admins 🙈
image

Seems rather odd. Would be better with the following settings to select who can force push explicitly, and just enable it when needed (imo)

image

And since I used jj to push to my own master branch in my fork it decided to push to master on each remote, because it could... damn. Quite unexpected. I will change my own branch so that does not occur again due to this.

@jeprecated

Copy link
Copy Markdown
Member Author

Replaced this PR with #743 coming from the right branch (Github does not allow one to change branch targets on an open PR 😢 ) so that this dumb issue does not happen again.

Sorry to break up the conversation, feel free and respond here it you like 👍

@jeprecated jeprecated closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants